import sys, os import argparse from datetime import datetime import math from helper import floatToFixed248 from inputParser import parseInputFile from layerParser import parseLayers from entityParse import parseEntities from constants import CHUNK_WIDTH, CHUNK_HEIGHT, TILE_WIDTH_HEIGHT, ENTITY_TYPE_MAP, CHUNK_TILE_COUNT # Check if the script is run with the correct arguments parser = argparse.ArgumentParser(description="Generate chunk header files") parser.add_argument('--output', required=True, help='Dir to output headers') parser.add_argument('--input', required=True, help='Input JSON file from tiled') args = parser.parse_args() # Ensure outdir exists outputDir = args.output os.makedirs(outputDir, exist_ok=True) # Create world directory if it does not exist worldDir = os.path.join(outputDir, "world") os.makedirs(worldDir, exist_ok=True) # Create chunks directory if it does not exist chunksDir = os.path.join(worldDir, "chunk") os.makedirs(chunksDir, exist_ok=True) # Some vars used during printing now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Read the input JSON file data = parseInputFile(args.input) layers = parseLayers(data) entityData = parseEntities(layers) # For each output chunk. worldWidth = 0 worldHeight = 0 chunksDone = set() for chunkY in range(layers['mapHeightInRealChunks']): for chunkX in range(layers['mapWidthInRealChunks']): # Top left X/Y based on real chunk size topLeftTileX = chunkX * CHUNK_WIDTH topLeftTileY = chunkY * CHUNK_HEIGHT # Top left coordinates based on input layer size inputTopLeftTileX = math.floor(float(topLeftTileX) / float(layers['inputLayerWidthInTiles'])) * layers['inputLayerWidthInTiles'] inputTopLeftTileY = math.floor(float(topLeftTileY) / float(layers['inputLayerHeightInTiles'])) * layers['inputLayerHeightInTiles'] # Get the layers for this chunk. chunkLayers = [] for layer in layers['tileLayers']: foundChunk = None if 'chunks' not in layer or not isinstance(layer['chunks'], list): print(f"Error: Layer '{layer['name']}' does not contain 'chunks' key or it is not a list.") sys.exit(1) for chunk in layer['chunks']: if 'x' not in chunk or 'y' not in chunk: print(f"Error: Chunk in layer '{layer['name']}' does not contain 'x' or 'y' key.") sys.exit(1) # Check if this chunk is within the bounds of the top left tile. if chunk['x'] != inputTopLeftTileX or chunk['y'] != inputTopLeftTileY: continue foundChunk = chunk break if foundChunk is None: chunkLayers.append(None) continue # Is layer empty? layerEmpty = True for tile in foundChunk['data']: if tile == 0: continue layerEmpty = False break if layerEmpty: chunkLayers.append(None) else: chunkLayers.append(foundChunk) # Now we have a chunkLayers list with the found chunks for each layer. if all(chunk is None for chunk in chunkLayers) or len(chunkLayers) == 0: continue entities = [] for ob in layers['objectLayer']['objects']: if 'x' not in ob or 'y' not in ob: print(f"Error: Object in object layer does not contain 'x' or 'y' key.") sys.exit(1) # Is this object within the chunk? if ob['x'] < topLeftTileX * TILE_WIDTH_HEIGHT: continue if ob['x'] >= (topLeftTileX + CHUNK_WIDTH) * TILE_WIDTH_HEIGHT: continue if ob['y'] < topLeftTileY * TILE_WIDTH_HEIGHT: continue if ob['y'] >= (topLeftTileY + CHUNK_HEIGHT) * TILE_WIDTH_HEIGHT: continue entities.append(ob) # Shorthand functions def getInputLocalTileX(absoluteTileX): return absoluteTileX % layers['inputLayerWidthInTiles'] def getInputLocalTileY(absoluteTileY): return absoluteTileY % layers['inputLayerHeightInTiles'] def getInputTileIndex(localX, localY): absoluteTileX = topLeftTileX + localX absoluteTileY = topLeftTileY + localY inputLocalTileX = getInputLocalTileX(absoluteTileX) inputLocalTileY = getInputLocalTileY(absoluteTileY) return inputLocalTileY * layers['inputLayerWidthInTiles'] + inputLocalTileX def getOutputTileIndex(localX, localY): return localY * CHUNK_WIDTH + localX # Determine the layer base. layerBase = chunkLayers[0] layerBaseOverlay = None if len(chunkLayers) > 1: layerBaseOverlay = chunkLayers[1] # Determine base layer data. layerBaseData = [] for y in range(CHUNK_HEIGHT): for x in range(CHUNK_WIDTH): inputTileIndex = getInputTileIndex(x, y) outputTileIndex = getOutputTileIndex(x, y) layerBaseData.append(layerBase['data'][inputTileIndex]) if len(layerBaseData) != CHUNK_TILE_COUNT: print(f"Error: Layer base data length {len(layerBaseData)} does not match expected chunk tile count {CHUNK_TILE_COUNT}.") sys.exit(1) # Layer base overlay. layerOverlayData = [] if layerBaseOverlay is not None: for y in range(CHUNK_HEIGHT): for x in range(CHUNK_WIDTH): inputTileIndex = getInputTileIndex(x, y) outputTileIndex = getOutputTileIndex(x, y) layerOverlayData.append(layerBaseOverlay['data'][inputTileIndex]) # This is a valid chunk. worldWidth = max(worldWidth, chunkX + 1) worldHeight = max(worldHeight, chunkY + 1) chunksDone.add((chunkX, chunkY)) chunkHeaderPath = os.path.join(chunksDir, f"chunk_{chunkX}_{chunkY}.h") with open(chunkHeaderPath, 'w') as f: f.write(f"// Generated chunk header for chunk at position ({chunkX}, {chunkY})\n") f.write(f"// Generated at {now}\n") f.write("#pragma once\n") f.write("#include \"world/chunkdata.h\"\n\n") f.write(f"static const chunkdata_t CHUNK_{chunkX}_{chunkY} = {{\n") f.write(f" .layerBase = {{\n") for y in range(CHUNK_HEIGHT): f.write(f" ") for x in range(CHUNK_WIDTH): i = y * CHUNK_WIDTH + x byte = layerBaseData[i] f.write(f"0x{byte:02x}, ") f.write(f"\n") f.write(" },\n\n") f.write(" .layerBaseOverlay = {\n") if layerBaseOverlay is not None: for y in range(CHUNK_HEIGHT): f.write(f" ") for x in range(CHUNK_WIDTH): i = y * CHUNK_WIDTH + x byte = layerOverlayData[i] f.write(f"0x{byte:02x}, ") f.write(f"\n") f.write(" },\n\n") f.write(f" .entities = {{\n") for entity in entities: # Entities are center aligned in tiled. localX = round(entity['x'] - (topLeftTileX * TILE_WIDTH_HEIGHT)) localY = round(entity['y'] - (topLeftTileY * TILE_WIDTH_HEIGHT)) if 'type' in entity and entity['type'] not in ENTITY_TYPE_MAP: continue f.write(" {\n") f.write(f" .id = {entity['id']},\n") f.write(f" .type = ENTITY_TYPE_NPC,\n") f.write(f" .x = {localX},\n") f.write(f" .y = {localY},\n") f.write(f" .dir = ENTITY_DIR_SOUTH,\n") f.write(" },\n") f.write(f" }},\n") f.write("};\n\n") # Output header file. headerPath = os.path.join(worldDir, "world.h") with open(headerPath, 'w') as f: f.write(f"// Generated chunks file. Generated at {now}\n\n") f.write("#pragma once\n") f.write("#include \"dusk.h\"\n") # Now, for each chunk, include its header file for (x, y) in chunksDone: chunk_header = f"world/chunk/chunk_{x}_{y}.h" f.write(f"#include \"{chunk_header}\"\n") f.write("\n") f.write(f"#define WORLD_WIDTH {worldWidth}\n") f.write(f"#define WORLD_HEIGHT {worldHeight}\n\n") f.write(f"static const chunkdata_t* WORLD_CHUNKS[] = {{\n") for i in range(worldHeight): f.write(" ") for j in range(worldWidth): if(j, i) in chunksDone: f.write(f"&CHUNK_{j}_{i}, ") else: f.write("NULL, ") f.write("\n") f.write("};\n\n") f.write(f"#define WORLD_PLAYER_SPAWN_X ((fixed248_t){floatToFixed248(entityData['playerSpawnX'])})\n") f.write(f"#define WORLD_PLAYER_SPAWN_Y ((fixed248_t){floatToFixed248(entityData['playerSpawnY'])})\n") print(f"chunks.h generated at: {headerPath}")