From 21a0d0c1c424f7db90c3282aad4bf6ad4ef809b7 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 8 Jul 2023 14:37:29 -0700 Subject: Load tile maps and tile sets from files. --- gfx-iso/asset/mkasset.py | 155 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 gfx-iso/asset/mkasset.py (limited to 'gfx-iso/asset/mkasset.py') diff --git a/gfx-iso/asset/mkasset.py b/gfx-iso/asset/mkasset.py new file mode 100644 index 0000000..15f7912 --- /dev/null +++ b/gfx-iso/asset/mkasset.py @@ -0,0 +1,155 @@ +# Converts tile sets and tile maps to binary formats (.TS, .TM) for the engine. +# +# Currently handles Tiled's .tsx and .tmx file formats. +# +# The output is a binary tile set file (.TS) or a binary tile map file (.TM). +import argparse +import ctypes +from PIL import Image +import sys +from xml.etree import ElementTree + +# Maximum length of path strings in .TS and .TM files. +MAX_PATH_LENGTH = 128 + + +def drop_extension(filepath): + return filepath[:filepath.rfind('.')] + + +def to_char_array(string, length): + """Convert a string to a fixed-length ASCII char array. + + The length of str must be at most length-1 so that the resulting string can + be null-terminated. + """ + assert (len(string) < length) + chars = string.encode("ascii") + nulls = ("\0" * (length - len(string))).encode("ascii") + return chars + nulls + + +def convert_tsx(input_filepath, output_filepath): + """Converts a Tiled .tsx tileset file to a .TS tile set file.""" + xml = ElementTree.parse(input_filepath) + root = xml.getroot() + + tile_count = int(root.attrib["tilecount"]) + max_tile_width = int(root.attrib["tilewidth"]) + max_tile_height = int(root.attrib["tileheight"]) + + print(f"Tile count: {tile_count}") + print(f"Max width: {max_tile_width}") + print(f"Max height: {max_tile_height}") + + with open(output_filepath, 'bw') as output: + output.write(ctypes.c_uint16(tile_count)) + output.write(ctypes.c_uint16(max_tile_width)) + output.write(ctypes.c_uint16(max_tile_height)) + + num_tile = 0 + for tile in root: + # Skip the "grid" and other non-tile elements. + if not tile.tag == "tile": + continue + + # Assuming tiles are numbered 0..N. + tile_id = int(tile.attrib["id"]) + assert (tile_id == num_tile) + num_tile += 1 + + image = tile[0] + tile_width = int(image.attrib["width"]) + tile_height = int(image.attrib["height"]) + tile_path = image.attrib["source"] + + output.write(ctypes.c_uint16(tile_width)) + output.write(ctypes.c_uint16(tile_height)) + + with Image.open(tile_path) as im: + bytes = im.convert('RGBA').tobytes() + output.write(bytes) + + +def convert_tmx(input_filepath, output_filepath): + """Converts a Tiled .tmx file to a .TM tile map file.""" + xml = ElementTree.parse(input_filepath) + root = xml.getroot() + + map_width = int(root.attrib["width"]) + map_height = int(root.attrib["height"]) + base_tile_width = int(root.attrib["tilewidth"]) + base_tile_height = int(root.attrib["tileheight"]) + num_layers = 1 + + print(f"Map width: {map_width}") + print(f"Map height: {map_height}") + print(f"Tile width: {base_tile_width}") + print(f"Tile height: {base_tile_height}") + + with open(output_filepath, 'bw') as output: + output.write(ctypes.c_uint16(map_width)) + output.write(ctypes.c_uint16(map_height)) + output.write(ctypes.c_uint16(base_tile_width)) + output.write(ctypes.c_uint16(base_tile_height)) + output.write(ctypes.c_uint16(num_layers)) + + tileset_path = None + + for child in root: + if child.tag == "tileset": + tileset = child + tileset_path = tileset.attrib["source"] + + print(f"Tile set: {tileset_path}") + + tileset_path = tileset_path.replace("tsx", "ts") + elif child.tag == "layer": + layer = child + layer_id = int(layer.attrib["id"]) + layer_width = int(layer.attrib["width"]) + layer_height = int(layer.attrib["height"]) + + print(f"Layer: {layer_id}") + print(f"Width: {layer_width}") + print(f"Height: {layer_height}") + + assert (tileset_path) + output.write(to_char_array(tileset_path, MAX_PATH_LENGTH)) + + # Assume the layer's dimensions matches the map's. + assert (layer_width == map_width) + assert (layer_height == map_height) + + data = layer[0] + # Handle other encodings later. + assert (data.attrib["encoding"] == "csv") + + csv = data.text.strip() + rows = csv.split('\n') + for row in rows: + tile_ids = [x.strip() for x in row.split(',') if x] + for tile_id in tile_ids: + output.write(ctypes.c_uint16(int(tile_id))) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("input", help="Input file (.tsx, .tmx)") + args = parser.parse_args() + + output_filepath_no_ext = drop_extension(args.input) + if ".tsx" in args.input: + output_filepath = output_filepath_no_ext + ".ts" + convert_tsx(args.input, output_filepath) + elif ".tmx" in args.input: + output_filepath = output_filepath_no_ext + ".tm" + convert_tmx(args.input, output_filepath) + else: + print(f"Unhandled file format: {args.input}") + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) -- cgit v1.2.3