""" This is the main converter script. It uses the tiled.py script to handle the tiled maps. We're trying to follow the PEP, so please read PEP 8 (if you haven't already): https://peps.python.org/pep-0008/, so please write variable names in sneak_case and class names in PascalCase. To improve the lisibility of this code, please document your methods, add comments (yes, it's hard to add the right amount of comments), and add type hints, to avoid bugs and make it easy to understand how to use them. To document your methods, you should read PEP 257: https://peps.python.org/pep-0257/. Thanks, Mibi88 """ import xml.etree.ElementTree as ET import json import os import sys # Add the assets folder to the path, to be able to import the tiled script. sys.path.append("../assets/") import fxconv from tinytiled import * # If the output of the converter should be verbose. VERBOSE = 1 # The sign types, used to find the sign icon. SIGN_TYPES = ["SGN", "INFO"] # The NPC faces, used to find the face id. FACES = ["MALE", "FEMALE", "MILKMAN", "POLICE"] # The precision of the fixed point numbers. # WARNING: The PRECISION define in config.h should contain the same value! PRECISION = 8 def convert(input: str, output: str, params: dict, target): """ This method gets called by fxconv for each asset to convert. """ if params["custom-type"] == "tmx": convert_map(input, output, params, target) return 0 elif params["custom-type"] == "dialog": convert_dialog(input, output, params, target) return 0 def convert_map(input: str, output: str, params: dict, target): """ Convert a map. """ if VERBOSE: print(f"INFO: Converting map {input} -> {output}") input_map = Map(input) dialog_file = "" background_layer = [] foreground_layer = [] walkable_layer = [] map_x = 0 map_y = 0 width = 0 height = 0 outdoor_tileset = None walkable_tileset = None dialog_num = 0 dialog_ids = [] npc_paths = {} npcs = {} signs = {} portals = {} name = os.path.splitext(os.path.basename(input))[0] map_struct = fxconv.Structure() # Get the dialog file try: if VERBOSE: print("INFO: Getting the dialog file") dialog_file = input_map.get_property("dialogFile") if VERBOSE: print(f"INFO: Dialog file: {dialog_file}.") except Exception as e: # Show a simple error message on failure. sys.stderr.write(f"ERROR: Failed to get the dialog file.\n" + f" Error message: {e}\n") sys.exit(1) # Get the map position try: if VERBOSE: print("INFO: Getting the map position") map_x = int(input_map.get_property("mapX")) map_y = int(input_map.get_property("mapY")) if VERBOSE: print(f"INFO: Map position: ({map_x}, {map_y}).") except Exception as e: # Show a simple error message on failure. sys.stderr.write(f"ERROR: Failed to get the map position.\n" + f" Error message: {e}\n") sys.exit(1) # Get informations about dialogs try: if VERBOSE: print("INFO: Getting informations about dialogs") with open(f"{input_map.parent_dir}/{dialog_file}", "r") as file: dialog_data = json.load(file) dialog_num = len(dialog_data["dialogs"]) for i in dialog_data["dialogs"]: dialog_ids.append(i["ID"]) except Exception as e: # Show a simple error message on failure. sys.stderr.write(f"ERROR: Failed to get informations about dialogs.\n" + f" Error message: {e}\n") sys.exit(1) # Get the outdoor tileset try: if VERBOSE: print("INFO: Getting the outdoor tileset") outdoor_tileset = input_map.get_tileset_by_firstgid(1) except Exception as e: # Show a simple error message on failure. sys.stderr.write(f"ERROR: Failed to get the outdoor tileset.\n" + f" Error message: {e}\n") sys.exit(1) # Get the walkable tileset try: if VERBOSE: print("INFO: Getting the walkable tileset") walkable_tileset = input_map.get_tileset_by_firstgid(409) except Exception as e: # Show a simple error message on failure. sys.stderr.write(f"ERROR: Failed to get the walkable tileset.\n" + f" Error message: {e}\n") sys.exit(1) # Get the background try: if VERBOSE: print("INFO: Getting the background layer") bg_layer = input_map.get_layer_by_name("Background") # The bg layer will be used to set the map width and height. width = bg_layer.get_width() height = bg_layer.get_height() if VERBOSE: print(f"INFO: Map size: ({width}, {height}).") # Get the layer data himself background_layer = bg_layer.get_data_with_tileset(outdoor_tileset) # Check if the size of the layer data is correct. if len(background_layer) != width*height: raise Exception("Bad layer size!") if VERBOSE: print("INFO: Layer data has the right size.") except Exception as e: # Show a simple error message on failure. sys.stderr.write(f"ERROR: Failed to get the background layer.\n" + f" Error message: {e}\n") sys.exit(1) # Get the foreground try: if VERBOSE: print("INFO: Getting the foreground layer") fg_layer = input_map.get_layer_by_name("Foreground") # Get the layer data himself foreground_layer = fg_layer.get_data_with_tileset(outdoor_tileset) # Check if the size of the layer data is correct. if len(foreground_layer) != width*height: raise Exception("Bad layer size!") if VERBOSE: print("INFO: Layer data has the right size.") except Exception as e: # Show a simple error message on failure. sys.stderr.write(f"ERROR: Failed to get the foreground layer.\n" + f" Error message: {e}\n") sys.exit(1) # Get the walkable layer try: if VERBOSE: print("INFO: Getting the walkable layer") wk_layer = input_map.get_layer_by_name("Walkable") # Get the layer data himself walkable_layer = wk_layer.get_data_with_tileset(walkable_tileset) # Check if the size of the layer data is correct. if len(walkable_layer) != width*height: raise Exception("Bad layer size!") if VERBOSE: print("INFO: Layer data has the right size.") except Exception as e: # Show a simple error message on failure. sys.stderr.write(f"ERROR: Failed to get the walkable layer.\n" + f" Error message: {e}\n") sys.exit(1) # Get the extra data try: if VERBOSE: print("INFO: Getting the extra data") ed_objgroup = input_map.get_objectgroup_by_name("ExtraData") # Get the paths the NPCs take. for object in ed_objgroup.objects: if object.get_data_type() == "polyline": npc_paths[object.id] = object.get_data() # Get the NPCs for object in ed_objgroup.objects: if object.get_data_type() == "point" and object.type == "NPC": path = [0, 0] if int(object.get_property("hasPath")): if object.get_property("path") in npc_paths: path = npc_paths[object.get_property("path")] else: raise Exception("Path required but not found!") dialog_id = 0 has_dialog = 0 try: dialog_id = int(object.get_property("dialogID")) has_dialog = 1 except: pass data = { "position": object.get_data(), "name": object.name, "needAction": int(object.get_property("needAction")), "dialogID": dialog_id, "hasDialog": has_dialog, "face": FACES.index(object.get_property("face")), "path": path } npcs[object.id] = data # Get the signs for object in ed_objgroup.objects: if object.get_data_type() == "point" and object.type in SIGN_TYPES: data = { "position": object.get_data(), "name": object.name, "needAction": int(object.get_property("needAction")), "dialogID": int(object.get_property("dialogID")), "icon": SIGN_TYPES.index(object.type) } signs[object.id] = data # Get the portals for object in ed_objgroup.objects: if (object.get_data_type() == "rectangle" and object.type == "PORTAL"): data = { "rect": object.get_data(), "name": object.name, "dest": object.get_property("dest"), "destPortal": object.get_property("destPortal") } portals[object.id] = data except Exception as e: # Show a simple error message on failure. sys.stderr.write(f"ERROR: Failed to get the extra data.\n" + f" Error message: {e}\n") sys.exit(1) # Generate the structs # Map struct map_struct += fxconv.u32(map_x) map_struct += fxconv.u32(map_y) map_struct += fxconv.u32(width) map_struct += fxconv.u32(height) map_struct += fxconv.u32(3) map_struct += fxconv.u32(outdoor_tileset.columns) tileset_name = os.path.splitext(os.path.basename(outdoor_tileset.source))[0] map_struct += fxconv.ref(f"img_{tileset_name}") # Store the walkable layer walkable_data = bytes() for i in walkable_layer: if i < 0: i = 0 walkable_data += fxconv.u8(i) map_struct += fxconv.ptr(walkable_data) # Load NPCs map_struct += fxconv.u32(len(npcs)) npc_struct = fxconv.Structure() for i in npcs.values(): # Convert currentpos to a fixed point value. npc_struct += fxconv.u32((i["position"][0]+i["path"][0])< 2) npc_struct += fxconv.u32(len(i["path"])//2) npc_struct += fxconv.u32(0) xpath = bytes() ypath = bytes() x = True for n in i["path"]: if x: xpath += fxconv.u16(n) else: ypath += fxconv.u16(n) x = not x npc_struct += fxconv.ptr(xpath) npc_struct += fxconv.ptr(ypath) npc_struct += fxconv.u32(0) # TODO: Type npc_struct += fxconv.u8(0) # TODO: Group npc_struct += fxconv.u8(0) # TODO: Hostile to npc_struct += fxconv.u16(0) # TODO: Padding (what is it ?) map_struct += fxconv.ptr(npc_struct) # Load signs map_struct += fxconv.u32(len(signs)) sign_struct = fxconv.Structure() for i in signs.values(): # Create a sign struct for each sign. sign_struct += fxconv.u32(i["position"][0]) sign_struct += fxconv.u32(i["position"][1]) sign_struct += fxconv.u32(i["icon"]) sign_struct += fxconv.string(i["name"]) sign_struct += fxconv.u32(i["dialogID"]) sign_struct += fxconv.u32(i["needAction"]) map_struct += fxconv.ptr(sign_struct) # Load portals map_struct += fxconv.u32(len(portals)) portal_struct = fxconv.Structure() for i in portals.values(): dest_file_name = os.path.splitext(os.path.basename(i["dest"]))[0] dest_portal = i["destPortal"] portal_name = i["name"] if VERBOSE: print(f"INFO: Storing portal {name}_{portal_name}") portal_struct += fxconv.sym(f"{name}_{portal_name}") # Add the collider rect = i["rect"] portal_struct += fxconv.u32(rect[0]) portal_struct += fxconv.u32(rect[1]) portal_struct += fxconv.u32(rect[2]) portal_struct += fxconv.u32(rect[3]) # Add a reference to the destination portal portal_struct += fxconv.ref(f"{dest_file_name}_{dest_portal}") portal_struct += fxconv.ref(f"{dest_file_name}") map_struct += fxconv.ptr(portal_struct) map_struct += fxconv.u32(dialog_num) # Get the name of the dialog file and create a reference to it: it is built # separately. dialog_name = os.path.splitext(os.path.basename(dialog_file))[0] map_struct += fxconv.ref(f"_{dialog_name}") # Store the background layer background_data = bytes() for i in background_layer: background_data += fxconv.u16(i) map_struct += fxconv.ptr(background_data) # Store the foreground layer foreground_data = bytes() for i in foreground_layer: foreground_data += fxconv.u16(i) map_struct += fxconv.ptr(foreground_data) # Create the fxconv object fxconv.elf(map_struct, output, f"_{name}", **target) def convert_dialog(input: str, output: str, params: dict, target): """ Convert a JSON dialog file. """ if VERBOSE: print(f"INFO: Converting dialog file {input} -> {output}") # Load the JSON dialog file. dialog_data = None try: with open(input, "r") as file: dialog_data = json.load(file) except Exception as e: sys.stderr.write(f"ERROR: Failed parse json.\n" + f" Error message: {e}\n") sys.exit(1) # Create the dialog struct dialog_struct = fxconv.Structure() try: for i in dialog_data["dialogs"]: # Create a dialog structure for each dialog. dialog_id = i["ID"] dialog_struct += fxconv.u32(dialog_id) dialog_struct += fxconv.string(i["dialog"]) dialog_struct += fxconv.u32(i["isQuestion"]) dialog_struct += fxconv.string(i["choice"].replace('$', '\0')) dialog_struct += fxconv.string(i["conclusion1"]) dialog_struct += fxconv.u32(i["next1"]) dialog_struct += fxconv.string(i["conclusion2"]) dialog_struct += fxconv.u32(i["next2"]) dialog_struct += fxconv.u32(i["nextOther"]) # Save this struct name = os.path.splitext(os.path.basename(input))[0] fxconv.elf(dialog_struct, output, f"__{name}", **target) except Exception as e: # Show an error message if the conversion fails. sys.stderr.write(f"ERROR: Failed convert dialogs.\n" + f" Error message: {e}\n") sys.exit(1)