1#!/usr/bin/env python3 2 3# decompose.py 4# Split a gfx tile_config.json into 1000s of little directories, 5# each with their own config file and tile image. 6 7"""Split a tileset's tile_config.json into one directory per tile 8containing the tile data and png. 9""" 10 11import argparse 12import json 13import math 14import os 15import subprocess 16import sys 17 18try: 19 import pyvips 20 Vips = pyvips 21except ImportError: 22 import gi 23 gi.require_version('Vips', '8.0') 24 from gi.repository import Vips 25 26 27def write_to_json(pathname, data, prettify=False): 28 with open(pathname, "w") as fp: 29 json.dump(data, fp) 30 31 json_formatter = "./tools/format/json_formatter.cgi" 32 if prettify and os.path.isfile(json_formatter): 33 cmd = [json_formatter, pathname] 34 subprocess.call(cmd) 35 36 37def find_or_make_dir(pathname): 38 try: 39 os.stat(pathname) 40 except OSError: 41 os.mkdir(pathname) 42 43 44class TileSheetData(object): 45 def __init__(self, tilesheet_data, refs): 46 self.ts_filename = tilesheet_data.get("file", "") 47 #print("working on {}".format(self.ts_filename)) 48 self.tile_entries = {} 49 self.sprite_height = tilesheet_data.get( 50 "sprite_height", refs.default_height) 51 self.sprite_width = tilesheet_data.get( 52 "sprite_width", refs.default_width) 53 self.sprite_offset_x = tilesheet_data.get("sprite_offset_x", 0) 54 self.sprite_offset_y = tilesheet_data.get("sprite_offset_y", 0) 55 self.write_dim = self.sprite_width != refs.default_width 56 self.write_dim |= self.sprite_height != refs.default_height 57 self.write_dim |= self.sprite_offset_x or self.sprite_offset_y 58 self.ts_pathname = refs.tileset_pathname + "/" + self.ts_filename 59 self.ts_image = Vips.Image.pngload(self.ts_pathname) 60 self.ts_width = self.ts_image.width 61 self.ts_tiles_per_row = int( 62 math.floor(self.ts_width / self.sprite_width)) 63 self.ts_height = self.ts_image.height 64 self.ts_rows = int(math.floor(self.ts_height / self.sprite_height)) 65 self.pngnum_min = refs.last_pngnum 66 self.pngnum_max = \ 67 refs.last_pngnum + self.ts_tiles_per_row * self.ts_rows - 1 68 #print("\t{}x{}; {} rows; spans {} to {}".format( 69 # self.ts_width, self.ts_height, self.ts_rows, 70 # self.pngnum_min, self.pngnum_max)) 71 self.expansions = [] 72 self.fallback = tilesheet_data.get("ascii") 73 refs.last_pngnum = self.pngnum_max + 1 74 refs.tspathname_to_tsfilename.setdefault( 75 self.ts_pathname, self.ts_filename) 76 77 def check_for_expansion(self, tile_entry): 78 if tile_entry.get("fg", -10) == 0: 79 self.expansions.append(tile_entry) 80 return True 81 return False 82 83 def check_id_valid(self, tile_id): 84 if not tile_id: 85 return True 86 # wielded terrain isn't valid 87 if tile_id.startswith("overlay_wielded_t_"): 88 return False 89 if tile_id.startswith("overlay_wielded_mon_"): 90 return False 91 if tile_id.startswith("overlay_wielded_fd_"): 92 return False 93 if tile_id.startswith("overlay_wielded_f_"): 94 return False 95 # wielding or wearing overlays makes no sense 96 if tile_id.startswith("overlay_wielded_overlay"): 97 return False 98 if tile_id.startswith("overlay_worn_overlay"): 99 return False 100 return True 101 102 def parse_id(self, tile_entry): 103 all_tile_ids = [] 104 read_tile_ids = tile_entry.get("id") 105 valid = True 106 if isinstance(read_tile_ids, list): 107 for tile_id in read_tile_ids: 108 valid &= self.check_id_valid(tile_id) 109 if tile_id and valid and tile_id not in all_tile_ids: 110 all_tile_ids.append(tile_id) 111 else: 112 valid &= self.check_id_valid(read_tile_ids) 113 if read_tile_ids and valid and read_tile_ids not in all_tile_ids: 114 all_tile_ids.append(read_tile_ids) 115 if not valid: 116 return None, None 117 if not all_tile_ids: 118 return "background", ["background"] 119 #print("tile {}".format(all_tile_ids[0])) 120 return all_tile_ids[0], all_tile_ids 121 122 def parse_index(self, read_pngnums, all_pngnums, refs): 123 local_pngnums = [] 124 if isinstance(read_pngnums, list): 125 for pngnum in read_pngnums: 126 if isinstance(pngnum, dict): 127 sprite_ids = pngnum.get("sprite", -10) 128 if isinstance(sprite_ids, list): 129 for sprite_id in sprite_ids: 130 if (sprite_id < 0 or 131 sprite_id in refs.delete_pngnums): 132 continue 133 if sprite_id not in all_pngnums: 134 all_pngnums.append(sprite_id) 135 if sprite_id not in local_pngnums: 136 local_pngnums.append(sprite_id) 137 else: 138 if sprite_ids < 0 or sprite_ids in refs.delete_pngnums: 139 continue 140 if sprite_ids not in all_pngnums: 141 all_pngnums.append(sprite_ids) 142 if sprite_ids not in local_pngnums: 143 local_pngnums.append(sprite_ids) 144 else: 145 if pngnum < 0 or pngnum in refs.delete_pngnums: 146 continue 147 if pngnum not in all_pngnums: 148 all_pngnums.append(pngnum) 149 if pngnum not in local_pngnums: 150 local_pngnums.append(pngnum) 151 elif read_pngnums >= 0 and read_pngnums not in refs.delete_pngnums: 152 if read_pngnums not in all_pngnums: 153 all_pngnums.append(read_pngnums) 154 if read_pngnums not in local_pngnums: 155 local_pngnums.append(read_pngnums) 156 return all_pngnums 157 158 def parse_png(self, tile_entry, refs): 159 all_pngnums = [] 160 fg_id = tile_entry.get("fg", -10) 161 all_pngnums = self.parse_index(fg_id, all_pngnums, refs) 162 163 bg_id = tile_entry.get("bg", -10) 164 all_pngnums = self.parse_index(bg_id, all_pngnums, refs) 165 166 add_tile_entrys = tile_entry.get("additional_tiles", []) 167 for add_tile_entry in add_tile_entrys: 168 add_pngnums = self.parse_png(add_tile_entry, refs) 169 for add_pngnum in add_pngnums: 170 if add_pngnum not in all_pngnums: 171 all_pngnums.append(add_pngnum) 172 # print("\tpngs: {}".format(all_pngnums)) 173 return all_pngnums 174 175 def parse_tile_entry(self, tile_entry, refs): 176 if self.check_for_expansion(tile_entry): 177 return None 178 tile_id, all_tile_ids = self.parse_id(tile_entry) 179 if not tile_id: 180 # print("no id for {}".format(tile_entry)) 181 return None 182 tile_id = tile_id.replace("/", "_") 183 all_pngnums = self.parse_png(tile_entry, refs) 184 offset = 0 185 for i in range(0, len(all_pngnums)): 186 pngnum = all_pngnums[i] 187 if pngnum in refs.pngnum_to_pngname: 188 continue 189 pngname = "{}_{}_{}".format(pngnum, tile_id, i + offset) 190 while pngname in refs.pngname_to_pngnum: 191 offset += 1 192 pngname = "{}_{}_{}".format(pngnum, tile_id, i + offset) 193 try: 194 refs.pngnum_to_pngname.setdefault(pngnum, pngname) 195 refs.pngname_to_pngnum.setdefault(pngname, pngnum) 196 refs.add_pngnum_to_tsfilepath(pngnum) 197 except TypeError: 198 print("failed to parse {}".format( 199 json.dumps(tile_entry, indent=2))) 200 raise 201 return tile_id 202 203 def summarize(self, tile_info, refs): 204 #debug statement to verify pngnum_min and pngnum_max 205 #print("{} from {} to {}".format( 206 # self.ts_filename, self.pngnum_min, self.pngnum_max)) 207 if self.fallback: 208 refs.ts_data[self.ts_filename] = self 209 ts_tile_info = { 210 "fallback": True 211 } 212 tile_info.append({self.ts_filename: ts_tile_info}) 213 return 214 if self.pngnum_max > 0: 215 refs.ts_data[self.ts_filename] = self 216 ts_tile_info = { 217 "//": "indices {} to {}".format( 218 self.pngnum_min, self.pngnum_max) 219 } 220 if self.write_dim: 221 ts_tile_info["sprite_offset_x"] = self.sprite_offset_x 222 ts_tile_info["sprite_offset_y"] = self.sprite_offset_y 223 ts_tile_info["sprite_width"] = self.sprite_width 224 ts_tile_info["sprite_height"] = self.sprite_height 225 #print("{}: {}".format( 226 # self.ts_filename, json.dumps(ts_tile_info, indent=2))) 227 tile_info.append({self.ts_filename: ts_tile_info}) 228 229 230class ExtractionData(object): 231 def __init__(self, ts_filename, refs): 232 self.ts_data = refs.ts_data.get(ts_filename) 233 self.valid = False 234 if not self.ts_data.sprite_width or not self.ts_data.sprite_height: 235 return 236 237 self.valid = True 238 239 ts_base = ts_filename.split(".png")[0] 240 geometry_dim = "{}x{}".format( 241 self.ts_data.sprite_width, self.ts_data.sprite_height) 242 pngs_dir = "/pngs_" + ts_base + "_{}".format(geometry_dim) 243 self.ts_dir_pathname = refs.tileset_pathname + pngs_dir 244 find_or_make_dir(self.ts_dir_pathname) 245 self.tilenum_in_dir = 256 246 self.dir_count = 0 247 self.subdir_pathname = "" 248 249 def write_expansions(self): 250 for expand_entry in self.ts_data.expansions: 251 expansion_id = expand_entry.get("id", "expansion") 252 if not isinstance(expansion_id, str): 253 continue 254 expand_entry_pathname = "{}/{}.json".format( 255 self.ts_dir_pathname, expansion_id) 256 write_to_json(expand_entry_pathname, expand_entry) 257 258 def increment_dir(self): 259 if self.tilenum_in_dir > 255: 260 self.subdir_pathname = "{}/images{}".format( 261 self.ts_dir_pathname, self.dir_count) 262 find_or_make_dir(self.subdir_pathname) 263 self.tilenum_in_dir = 0 264 self.dir_count += 1 265 else: 266 self.tilenum_in_dir += 1 267 return self.subdir_pathname 268 269 def extract_image(self, png_index, refs): 270 if not png_index or refs.extracted_pngnums.get(png_index): 271 return 272 if png_index not in refs.pngnum_to_pngname: 273 return 274 pngname = refs.pngnum_to_pngname[png_index] 275 ts_pathname = refs.pngnum_to_tspathname[png_index] 276 ts_filename = refs.tspathname_to_tsfilename[ts_pathname] 277 self.increment_dir() 278 tile_data = refs.ts_data[ts_filename] 279 file_index = png_index - tile_data.pngnum_min 280 y_index = math.floor(file_index / tile_data.ts_tiles_per_row) 281 x_index = file_index - y_index * tile_data.ts_tiles_per_row 282 tile_off_x = max(0, tile_data.sprite_width * x_index) 283 tile_off_y = max(0, tile_data.sprite_height * y_index) 284 tile_image = tile_data.ts_image.extract_area(tile_off_x, tile_off_y, 285 tile_data.sprite_width, 286 tile_data.sprite_height) 287 tile_png_pathname = self.subdir_pathname + "/" + pngname + ".png" 288 tile_image.pngsave(tile_png_pathname) 289 refs.extracted_pngnums[png_index] = True 290 291 def write_images(self, refs): 292 for pngnum in range( 293 self.ts_data.pngnum_min, self.ts_data.pngnum_max + 1): 294 out_data.extract_image(pngnum, refs) 295 296 297class PngRefs(object): 298 def __init__(self): 299 # dict of png absolute numbers to png names 300 self.pngnum_to_pngname = {} 301 # dict of pngnames to png numbers; used to control uniqueness 302 self.pngname_to_pngnum = {} 303 # dict of png absolute numbers to tilesheet paths, 304 # used to know where to extract images 305 self.pngnum_to_tspathname = {} 306 self.tspathname_to_tsfilename = {} 307 # list of pngs written out 308 self.extracted_pngnums = {} 309 # list of pngs to not use 310 self.delete_pngnums = [] 311 # misc data 312 self.tileset_pathname = "" 313 self.default_width = 16 314 self.default_height = 16 315 self.last_pngnum = 0 316 self.ts_data = {} 317 318 def get_all_data(self, tileset_dirname, delete_pathname): 319 self.tileset_pathname = tileset_dirname 320 321 try: 322 os.stat(self.tileset_pathname) 323 except KeyError: 324 print("cannot find a directory {}".format(self.tileset_pathname)) 325 sys.exit(-1) 326 327 tileset_confname = refs.tileset_pathname + "/" + "tile_config.json" 328 329 try: 330 os.stat(tileset_confname) 331 except KeyError: 332 print("cannot find a directory {}".format(tileset_confname)) 333 sys.exit(-1) 334 335 if delete_pathname: 336 with open(delete_pathname) as del_file: 337 del_ranges = json.load(del_file) 338 for delete_range in del_ranges: 339 if not isinstance(delete_range, list): 340 continue 341 min_png = delete_range[0] 342 max_png = min_png 343 if len(delete_range) > 1: 344 max_png = delete_range[1] 345 for i in range(min_png, max_png + 1): 346 self.delete_pngnums.append(i) 347 348 with open(tileset_confname) as conf_file: 349 return(json.load(conf_file)) 350 351 def add_pngnum_to_tsfilepath(self, pngnum): 352 if not isinstance(pngnum, int): 353 return 354 if pngnum in self.pngnum_to_tspathname: 355 return 356 for ts_filename, ts_data in self.ts_data.items(): 357 if pngnum >= ts_data.pngnum_min and pngnum <= ts_data.pngnum_max: 358 self.pngnum_to_tspathname.setdefault( 359 pngnum, ts_data.ts_pathname) 360 return 361 raise KeyError("index %s out of range", pngnum) 362 363 def convert_index(self, read_pngnums, new_index): 364 if isinstance(read_pngnums, list): 365 for pngnum in read_pngnums: 366 if isinstance(pngnum, dict): 367 sprite_ids = pngnum.get("sprite", -10) 368 if isinstance(sprite_ids, list): 369 new_sprites = [] 370 for sprite_id in sprite_ids: 371 if (sprite_id >= 0 and 372 sprite_id not in self.delete_pngnums): 373 new_sprites.append( 374 self.pngnum_to_pngname[sprite_id]) 375 pngnum["sprite"] = new_sprites 376 else: 377 if (sprite_ids >= 0 and 378 sprite_ids not in self.delete_pngnums): 379 pngnum["sprite"] = \ 380 self.pngnum_to_pngname[sprite_ids] 381 new_index.append(pngnum) 382 elif pngnum >= 0 and pngnum not in self.delete_pngnums: 383 new_index.append(self.pngnum_to_pngname[pngnum]) 384 elif read_pngnums >= 0 and read_pngnums not in self.delete_pngnums: 385 new_index.append(self.pngnum_to_pngname[read_pngnums]) 386 387 def convert_pngnum_to_pngname(self, tile_entry): 388 new_fg = [] 389 fg_id = tile_entry.get("fg", -10) 390 self.convert_index(fg_id, new_fg) 391 bg_id = tile_entry.get("bg", -10) 392 new_bg = [] 393 self.convert_index(bg_id, new_bg) 394 add_tile_entrys = tile_entry.get("additional_tiles", []) 395 for add_tile_entry in add_tile_entrys: 396 self.convert_pngnum_to_pngname(add_tile_entry) 397 tile_entry["fg"] = new_fg 398 tile_entry["bg"] = new_bg 399 400 new_id = "" 401 entity_id = tile_entry["id"] 402 if isinstance(entity_id, list): 403 # we have to truncate the name since it could get really long, 404 # which means this still may not be unique in the case of a 405 # bunch instances like this but it's better than nothing... 406 new_id = "_".join(entity_id)[0:150] 407 else: 408 new_id = entity_id 409 410 return new_id, tile_entry 411 412 def report_missing(self): 413 for pngnum in self.pngnum_to_pngname: 414 if not self.extracted_pngnums.get(pngnum): 415 print("missing index {}, {}".format( 416 pngnum, self.pngnum_to_pngname[pngnum])) 417 418 419if __name__ == '__main__': 420 args = argparse.ArgumentParser(description=__doc__) 421 args.add_argument( 422 "tileset_dir", action="store", 423 help="local name of the tileset directory") 424 args.add_argument( 425 "--delete_file", dest="del_path", action="store", 426 help="local name of file containing" + 427 " lists of ranges of indices to delete") 428 argsDict = vars(args.parse_args()) 429 430 tileset_dirname = argsDict.get("tileset_dir", "") 431 delete_pathname = argsDict.get("del_path", "") 432 433 refs = PngRefs() 434 all_tiles = refs.get_all_data(tileset_dirname, delete_pathname) 435 436 all_tilesheet_data = all_tiles.get("tiles-new", []) 437 tile_info = all_tiles.get("tile_info", {}) 438 if tile_info: 439 refs.default_width = tile_info[0].get("width") 440 refs.default_height = tile_info[0].get("height") 441 442 overlay_ordering = all_tiles.get("overlay_ordering", []) 443 ts_sequence = [] 444 for tilesheet_data in all_tilesheet_data: 445 ts_data = TileSheetData(tilesheet_data, refs) 446 ts_data.summarize(tile_info, refs) 447 ts_sequence.append(ts_data.ts_filename) 448 449 for tilesheet_data in all_tilesheet_data: 450 ts_filename = tilesheet_data.get("file", "") 451 ts_data = refs.ts_data[ts_filename] 452 if ts_data.fallback: 453 continue 454 tile_entries = {} 455 all_tile_entry = tilesheet_data.get("tiles", []) 456 for tile_entry in all_tile_entry: 457 tile_id = ts_data.parse_tile_entry(tile_entry, refs) 458 if tile_id: 459 tile_entries.setdefault(tile_id, []) 460 tile_entries[tile_id].append(tile_entry) 461 ts_data.tile_entries = tile_entries 462 463 #debug statements to verify pngnum_to_pngname and pngname_to_pngnum 464 #print("pngnum_to_pngname: {}".format( 465 # json.dumps(refs.pngnum_to_pngname, sort_keys=True, indent=2))) 466 #print("pngname_to_pngnum: {}".format( 467 # json.dumps(refs.pngname_to_pngnum, sort_keys=True, indent=2))) 468 #print("{}".format(json.dumps(file_tile_id_to_tile_entrys, indent=2))) 469 #print("{}".format(json.dumps(refs.pngnum_to_tspathname, indent=2))) 470 471 for ts_filename in ts_sequence: 472 out_data = ExtractionData(ts_filename, refs) 473 474 if not out_data.valid: 475 continue 476 out_data.write_expansions() 477 478 for tile_id, tile_entrys in out_data.ts_data.tile_entries.items(): 479 #print("tile id {} with {} entries".format( 480 # tile_id, len(tile_entrys))) 481 for idx, tile_entry in enumerate(tile_entrys): 482 subdir_pathname = out_data.increment_dir() 483 tile_entry_name, tile_entry = \ 484 refs.convert_pngnum_to_pngname(tile_entry) 485 if not tile_entry_name: 486 continue 487 tile_entry_pathname = "{}/{}_{}.json".format( 488 subdir_pathname, tile_entry_name, idx) 489 490 #if os.path.isfile(tile_entry_pathname): 491 # print("overwriting {}".format(tile_entry_pathname)) 492 write_to_json(tile_entry_pathname, tile_entry) 493 out_data.write_images(refs) 494 495 if tile_info: 496 tile_info_pathname = refs.tileset_pathname + "/" + "tile_info.json" 497 write_to_json(tile_info_pathname, tile_info, True) 498 499 refs.report_missing() 500