diff options
Diffstat (limited to 'gfx-iso/asset')
| -rw-r--r-- | gfx-iso/asset/mkasset.py | 155 |
1 files changed, 155 insertions, 0 deletions
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 @@ | |||
| 1 | # Converts tile sets and tile maps to binary formats (.TS, .TM) for the engine. | ||
| 2 | # | ||
| 3 | # Currently handles Tiled's .tsx and .tmx file formats. | ||
| 4 | # | ||
| 5 | # The output is a binary tile set file (.TS) or a binary tile map file (.TM). | ||
| 6 | import argparse | ||
| 7 | import ctypes | ||
| 8 | from PIL import Image | ||
| 9 | import sys | ||
| 10 | from xml.etree import ElementTree | ||
| 11 | |||
| 12 | # Maximum length of path strings in .TS and .TM files. | ||
| 13 | MAX_PATH_LENGTH = 128 | ||
| 14 | |||
| 15 | |||
| 16 | def drop_extension(filepath): | ||
| 17 | return filepath[:filepath.rfind('.')] | ||
| 18 | |||
| 19 | |||
| 20 | def to_char_array(string, length): | ||
| 21 | """Convert a string to a fixed-length ASCII char array. | ||
| 22 | |||
| 23 | The length of str must be at most length-1 so that the resulting string can | ||
| 24 | be null-terminated. | ||
| 25 | """ | ||
| 26 | assert (len(string) < length) | ||
| 27 | chars = string.encode("ascii") | ||
| 28 | nulls = ("\0" * (length - len(string))).encode("ascii") | ||
| 29 | return chars + nulls | ||
| 30 | |||
| 31 | |||
| 32 | def convert_tsx(input_filepath, output_filepath): | ||
| 33 | """Converts a Tiled .tsx tileset file to a .TS tile set file.""" | ||
| 34 | xml = ElementTree.parse(input_filepath) | ||
| 35 | root = xml.getroot() | ||
| 36 | |||
| 37 | tile_count = int(root.attrib["tilecount"]) | ||
| 38 | max_tile_width = int(root.attrib["tilewidth"]) | ||
| 39 | max_tile_height = int(root.attrib["tileheight"]) | ||
| 40 | |||
| 41 | print(f"Tile count: {tile_count}") | ||
| 42 | print(f"Max width: {max_tile_width}") | ||
| 43 | print(f"Max height: {max_tile_height}") | ||
| 44 | |||
| 45 | with open(output_filepath, 'bw') as output: | ||
| 46 | output.write(ctypes.c_uint16(tile_count)) | ||
| 47 | output.write(ctypes.c_uint16(max_tile_width)) | ||
| 48 | output.write(ctypes.c_uint16(max_tile_height)) | ||
| 49 | |||
| 50 | num_tile = 0 | ||
| 51 | for tile in root: | ||
| 52 | # Skip the "grid" and other non-tile elements. | ||
| 53 | if not tile.tag == "tile": | ||
| 54 | continue | ||
| 55 | |||
| 56 | # Assuming tiles are numbered 0..N. | ||
| 57 | tile_id = int(tile.attrib["id"]) | ||
| 58 | assert (tile_id == num_tile) | ||
| 59 | num_tile += 1 | ||
| 60 | |||
| 61 | image = tile[0] | ||
| 62 | tile_width = int(image.attrib["width"]) | ||
| 63 | tile_height = int(image.attrib["height"]) | ||
| 64 | tile_path = image.attrib["source"] | ||
| 65 | |||
| 66 | output.write(ctypes.c_uint16(tile_width)) | ||
| 67 | output.write(ctypes.c_uint16(tile_height)) | ||
| 68 | |||
| 69 | with Image.open(tile_path) as im: | ||
| 70 | bytes = im.convert('RGBA').tobytes() | ||
| 71 | output.write(bytes) | ||
| 72 | |||
| 73 | |||
| 74 | def convert_tmx(input_filepath, output_filepath): | ||
| 75 | """Converts a Tiled .tmx file to a .TM tile map file.""" | ||
| 76 | xml = ElementTree.parse(input_filepath) | ||
| 77 | root = xml.getroot() | ||
| 78 | |||
| 79 | map_width = int(root.attrib["width"]) | ||
| 80 | map_height = int(root.attrib["height"]) | ||
| 81 | base_tile_width = int(root.attrib["tilewidth"]) | ||
| 82 | base_tile_height = int(root.attrib["tileheight"]) | ||
| 83 | num_layers = 1 | ||
| 84 | |||
| 85 | print(f"Map width: {map_width}") | ||
| 86 | print(f"Map height: {map_height}") | ||
| 87 | print(f"Tile width: {base_tile_width}") | ||
| 88 | print(f"Tile height: {base_tile_height}") | ||
| 89 | |||
| 90 | with open(output_filepath, 'bw') as output: | ||
| 91 | output.write(ctypes.c_uint16(map_width)) | ||
| 92 | output.write(ctypes.c_uint16(map_height)) | ||
| 93 | output.write(ctypes.c_uint16(base_tile_width)) | ||
| 94 | output.write(ctypes.c_uint16(base_tile_height)) | ||
| 95 | output.write(ctypes.c_uint16(num_layers)) | ||
| 96 | |||
| 97 | tileset_path = None | ||
| 98 | |||
| 99 | for child in root: | ||
| 100 | if child.tag == "tileset": | ||
| 101 | tileset = child | ||
| 102 | tileset_path = tileset.attrib["source"] | ||
| 103 | |||
| 104 | print(f"Tile set: {tileset_path}") | ||
| 105 | |||
| 106 | tileset_path = tileset_path.replace("tsx", "ts") | ||
| 107 | elif child.tag == "layer": | ||
| 108 | layer = child | ||
| 109 | layer_id = int(layer.attrib["id"]) | ||
| 110 | layer_width = int(layer.attrib["width"]) | ||
| 111 | layer_height = int(layer.attrib["height"]) | ||
| 112 | |||
| 113 | print(f"Layer: {layer_id}") | ||
| 114 | print(f"Width: {layer_width}") | ||
| 115 | print(f"Height: {layer_height}") | ||
| 116 | |||
| 117 | assert (tileset_path) | ||
| 118 | output.write(to_char_array(tileset_path, MAX_PATH_LENGTH)) | ||
| 119 | |||
| 120 | # Assume the layer's dimensions matches the map's. | ||
| 121 | assert (layer_width == map_width) | ||
| 122 | assert (layer_height == map_height) | ||
| 123 | |||
| 124 | data = layer[0] | ||
| 125 | # Handle other encodings later. | ||
| 126 | assert (data.attrib["encoding"] == "csv") | ||
| 127 | |||
| 128 | csv = data.text.strip() | ||
| 129 | rows = csv.split('\n') | ||
| 130 | for row in rows: | ||
| 131 | tile_ids = [x.strip() for x in row.split(',') if x] | ||
| 132 | for tile_id in tile_ids: | ||
| 133 | output.write(ctypes.c_uint16(int(tile_id))) | ||
| 134 | |||
| 135 | |||
| 136 | def main(): | ||
| 137 | parser = argparse.ArgumentParser() | ||
| 138 | parser.add_argument("input", help="Input file (.tsx, .tmx)") | ||
| 139 | args = parser.parse_args() | ||
| 140 | |||
| 141 | output_filepath_no_ext = drop_extension(args.input) | ||
| 142 | if ".tsx" in args.input: | ||
| 143 | output_filepath = output_filepath_no_ext + ".ts" | ||
| 144 | convert_tsx(args.input, output_filepath) | ||
| 145 | elif ".tmx" in args.input: | ||
| 146 | output_filepath = output_filepath_no_ext + ".tm" | ||
| 147 | convert_tmx(args.input, output_filepath) | ||
| 148 | else: | ||
| 149 | print(f"Unhandled file format: {args.input}") | ||
| 150 | |||
| 151 | return 0 | ||
| 152 | |||
| 153 | |||
| 154 | if __name__ == '__main__': | ||
| 155 | sys.exit(main()) | ||
