Files
dusk/tools/assetstool/processtileset.py
2025-09-19 12:43:57 -05:00

175 lines
6.2 KiB
Python

import json
from processimage import processImage
import sys
from assethelpers import getAssetRelativePath
import os
import datetime
from args import args
from xml.etree import ElementTree
from assetcache import assetGetCache, assetCache
tilesets = []
def loadTilesetFromTSX(asset):
# Load the TSX file
tree = ElementTree.parse(asset['path'])
root = tree.getroot()
# Expect tileheight, tilewidth, columns and tilecount attributes
if 'tilewidth' not in root.attrib or 'tileheight' not in root.attrib or 'columns' not in root.attrib or 'tilecount' not in root.attrib:
print(f"Error: TSX file {asset['path']} is missing required attributes (tilewidth, tileheight, columns, tilecount)")
sys.exit(1)
tileWidth = int(root.attrib['tilewidth'])
tileHeight = int(root.attrib['tileheight'])
columns = int(root.attrib['columns'])
tileCount = int(root.attrib['tilecount'])
rows = (tileCount + columns - 1) // columns # Calculate rows based on tileCount and columns
# Find the image element
imageElement = root.find('image')
if imageElement is None or 'source' not in imageElement.attrib:
print(f"Error: TSX file {asset['path']} is missing an image element with a source attribute")
sys.exit(1)
imagePath = imageElement.attrib['source']
# Image is relative to the TSX file
imageAssetPath = os.path.join(os.path.dirname(asset['path']), imagePath)
image = processImage({
'path': imageAssetPath,
'options': asset['options'],
})
return {
"image": image,
"tileWidth": tileWidth,
"tileHeight": tileHeight,
"columns": columns,
"rows": rows,
"originalWidth": tileWidth * columns,
"originalHeight": tileHeight * rows,
}
def loadTilesetFromArgs(asset):
# We need to determine how big each tile is. This can either be provided as
# an arg of tileWidth/tileHeight or as a count of rows/columns.
# Additionally, if the image has been factored, then the user can provide both
# tile sizes AND cols/rows to indicate the original size of the image.
image = processImage(asset)
tileWidth, tileHeight = None, None
columns, rows = None, None
originalWidth, originalHeight = image['width'], image['height']
if 'tileWidth' in asset['options'] and 'columns' in asset['options']:
tileWidth = int(asset['options']['tileWidth'])
columns = int(asset['options']['columns'])
originalWidth = tileWidth * columns
elif 'tileWidth' in asset['options']:
tileWidth = int(asset['options']['tileWidth'])
columns = image['width'] // tileWidth
elif 'columns' in asset['options']:
columns = int(asset['options']['columns'])
tileWidth = image['width'] // columns
else:
print(f"Error: Tileset {asset['path']} must specify either tileWidth or columns")
sys.exit(1)
if 'tileHeight' in asset['options'] and 'rows' in asset['options']:
tileHeight = int(asset['options']['tileHeight'])
rows = int(asset['options']['rows'])
originalHeight = tileHeight * rows
elif 'tileHeight' in asset['options']:
tileHeight = int(asset['options']['tileHeight'])
rows = image['height'] // tileHeight
elif 'rows' in asset['options']:
rows = int(asset['options']['rows'])
tileHeight = image['height'] // rows
else:
print(f"Error: Tileset {asset['path']} must specify either tileHeight or rows")
sys.exit(1)
return {
"image": image,
"tileWidth": tileWidth,
"tileHeight": tileHeight,
"columns": columns,
"rows": rows,
"originalWidth": originalWidth,
"originalHeight": originalHeight,
}
def processTileset(asset):
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)
else:
tilesetData = loadTilesetFromArgs(asset)
fileNameWithoutExtension = os.path.splitext(os.path.basename(asset['path']))[0]
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
tilesetName = fileNameWithoutExtension
tilesetNameUpper = tilesetName.upper()
widthScale = tilesetData['originalWidth'] / tilesetData['image']['width']
heightScale = tilesetData['originalHeight'] / tilesetData['image']['height']
# Create header
data = f"// Tileset Generated for {asset['path']} at {now}\n"
data += f"#pragma once\n"
data += f"#include \"display/tileset.h\"\n\n"
data += f"static const tileset_t TILESET_{tilesetNameUpper} = {{\n"
data += f" .tileWidth = {tilesetData['tileWidth']},\n"
data += f" .tileHeight = {tilesetData['tileHeight']},\n"
data += f" .tileCount = {tilesetData['columns'] * tilesetData['rows']},\n"
data += f" .columns = {tilesetData['columns']},\n"
data += f" .rows = {tilesetData['rows']},\n"
data += f" .uv = {{ {widthScale / tilesetData['columns']}f, {heightScale / tilesetData['rows']}f }},\n"
data += f" .image = {json.dumps(tilesetData['image']['imagePath'])},\n"
data += f"}};\n"
# Write Header
outputFile = os.path.join(args.headers_dir, "display", "tileset", f"tileset_{tilesetName}.h")
os.makedirs(os.path.dirname(outputFile), exist_ok=True)
with open(outputFile, 'w') as f:
f.write(data)
tileset = {
"files": [],
"image": tilesetData['image'],
"headerFile": os.path.relpath(outputFile, args.headers_dir),
"tilesetName": tilesetName,
"tilesetNameUpper": tilesetNameUpper,
"tilesetIndex": len(tilesets),
"tilesetData": tilesetData,
"files": tilesetData['image']['files'],
}
tilesets.append(tileset)
return assetCache(asset['path'], tileset)
def processTilesetList():
data = f"// Tileset List Generated at {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
data += f"#pragma once\n"
for tileset in tilesets:
data += f"#include \"{tileset['headerFile']}\"\n"
data += f"\n"
data += f"#define TILESET_LIST_COUNT {len(tilesets)}\n\n"
data += f"static const tileset_t* TILESET_LIST[TILESET_LIST_COUNT] = {{\n"
for tileset in tilesets:
data += f" &TILESET_{tileset['tilesetNameUpper']},\n"
data += f"}};\n"
# Write header.
outputFile = os.path.join(args.headers_dir, "display", "tileset", f"tilesetlist.h")
os.makedirs(os.path.dirname(outputFile), exist_ok=True)
with open(outputFile, 'w') as f:
f.write(data)