Files
dusk/tools/assetstool/processmap.py
2025-09-18 17:01:10 -05:00

164 lines
5.9 KiB
Python

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 <map> 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: <tileset> 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: <tileset> 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: <layer> 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: <layer> element in {asset['path']} must have exactly one <data> 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: <data> 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: <data> 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)