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)