diff options
Diffstat (limited to 'gfx-iso/asset/mkasset.py')
-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()) | ||