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')

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