diff --git a/tools/mapcompile/constants.py b/tools/mapcompile/constants.py new file mode 100644 index 0000000..30a795d --- /dev/null +++ b/tools/mapcompile/constants.py @@ -0,0 +1,11 @@ +# Values defined within C +CHUNK_WIDTH = 8 +CHUNK_HEIGHT = 8 +CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT +CHUNK_ENTITY_COUNT_MAX = 8 +TILE_WIDTH_HEIGHT = 16 +TILE_WIDTH_HEIGHT = 16 + +ENTITY_TYPE_MAP = { + "npc": "ENTITY_TYPE_NPC", +} \ No newline at end of file diff --git a/tools/mapcompile/entityParse.py b/tools/mapcompile/entityParse.py new file mode 100644 index 0000000..ce306ce --- /dev/null +++ b/tools/mapcompile/entityParse.py @@ -0,0 +1,20 @@ +import os +import sys + +def parseEntities(layers): + parsed = { + 'playerSpawnX': 0, + 'playerSpawnY': 0, + } + + for ob in layers['objectLayer']['objects']: + if 'type' not in ob or ob['type'] != 'player_spawn': + continue + if 'x' not in ob or 'y' not in ob: + print(f"Error: Player spawn object does not contain 'x' or 'y' key.") + sys.exit(1) + parsed['playerSpawnX'] = ob['x'] + parsed['playerSpawnY'] = ob['y'] + break + + return parsed \ No newline at end of file diff --git a/tools/mapcompile/helper.py b/tools/mapcompile/helper.py new file mode 100644 index 0000000..dcffc6a --- /dev/null +++ b/tools/mapcompile/helper.py @@ -0,0 +1,5 @@ +def floatToFixed248(value): + # Converts a float to the fixed248_t used internally. + high24 = int(value) & 0xFFFFFF + low8 = int((value * 256.0 - high24) * 256.0) & 0xFF + return (high24 << 8) | low8 \ No newline at end of file diff --git a/tools/mapcompile/inputParser.py b/tools/mapcompile/inputParser.py new file mode 100644 index 0000000..c0863fb --- /dev/null +++ b/tools/mapcompile/inputParser.py @@ -0,0 +1,32 @@ +import os +import sys +import json + +def parseInputFile(inputFile): + if not os.path.isfile(inputFile): + print(f"Error: Input file '{inputFile}' does not exist.") + sys.exit(1) + + data = None + with open(inputFile, 'r') as f: + data = json.load(f) + + # Data should have height key + if 'height' not in data or 'width' not in data: + print(f"Error: Input file '{inputFile}' does not contain 'height' or 'width' key.") + sys.exit(1) + + if 'tilewidth' not in data or 'tileheight' not in data: + print(f"Error: Input file '{inputFile}' does not contain 'tilewidth' or 'tileheight' key.") + sys.exit(1) + + if 'infinite' not in data or not isinstance(data['infinite'], bool): + print(f"Error: Input file '{inputFile}' does not contain 'infinite' key.") + sys.exit(1) + + # Need layers + if 'layers' not in data or not isinstance(data['layers'], list) or len(data['layers']) == 0: + print(f"Error: Input file '{inputFile}' does not contain 'layers' key.") + sys.exit(1) + + return data \ No newline at end of file diff --git a/tools/mapcompile/layerParser.py b/tools/mapcompile/layerParser.py new file mode 100644 index 0000000..d58f007 --- /dev/null +++ b/tools/mapcompile/layerParser.py @@ -0,0 +1,144 @@ +import sys +from constants import TILE_WIDTH_HEIGHT, CHUNK_WIDTH, CHUNK_HEIGHT +import math + +def parseLayers(data): + parsed = {} + + # Extract layers + parsed['layers'] = data['layers'] + + # Object layer + for layer in parsed['layers']: + if layer.get('type') == 'objectgroup': + parsed['objectLayer'] = layer + break + + if parsed['objectLayer'] is None: + print(f"Error: Data does not contain an object layer.") + sys.exit(1) + + if 'objects' not in parsed['objectLayer'] or not isinstance(parsed['objectLayer']['objects'], list): + print(f"Error: Object layer does not contain 'objects' key or it is not a list.") + sys.exit(1) + + # Tile Layers + parsed['tileLayers'] = [] + for layer in parsed['layers']: + if layer.get('type') == 'tilelayer': + parsed['tileLayers'].append(layer) + + if len(parsed['tileLayers']) == 0: + print(f"Error: Data does not contain any tile layers.") + sys.exit(1) + + # First layer + parsed['firstLayer'] = parsed['tileLayers'][0] + if 'width' not in parsed['firstLayer'] or 'height' not in parsed['firstLayer']: + print(f"Error: First layer does not contain 'width' or 'height' key.") + sys.exit(1) + + if 'chunks' not in parsed['firstLayer'] or not isinstance(parsed['firstLayer']['chunks'], list): + print(f"Error: First layer does not contain 'chunks' key.") + sys.exit(1) + + if len(parsed['firstLayer']['chunks']) == 0: + print(f"Error: First layer does not contain any chunks.") + sys.exit(1) + + parsed['firstLayerFirstChunk'] = parsed['firstLayer']['chunks'][0] + + # Now determine the input map bounds. + isMinXFound = False + isMaxXFound = False + isMinYFound = False + isMaxYFound = False + parsed['inputMapLowestX'] = 0 + parsed['inputMapHighestX'] = 0 + parsed['inputMapLowestY'] = 0 + parsed['inputMapHighestY'] = 0 + parsed['inputLayerWidthInTiles'] = parsed['firstLayerFirstChunk']['width'] + parsed['inputLayerHeightInTiles'] = parsed['firstLayerFirstChunk']['height'] + + for chunk in parsed['firstLayer']['chunks']: + if 'x' not in chunk or 'y' not in chunk: + print(f"Error: Chunk in first layer does not contain 'x' or 'y' key.") + sys.exit(1) + + # Check chunk is not empty + if 'data' not in chunk or not isinstance(chunk['data'], list): + print(f"Error: Chunk in first layer does not contain 'data' key or it is not a list.") + sys.exit(1) + + if len(chunk['data']) != parsed['inputLayerWidthInTiles'] * parsed['inputLayerHeightInTiles']: + print(f"Error: Chunk in first layer does not contain the expected number of tiles.") + sys.exit(1) + + chunkEmpty = True + for tile in chunk['data']: + if tile == 0: + continue + chunkEmpty = False + break + + if chunkEmpty: + print(f"Warning: Chunk at ({chunk['x']}, {chunk['y']}) is empty, skipping.") + continue + + chunkX = chunk['x'] + chunkY = chunk['y'] + + if parsed['inputMapLowestX'] > chunkX or not isMinXFound: + parsed['inputMapLowestX'] = chunkX + isMinXFound = True + if parsed['inputMapHighestX'] < chunkX or not isMaxXFound: + parsed['inputMapHighestX'] = chunkX + isMaxXFound = True + + if parsed['inputMapLowestY'] > chunkY or not isMinYFound: + parsed['inputMapLowestY'] = chunkY + isMinYFound = True + if parsed['inputMapHighestY'] < chunkY or not isMaxYFound: + parsed['inputMapHighestY'] = chunkY + isMaxYFound = True + + parsed['inputMapHighestX'] += parsed['inputLayerWidthInTiles'] + parsed['inputMapHighestY'] += parsed['inputLayerHeightInTiles'] + + # We now offset all chunks by the lowest X/Y values to make them start at (0, 0). + for layerIndex, layer in enumerate(parsed['tileLayers']): + for chunkIndex, chunk in enumerate(layer['chunks']): + chunk['x'] -= parsed['inputMapLowestX'] + chunk['y'] -= parsed['inputMapLowestY'] + layer['chunks'][chunkIndex] = chunk + + parsed['layers'][layerIndex] = layer + + # Same for object layers + for obIndex, ob in enumerate(parsed['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) + + ob['x'] -= parsed['inputMapLowestX'] * TILE_WIDTH_HEIGHT + ob['y'] -= parsed['inputMapLowestY'] * TILE_WIDTH_HEIGHT + + # Objects are bottom aligned in tiled, so we need to adjust the Y coordinate. + ob['y'] -= TILE_WIDTH_HEIGHT + + # Round off the coordinates + ob['x'] = round(ob['x']) + ob['y'] = round(ob['y']) + + parsed['objectLayer']['objects'][obIndex] = ob + + parsed['mapWidthInTiles'] = parsed['inputMapHighestX'] - parsed['inputMapLowestX'] + parsed['mapHeightInTiles'] = parsed['inputMapHighestY'] - parsed['inputMapLowestY'] + parsed['mapWidthInRealChunks'] = math.ceil(float(parsed['mapWidthInTiles']) / float(CHUNK_WIDTH)) + parsed['mapHeightInRealChunks'] = math.ceil(float(parsed['mapHeightInTiles']) / float(CHUNK_HEIGHT)) + + if parsed['inputLayerWidthInTiles'] < CHUNK_WIDTH or parsed['inputLayerHeightInTiles'] < CHUNK_HEIGHT: + print(f"Error: Input layer size is smaller than chunk size.") + sys.exit(1) + + return parsed \ No newline at end of file diff --git a/tools/mapcompile/mapcompile.py b/tools/mapcompile/mapcompile.py index 7b974b7..c423912 100644 --- a/tools/mapcompile/mapcompile.py +++ b/tools/mapcompile/mapcompile.py @@ -1,27 +1,12 @@ import sys, os import argparse from datetime import datetime -import json import math - -# Values defined within C -CHUNK_WIDTH = 8 -CHUNK_HEIGHT = 8 -CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT -CHUNK_ENTITY_COUNT_MAX = 8 -TILE_WIDTH_HEIGHT = 16 -TILE_WIDTH_HEIGHT = 16 - -ENTITY_TYPE_MAP = { - "npc": "ENTITY_TYPE_NPC", -} - -# Helper functions -def floatToFixed248(value): - # Converts a float to the fixed248_t used internally. - high24 = int(value) & 0xFFFFFF - low8 = int((value * 256.0 - high24) * 256.0) & 0xFF - return (high24 << 8) | low8 +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") @@ -45,190 +30,28 @@ os.makedirs(chunksDir, exist_ok=True) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Read the input JSON file -inputFile = args.input -if not os.path.isfile(inputFile): - print(f"Error: Input file '{inputFile}' does not exist.") - sys.exit(1) - -with open(inputFile, 'r') as f: - data = json.load(f) - -# Data should have height key -if 'height' not in data or 'width' not in data: - print(f"Error: Input file '{inputFile}' does not contain 'height' or 'width' key.") - sys.exit(1) - -if 'tilewidth' not in data or 'tileheight' not in data: - print(f"Error: Input file '{inputFile}' does not contain 'tilewidth' or 'tileheight' key.") - sys.exit(1) - -if 'infinite' not in data or not isinstance(data['infinite'], bool): - print(f"Error: Input file '{inputFile}' does not contain 'infinite' key.") - sys.exit(1) - -# Need layers -if 'layers' not in data or not isinstance(data['layers'], list): - print(f"Error: Input file '{inputFile}' does not contain 'layers' key.") - sys.exit(1) - -layers = data['layers'] -if len(layers) == 0: - print(f"Error: Input file '{inputFile}' does not contain any layers.") - sys.exit(1) - -# Object layer -objectLayer = None -for layer in layers: - if layer.get('type') == 'objectgroup': - objectLayer = layer - break - -if objectLayer is None: - print(f"Error: Input file '{inputFile}' does not contain an object layer.") - sys.exit(1) - -if 'objects' not in objectLayer or not isinstance(objectLayer['objects'], list): - print(f"Error: Object layer in '{inputFile}' does not contain 'objects' key or it is not a list.") - sys.exit(1) - -# Tile Layers -tileLayers = [] -for layer in layers: - if layer.get('type') == 'tilelayer': - tileLayers.append(layer) - -if len(tileLayers) == 0: - print(f"Error: Input file '{inputFile}' does not contain any tile layers.") - sys.exit(1) - -# First layer -firstLayer = tileLayers[0] -if 'width' not in firstLayer or 'height' not in firstLayer: - print(f"Error: First layer in '{inputFile}' does not contain 'width' or 'height' key.") - sys.exit(1) - -if 'chunks' not in firstLayer or not isinstance(firstLayer['chunks'], list): - print(f"Error: First layer in '{inputFile}' does not contain 'chunks' key.") - sys.exit(1) - -if len(firstLayer['chunks']) == 0: - print(f"Error: First layer in '{inputFile}' does not contain any chunks.") - sys.exit(1) - -firstLayerFirstChunk = firstLayer['chunks'][0] - -# Now determine the input map bounds. -isMinXFound = False -isMaxXFound = False -isMinYFound = False -isMaxYFound = False -inputMapLowestX = 0 -inputMapHighestX = 0 -inputMapLowestY = 0 -inputMapHighestY = 0 -inputLayerWidthInTiles = firstLayerFirstChunk['width'] -inputLayerHeightInTiles = firstLayerFirstChunk['height'] - -for chunk in firstLayer['chunks']: - if 'x' not in chunk or 'y' not in chunk: - print(f"Error: Chunk in first layer does not contain 'x' or 'y' key.") - sys.exit(1) - - # Check chunk is not empty - if 'data' not in chunk or not isinstance(chunk['data'], list): - print(f"Error: Chunk in first layer does not contain 'data' key or it is not a list.") - sys.exit(1) - - if len(chunk['data']) != inputLayerWidthInTiles * inputLayerHeightInTiles: - print(f"Error: Chunk in first layer does not contain the expected number of tiles ({inputLayerWidthInTiles * inputLayerHeightInTiles}).") - sys.exit(1) - - chunkEmpty = True - for tile in chunk['data']: - if tile == 0: - continue - chunkEmpty = False - break - - if chunkEmpty: - print(f"Warning: Chunk at ({chunk['x']}, {chunk['y']}) is empty, skipping.") - continue - - chunkX = chunk['x'] - chunkY = chunk['y'] - - if inputMapLowestX > chunkX or not isMinXFound: - inputMapLowestX = chunkX - isMinXFound = True - if inputMapHighestX < chunkX or not isMaxXFound: - inputMapHighestX = chunkX - isMaxXFound = True - - if inputMapLowestY > chunkY or not isMinYFound: - inputMapLowestY = chunkY - isMinYFound = True - if inputMapHighestY < chunkY or not isMaxYFound: - inputMapHighestY = chunkY - isMaxYFound = True - -inputMapHighestX += inputLayerWidthInTiles -inputMapHighestY += inputLayerHeightInTiles - -# We now offset all chunks by the lowest X/Y values to make them start at (0, 0). -for layerIndex, layer in enumerate(tileLayers): - for chunkIndex, chunk in enumerate(layer['chunks']): - chunk['x'] -= inputMapLowestX - chunk['y'] -= inputMapLowestY - layer['chunks'][chunkIndex] = chunk - - layers[layerIndex] = layer - - -# Pre generate entity data -for obIndex, ob in enumerate(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) - - ob['x'] -= inputMapLowestX * TILE_WIDTH_HEIGHT - ob['y'] -= inputMapLowestY * TILE_WIDTH_HEIGHT - - # Objects are bottom aligned in tiled, so we need to adjust the Y coordinate. - ob['y'] -= TILE_WIDTH_HEIGHT - - # Round off the coordinates - ob['x'] = round(ob['x']) - ob['y'] = round(ob['y']) - - objectLayer['objects'][obIndex] = ob - -mapWidthInTiles = inputMapHighestX - inputMapLowestX -mapHeightInTiles = inputMapHighestY - inputMapLowestY -mapWidthInRealChunks = math.ceil(float(mapWidthInTiles) / float(CHUNK_WIDTH)) -mapHeightInRealChunks = math.ceil(float(mapHeightInTiles) / float(CHUNK_HEIGHT)) - -if inputLayerWidthInTiles < CHUNK_WIDTH or inputLayerHeightInTiles < CHUNK_HEIGHT: - print(f"Error: Input layer size {inputLayerWidthInTiles}x{inputLayerHeightInTiles} is smaller than chunk size {CHUNK_WIDTH}x{CHUNK_HEIGHT}.") - sys.exit(1) +data = parseInputFile(args.input) +layers = parseLayers(data) +entityData = parseEntities(layers) # For each output chunk. worldWidth = 0 worldHeight = 0 chunksDone = set() -for chunkY in range(mapHeightInRealChunks): - for chunkX in range(mapWidthInRealChunks): +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(inputLayerWidthInTiles)) * inputLayerWidthInTiles - inputTopLeftTileY = math.floor(float(topLeftTileY) / float(inputLayerHeightInTiles)) * inputLayerHeightInTiles + 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 tileLayers: + for layer in layers['tileLayers']: foundChunk = None if 'chunks' not in layer or not isinstance(layer['chunks'], list): @@ -269,7 +92,7 @@ for chunkY in range(mapHeightInRealChunks): continue entities = [] - for ob in objectLayer['objects']: + 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) @@ -288,17 +111,17 @@ for chunkY in range(mapHeightInRealChunks): # Shorthand functions def getInputLocalTileX(absoluteTileX): - return absoluteTileX % inputLayerWidthInTiles + return absoluteTileX % layers['inputLayerWidthInTiles'] def getInputLocalTileY(absoluteTileY): - return absoluteTileY % inputLayerHeightInTiles + return absoluteTileY % layers['inputLayerHeightInTiles'] def getInputTileIndex(localX, localY): absoluteTileX = topLeftTileX + localX absoluteTileY = topLeftTileY + localY inputLocalTileX = getInputLocalTileX(absoluteTileX) inputLocalTileY = getInputLocalTileY(absoluteTileY) - return inputLocalTileY * inputLayerWidthInTiles + inputLocalTileX + return inputLocalTileY * layers['inputLayerWidthInTiles'] + inputLocalTileX def getOutputTileIndex(localX, localY): return localY * CHUNK_WIDTH + localX @@ -383,20 +206,6 @@ for chunkY in range(mapHeightInRealChunks): f.write("};\n\n") -# Determine map global things -playerSpawnX = 0 -playerSpawnY = 0 - -for ob in objectLayer['objects']: - if 'type' not in ob or ob['type'] != 'player_spawn': - continue - if 'x' not in ob or 'y' not in ob: - print(f"Error: Player spawn object does not contain 'x' or 'y' key.") - sys.exit(1) - playerSpawnX = ob['x'] - playerSpawnY = ob['y'] - break - # Output header file. headerPath = os.path.join(worldDir, "world.h") with open(headerPath, 'w') as f: @@ -422,7 +231,7 @@ with open(headerPath, 'w') as f: f.write("NULL, ") f.write("\n") f.write("};\n\n") - f.write(f"#define WORLD_PLAYER_SPAWN_X (fixed248_t){floatToFixed248(playerSpawnX)}\n") - f.write(f"#define WORLD_PLAYER_SPAWN_Y (fixed248_t){floatToFixed248(playerSpawnY)}\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}") \ No newline at end of file