diff --git a/assets/CMakeLists.txt b/assets/CMakeLists.txt index 660e4ad..334492b 100644 --- a/assets/CMakeLists.txt +++ b/assets/CMakeLists.txt @@ -7,4 +7,5 @@ add_subdirectory(palette)# Palette asset needs to be added before any images. add_subdirectory(config) add_subdirectory(entity) +add_subdirectory(map) add_subdirectory(ui) \ No newline at end of file diff --git a/assets/entity/entities.tsx b/assets/entity/entities.tsx index ede9f43..c7d7ecf 100644 --- a/assets/entity/entities.tsx +++ b/assets/entity/entities.tsx @@ -1,4 +1,4 @@ - + diff --git a/assets/map/CMakeLists.txt b/assets/map/CMakeLists.txt new file mode 100644 index 0000000..4f7a2b6 --- /dev/null +++ b/assets/map/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +add_asset(MAP untitled.tmx) \ No newline at end of file diff --git a/assets/map/untitled.tmx b/assets/map/untitled.tmx new file mode 100644 index 0000000..9f69427 --- /dev/null +++ b/assets/map/untitled.tmx @@ -0,0 +1,28 @@ + + + + + +2,3,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +9,10,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +16,17,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + diff --git a/assets/palette/CMakeLists.txt b/assets/palette/CMakeLists.txt index 947af28..d8eda53 100644 --- a/assets/palette/CMakeLists.txt +++ b/assets/palette/CMakeLists.txt @@ -3,4 +3,4 @@ # This software is released under the MIT License. # https://opensource.org/licenses/MIT -add_asset(PALETTE pallet0.png) \ No newline at end of file +add_asset(PALETTE palette0.png) \ No newline at end of file diff --git a/assets/palette/palette0.png b/assets/palette/palette0.png new file mode 100644 index 0000000..e60ee7f Binary files /dev/null and b/assets/palette/palette0.png differ diff --git a/assets/palette/palette0.pxo b/assets/palette/palette0.pxo new file mode 100644 index 0000000..070274a Binary files /dev/null and b/assets/palette/palette0.pxo differ diff --git a/assets/palette/pallet0.png b/assets/palette/pallet0.png deleted file mode 100644 index c611a09..0000000 Binary files a/assets/palette/pallet0.png and /dev/null differ diff --git a/assets/tileset/prarie.png b/assets/tileset/prarie.png new file mode 100644 index 0000000..324cdd0 Binary files /dev/null and b/assets/tileset/prarie.png differ diff --git a/assets/tileset/prarie.pxo b/assets/tileset/prarie.pxo new file mode 100644 index 0000000..34d9b81 Binary files /dev/null and b/assets/tileset/prarie.pxo differ diff --git a/assets/tileset/prarie.tsx b/assets/tileset/prarie.tsx new file mode 100644 index 0000000..dfaa242 --- /dev/null +++ b/assets/tileset/prarie.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/assets/untitled.tiled-project b/assets/untitled.tiled-project new file mode 100644 index 0000000..d0eb592 --- /dev/null +++ b/assets/untitled.tiled-project @@ -0,0 +1,14 @@ +{ + "automappingRulesFile": "", + "commands": [ + ], + "compatibilityVersion": 1100, + "extensionsPath": "extensions", + "folders": [ + "." + ], + "properties": [ + ], + "propertyTypes": [ + ] +} diff --git a/assets/untitled.tiled-session b/assets/untitled.tiled-session new file mode 100644 index 0000000..0620966 --- /dev/null +++ b/assets/untitled.tiled-session @@ -0,0 +1,56 @@ +{ + "Map/SizeTest": { + "height": 4300, + "width": 2 + }, + "activeFile": "map/untitled.tmx", + "expandedProjectPaths": [ + "map", + ".", + "tileset" + ], + "file.lastUsedOpenFilter": "All Files (*)", + "fileStates": { + "entity/entities.tsx": { + "scaleInDock": 1, + "scaleInEditor": 1 + }, + "map/prarie.tsx": { + "scaleInDock": 1, + "scaleInEditor": 11 + }, + "map/untitled.tmx": { + "scale": 3.8326562499999994, + "selectedLayer": 0, + "viewCenter": { + "x": 169.5951730604591, + "y": 108.41045293326268 + } + }, + "tileset/prarie.tsx": { + "scaleInDock": 1, + "scaleInEditor": 1 + } + }, + "last.imagePath": "/home/yourwishes/htdocs/dusk/assets/tileset", + "map.lastUsedFormat": "tmx", + "map.tileHeight": 16, + "map.tileWidth": 16, + "openFiles": [ + "entity/entities.tsx", + "map/untitled.tmx", + "tileset/prarie.tsx" + ], + "project": "untitled.tiled-project", + "recentFiles": [ + "entity/entities.tsx", + "tileset/prarie.tsx", + "map/untitled.tmx", + "map/prarie.tsx" + ], + "tileset.lastUsedFormat": "tsx", + "tileset.tileSize": { + "height": 16, + "width": 16 + } +} diff --git a/data/map project.tiled-session b/data/map project.tiled-session index 921a6ac..aaf4404 100644 --- a/data/map project.tiled-session +++ b/data/map project.tiled-session @@ -1,10 +1,26 @@ { - "activeFile": "map.tmj", + "Map/SizeTest": { + "height": 4300, + "width": 2 + }, + "activeFile": "/home/yourwishes/htdocs/dusk/assets/map/untitled.tmx", "expandedProjectPaths": [ - ".", - "templates" + "templates", + "." ], "fileStates": { + "/home/yourwishes/htdocs/dusk/assets/entity/entities.tsx": { + "scaleInDock": 1, + "scaleInEditor": 1 + }, + "/home/yourwishes/htdocs/dusk/assets/map/untitled.tmx": { + "scale": 1.9163281249999997, + "selectedLayer": 0, + "viewCenter": { + "x": 546.8792042072649, + "y": 320.14350360797425 + } + }, ":/automap-tiles.tsx": { "scaleInDock": 1 }, @@ -16,8 +32,8 @@ "scale": 3, "selectedLayer": 2, "viewCenter": { - "x": 6603.333333333333, - "y": 6846.5 + "x": 6912, + "y": 6911.833333333333 } }, "minogram.tsx": { @@ -32,17 +48,20 @@ "last.externalTilesetPath": "/home/yourwishes/htdocs/dusk/data", "last.imagePath": "/home/yourwishes/htdocs/dusk/data", "last.objectTemplatePath": "/home/yourwishes/htdocs/dusk/data/templates", + "map.lastUsedFormat": "tmx", "openFiles": [ - "map.tmj", - "overworld.tsx" + "/home/yourwishes/htdocs/dusk/assets/map/untitled.tmx", + "/home/yourwishes/htdocs/dusk/assets/entity/entities.tsx" ], "project": "map project.tiled-project", "property.type": "string", "recentFiles": [ - "overworld.tsx", + "/home/yourwishes/htdocs/dusk/assets/entity/entities.tsx", + "/home/yourwishes/htdocs/dusk/assets/map/untitled.tmx", "map.tmj", - "minogram.tsx", - "entities.tsx" + "overworld.tsx", + "entities.tsx", + "minogram.tsx" ], "tileset.lastUsedFilter": "Tiled tileset files (*.tsx *.xml)", "tileset.lastUsedFormat": "tsx", diff --git a/data/map.tmj b/data/map.tmj index c310d4a..f5b013c 100644 --- a/data/map.tmj +++ b/data/map.tmj @@ -455,7 +455,7 @@ "nextobjectid":14, "orientation":"orthogonal", "renderorder":"right-down", - "tiledversion":"1.11.1", + "tiledversion":"1.11.2", "tileheight":16, "tilesets":[ { diff --git a/tools/assetstool/assetcache.py b/tools/assetstool/assetcache.py new file mode 100644 index 0000000..9c1d028 --- /dev/null +++ b/tools/assetstool/assetcache.py @@ -0,0 +1,12 @@ +processedAssets = {} + +def assetGetCache(assetPath): + if assetPath in processedAssets: + return processedAssets[assetPath] + return None + +def assetCache(assetPath, processedData): + if assetPath in processedAssets: + return processedAssets[assetPath] + processedAssets[assetPath] = processedData + return processedData \ No newline at end of file diff --git a/tools/assetstool/processasset.py b/tools/assetstool/processasset.py index 4b5f1a2..6411f3a 100644 --- a/tools/assetstool/processasset.py +++ b/tools/assetstool/processasset.py @@ -4,13 +4,13 @@ from processimage import processImage from processpalette import processPalette from processconfig import processConfig from processtileset import processTileset +from processmap import processMap processedAssets = [] def processAsset(asset): if asset['path'] in processedAssets: return - processedAssets.append(asset['path']) # Handle tiled tilesets @@ -23,6 +23,8 @@ def processAsset(asset): return processConfig(asset) elif t == 'tileset': return processTileset(asset) + elif t == 'map': + return processMap(asset) else: print(f"Error: Unknown asset type '{asset['type']}' for path '{asset['path']}'") sys.exit(1) \ No newline at end of file diff --git a/tools/assetstool/processconfig.py b/tools/assetstool/processconfig.py index 40df4eb..2c17697 100644 --- a/tools/assetstool/processconfig.py +++ b/tools/assetstool/processconfig.py @@ -2,9 +2,14 @@ import os import sys from args import args from assethelpers import getAssetRelativePath +from assetcache import assetGetCache, assetCache def processConfig(asset): assetPath = asset['path'] + cache = assetGetCache(assetPath) + if cache is not None: + return cache + print(f"Processing config: {assetPath}") # Takes each line, seperates it by either semicolon or newline, @@ -41,4 +46,4 @@ def processConfig(asset): outConfig = { "files": [ outputFilePath ], } - return outConfig \ No newline at end of file + return assetCache(assetPath, outConfig) \ No newline at end of file diff --git a/tools/assetstool/processimage.py b/tools/assetstool/processimage.py index 85b0ed4..7ed0ec6 100644 --- a/tools/assetstool/processimage.py +++ b/tools/assetstool/processimage.py @@ -4,24 +4,32 @@ from PIL import Image from processpalette import extractPaletteFromImage, palettes from args import args from assethelpers import getAssetRelativePath +from assetcache import assetGetCache, assetCache images = [] def processImage(asset): + cache = assetGetCache(asset['path']) + if cache is not None: + return cache + type = None if 'type' in asset['options']: type = asset['options'].get('type', 'PALETTIZED').upper() if type == 'PALETTIZED' or type is None: - return processPalettizedImage(asset) + return assetCache(asset['path'], processPalettizedImage(asset)) elif type == 'ALPHA': - return processAlphaImage(asset) + return assetCache(asset['path'], processAlphaImage(asset)) else: print(f"Error: Unknown image type {type} for asset {asset['path']}") sys.exit(1) def processPalettizedImage(asset): assetPath = asset['path'] + cache = assetGetCache(assetPath) + if cache is not None: + return cache image = Image.open(assetPath) imagePalette = extractPaletteFromImage(image) @@ -72,10 +80,14 @@ def processPalettizedImage(asset): 'width': image.width, 'height': image.height, } - return outImage + return assetCache(assetPath, outImage) def processAlphaImage(asset): assetPath = asset['path'] + cache = assetGetCache(assetPath) + if cache is not None: + return cache + print(f"Processing alpha image: {assetPath}") data = bytearray() @@ -101,4 +113,4 @@ def processAlphaImage(asset): 'width': image.width, 'height': image.height, } - return outImage \ No newline at end of file + return assetCache(assetPath, outImage) \ No newline at end of file diff --git a/tools/assetstool/processmap.py b/tools/assetstool/processmap.py new file mode 100644 index 0000000..15764bb --- /dev/null +++ b/tools/assetstool/processmap.py @@ -0,0 +1,164 @@ +import sys +import os +from args import args +from xml.etree import ElementTree as ET +from processtileset import processTileset +from assetcache import assetCache, assetGetCache +from assethelpers import getAssetRelativePath + +def processMap(asset): + cache = assetGetCache(asset['path']) + if cache is not None: + return cache + + # Load the TMX file + tree = ET.parse(asset['path']) + root = tree.getroot() + + # Root needs to be "map" element. + if root.tag != 'map': + print(f"Error: TMX file {asset['path']} does not have a root element") + sys.exit(1) + + # Root needs to be orientation="orthogonal" + if 'orientation' not in root.attrib or root.attrib['orientation'] != 'orthogonal': + print(f"Error: TMX file {asset['path']} does not have orientation='orthogonal'") + sys.exit(1) + + # Extract width, height, tilewidth, tileheight attributes + if 'width' not in root.attrib or 'height' not in root.attrib or 'tilewidth' not in root.attrib or 'tileheight' not in root.attrib: + print(f"Error: TMX file {asset['path']} is missing required attributes (width, height, tilewidth, tileheight)") + sys.exit(1) + + mapWidth = int(root.attrib['width']) + mapHeight = int(root.attrib['height']) + tileWidth = int(root.attrib['tilewidth']) + tileHeight = int(root.attrib['tileheight']) + + # Find all tileset elements + tilesets = [] + for tilesetElement in root.findall('tileset'): + # Tileset must have a source attribute + if 'source' not in tilesetElement.attrib: + print(f"Error: element in {asset['path']} is missing a source attribute") + sys.exit(1) + # Must have a firstgid attribute + if 'firstgid' not in tilesetElement.attrib: + print(f"Error: element in {asset['path']} is missing a firstgid attribute") + sys.exit(1) + + firstGid = int(tilesetElement.attrib['firstgid']) + source = tilesetElement.attrib['source'] + + # Get source path relative to the tmx file's working directory. + # Needs normalizing also since ".." is often used. + source = os.path.normpath(os.path.join(os.path.dirname(asset['path']), source)) + tileset = processTileset({ 'path': source, 'type': 'tileset', 'options': {} }) + + tilesets.append({ + 'firstGid': firstGid, + 'source': source, + 'tileset': tileset + }) + + # Sort tilesets by firstGid, highest first + tilesets.sort(key=lambda x: x['firstGid'], reverse=True) + + # Layer types + # objectLayers = [] # Not implemented + tileLayers = [] + for layerElement in root.findall('layer'): + # Assume tile layer for now + # Must have id, name, width, height attributes + if 'id' not in layerElement.attrib or 'name' not in layerElement.attrib or 'width' not in layerElement.attrib or 'height' not in layerElement.attrib: + print(f"Error: element in {asset['path']} is missing required attributes (id, name, width, height)") + sys.exit(1) + + id = int(layerElement.attrib['id']) + name = layerElement.attrib['name'] + width = int(layerElement.attrib['width']) + height = int(layerElement.attrib['height']) + + # Need exactly one data element + dataElements = layerElement.findall('data') + if len(dataElements) != 1: + print(f"Error: element in {asset['path']} must have exactly one child element") + sys.exit(1) + + # Get text, remove whitespace, split by comman and convert to int + dataElement = dataElements[0] + if dataElement.attrib.get('encoding', '') != 'csv': + print(f"Error: element in {asset['path']} must have encoding='csv'") + sys.exit(1) + + dataText = dataElement.text.strip() + data = [int(gid) for gid in dataText.split(',') if gid.strip().isdigit()] + + # Should be exactly width * height entries + if len(data) != width * height: + print(f"Error: element in {asset['path']} has {len(data)} entries but expected {width * height} (width * height)") + sys.exit(1) + + tileLayers.append({ + 'id': id, + 'name': name, + 'width': width, + 'height': height, + 'data': data, + }) + + # Now we have our layers all parsed out. + data = bytearray() + data += b'drm' # Dusk RPG Map + data += mapWidth.to_bytes(4, 'little') # Map width in tiles + data += mapHeight.to_bytes(4, 'little') # Map height in tiles + data += len(tilesets).to_bytes(4, 'little') # Number of tilesets + data += len(tileLayers).to_bytes(4, 'little') # Number of layers + + # For each tileset + for tileset in tilesets: + data += tileset['firstGid'].to_bytes(4, 'little') # First GID + + # For each layer... + for layer in tileLayers: + for gid in layer['data']: + # Get tileset for this gid, since the tilesets are already sorted we can + # simply find the first one that has firstGid <= gid + if gid == 0: + # Empty tile + localIndex = 0xFF + tilesetIndex = 0xFFFFFFFF + else: + for tileset in tilesets: + if gid >= tileset['firstGid']: + tilesetIndex = tilesets.index(tileset) + localIndex = gid - tileset['firstGid'] + break + else: + # If no tileset was found, this is an invalid gid + print(f"Error: Invalid tile GID {gid} in {asset['path']}") + sys.exit(1) + + if localIndex > 255: + print(f"Error: Local tile index {localIndex} exceeds 255 in {asset['path']}") + sys.exit(1) + + data += tilesetIndex.to_bytes(4, 'little') # Tileset index + data += localIndex.to_bytes(1, 'little') # Local tile index + + relative = getAssetRelativePath(asset['path']) + fileNameWithoutExt = os.path.splitext(os.path.basename(asset['path']))[0] + outputFileRelative = os.path.join(os.path.dirname(relative), f"{fileNameWithoutExt}.drm") + outputFilePath = os.path.join(args.output_assets, outputFileRelative) + os.makedirs(os.path.dirname(outputFilePath), exist_ok=True) + with open(outputFilePath, "wb") as f: + f.write(data) + + outMap = { + 'mapPath': outputFileRelative, + 'files': [ outputFilePath ], + 'width': mapWidth, + 'height': mapHeight, + } + + return assetCache(asset['path'], outMap) \ No newline at end of file diff --git a/tools/assetstool/processpalette.py b/tools/assetstool/processpalette.py index d45a457..ae8d22c 100644 --- a/tools/assetstool/processpalette.py +++ b/tools/assetstool/processpalette.py @@ -3,6 +3,7 @@ from PIL import Image from args import args import sys import datetime +from assetcache import assetCache, assetGetCache palettes = [] @@ -22,6 +23,9 @@ def extractPaletteFromImage(image): def processPalette(asset): print(f"Processing palette: {asset['path']}") + cache = assetGetCache(asset['path']) + if cache is not None: + return cache paletteIndex = len(palettes) image = Image.open(asset['path']) @@ -72,7 +76,7 @@ def processPalette(asset): } palettes.append(palette) - return palette + return assetCache(asset['path'], palette) def processPaletteList(): data = f"// Auto-generated palette list\n" diff --git a/tools/assetstool/processtileset.py b/tools/assetstool/processtileset.py index a10d119..970ab65 100644 --- a/tools/assetstool/processtileset.py +++ b/tools/assetstool/processtileset.py @@ -6,6 +6,7 @@ import os import datetime from args import args from xml.etree import ElementTree +from assetcache import assetGetCache, assetCache def loadTilesetFromTSX(asset): # Load the TSX file @@ -99,8 +100,11 @@ def loadTilesetFromArgs(asset): } def processTileset(asset): - print(f"Processing tileset: {asset['path']}") + cache = assetGetCache(asset['path']) + if cache is not None: + return cache + print(f"Processing tileset: {asset['path']}") tilesetData = None if asset['path'].endswith('.tsx'): tilesetData = loadTilesetFromTSX(asset) @@ -135,9 +139,9 @@ def processTileset(asset): with open(outputFile, 'w') as f: f.write(data) - outTileset = { + return assetCache(asset['path'], { + "files": [], "image": tilesetData['image'], "headerFile": outputFile, "files": tilesetData['image']['files'], - } - return outTileset \ No newline at end of file + }) \ No newline at end of file