import xml.etree.ElementTree as ET import json import os import sys import fxconv VERBOSE = 1 SIGN_TYPES = ["INFO", "SGN"] # TODO: Add more doc. class Tileset: """ Handle the tiled tileset. """ def __init__(self, element: ET.Element, parent_dir = ""): firstgid_str = element.get("firstgid") if firstgid_str == None: raise Exception("firstgid not found!") self.firstgid = int(firstgid_str) self.source = element.get("source") if self.source == None: raise Exception("source not found!") self.source = parent_dir + self.source tree = ET.parse(self.source) self.root = tree.getroot() tilecount_str = self.root.get("tilecount") if tilecount_str == None: raise Exception("tilecount not found!") self.tilecount = int(tilecount_str) columns_str = self.root.get("columns") if columns_str == None: raise Exception("columns not found!") self.columns = int(columns_str) def is_raw_in_tileset(self, raw: int) -> bool: if raw >= self.firstgid and raw < self.firstgid+self.tilecount: return True return False def get_tile_from_raw(self, raw: int) -> int: if not self.is_raw_in_tileset(raw) and raw: raise Exception(f"Tile {raw} not in tileset!") return raw-self.firstgid class Layer: """ A class to handle a tiled map layer """ def __init__(self, element: ET.Element): self.element = element def get_width(self) -> int: """ Get the layer width """ value = self.element.get("width") if value == None: raise Exception("Layer width not found") return int(value) def get_height(self) -> int: """ Get the layer height """ value = self.element.get("height") if value == None: raise Exception("Layer height not found") return int(value) def get_raw_data(self) -> list: """ Get the data of the map """ data_tag = self.element.find("data") if data_tag == None: raise Exception("Data not found!") raw_data = data_tag.text.split(",") int_data = [] for tile in raw_data: int_data.append(int(tile)) return int_data def get_data_with_tileset(self, tileset: Tileset): raw_data = self.get_raw_data() out_data = [] for i in raw_data: out_data.append(tileset.get_tile_from_raw(i)) return out_data class Object: """ An group object (see ObjectGroup) object. """ def __init__(self, element: ET.Element): self.element = element self.name = element.get("name") if self.name == None: raise Exception("Name attribute missing!") self.type = element.get("type") if self.type == None: self.type = "" if VERBOSE: print("WARNING: Type attribute missing!") x_str = element.get("x") if x_str == None: raise Exception("X attribute missing!") self.x = int(float(x_str)) y_str = element.get("y") if y_str == None: raise Exception("Y attribute missing!") self.y = int(float(y_str)) self.id = element.get("id") if self.id == None: raise Exception("ID attribute missing!") def __get_point(self) -> list: # Private method to get a point. Used in get_data. return [self.x, self.y] def __get_polyline(self) -> list: # Private method to get a polyline. Used in get_data. data = self.element.find("polyline").get("points") if data == None: raise Exception("Data not found!") data = data.replace(' ', ',').split(',') out_data = [] for i in data: out_data.append(int(float(i))) return out_data def get_data(self) -> list: """ Get the geometric shape of this object. """ if self.element.find("point") != None: # It is a point. return self.__get_point() if self.element.find("polyline") != None: # It is a polyline. return self.__get_polyline() raise Exception("Unknown data!") def get_data_type(self) -> str: """ Get the geometric shape of this object. """ if self.element.find("point") != None: # It is a point. return "point" if self.element.find("polyline") != None: # It is a polyline. return "polyline" raise Exception("Unknown data!") def get_property(self, property: str) -> str: """ Get the value of a property. """ properties = self.element.find("properties") if properties == None: raise Exception("Properties not found!") for i in properties: if i.get("name") == property: value = i.get("value") if value == None: raise Exception("Property value not found!") return value raise Exception(f"Property {property} not found!") class ObjectGroup: """ Handle tiled object groups. They can contain points, lines and other geometric shapes that can be very handy to add NPCs, the path they walk on, as we do it here, in Collab_RPG. """ def __init__(self, element: ET.Element): self.element = element self.objects = [] for object in self.element.iterfind("object"): self.objects.append(Object(object)) class Map: """ A class to handle the tiled maps. """ def __init__(self, input: str): """ Loads a tmx map made with tiled. """ tree = ET.parse(input) self.root = tree.getroot() self.parent_dir = os.path.abspath(input).rpartition('/')[0] def get_property(self, property: str) -> str: """ Get a map property. """ properties = self.root.find("properties") # If properties wasn't found. if properties == None: raise Exception("Properties not found!") for child in properties: # Look at the name attribute of each property if child.get("name") == property: value = child.get("value") if value == None: raise Exception("Value attribute not found!") return value # The dialog file property wasn't found. raise Exception(f"\"{property}\" property not found!") def get_layer_by_id(self, layer_id: str) -> Layer: """ Get a layer by its id. """ for layer in self.root.iterfind("layer"): if layer.get("id") == layer_id: return Layer(layer) raise Exception("Layer not found!") def get_layer_by_name(self, name: str) -> Layer: """ Get a layer by its name. """ for layer in self.root.iterfind("layer"): if layer.get("name") == name: return Layer(layer) raise Exception("Layer not found!") def get_objectgroup_by_id(self, group_id: str) -> Layer: """ Get a layer by its id. """ for layer in self.root.iterfind("objectgroup"): if layer.get("id") == group_id: return ObjectGroup(layer) raise Exception("Object group not found!") def get_objectgroup_by_name(self, name: str) -> Layer: """ Get a layer by its name. """ for layer in self.root.iterfind("objectgroup"): if layer.get("name") == name: return ObjectGroup(layer) raise Exception("Object group not found!") def get_tileset_by_firstgid(self, firstgid: int) -> Tileset: for tileset in self.root.iterfind("tileset"): if tileset.get("firstgid") == str(firstgid): return Tileset(tileset, self.parent_dir + "/") raise Exception("Tileset not found!") def convert(input, output, params, target): if params["custom-type"] == "tmx": convert_map(input, output, params, target) return 0 elif params["custom-type"] == "json": convert_dialog(input, output, params, target) return 0 def convert_map(input, output, params, target): if VERBOSE: print(f"INFO: Converting map {input} -> {output}") input_map = Map(input) dialog_file = "" background_layer = [] foreground_layer = [] walkable_layer = [] width = 0 height = 0 outdoor_tileset = None walkable_tileset = None npc_paths = {} npcs = {} signs = {} 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: sys.stderr.write(f"ERROR: Failed to get the dialog file.\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: 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: 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: 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: 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: 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 = None 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!") data = { "position": object.get_data(), "needAction": object.get_property("needAction"), "dialogID": object.get_property("dialogID"), #"face": 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 = { "needAction": object.get_property("needAction"), "dialogID": object.get_property("dialogID") } signs[object.id] = data except Exception as e: 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 += fxconv.u32(width) map_struct += fxconv.u32(height) map_struct += fxconv.u32(3) map_struct += fxconv.u32(outdoor_tileset.columns) map_struct += fxconv.u32(0) map_struct += fxconv.u32(0) map_struct += fxconv.u32(0) map_struct += fxconv.u32(0) tileset_name = os.path.splitext(os.path.basename(outdoor_tileset.source))[0] map_struct += fxconv.ref(f"img_{tileset_name}") walkable_data = bytes() for i in walkable_layer: if i < 0: i = 0 walkable_data += fxconv.u8(i) map_struct += fxconv.ptr(walkable_data) map_struct += fxconv.u32(0) # TODO: NPC support in-game map_struct += fxconv.ptr(bytes()) map_struct += fxconv.u32(0) # TODO: Sign support in-game map_struct += fxconv.ptr(bytes()) map_struct += fxconv.u32(0) # TODO: Portal support in-game map_struct += fxconv.ptr(bytes()) map_struct += fxconv.u32(0) # TODO: Dialog support map_struct += fxconv.ptr(bytes()) background_data = bytes() for i in background_layer: background_data += fxconv.u16(i) map_struct += fxconv.ptr(background_data) foreground_data = bytes() for i in foreground_layer: foreground_data += fxconv.u16(i) map_struct += fxconv.ptr(foreground_data) # Create the fxconv object name = os.path.splitext(os.path.basename(input))[0] fxconv.elf(map_struct, output, f"_{name}", **target) def convert_dialog(input, output, params, target): if VERBOSE: print(f"INFO: Converting dialog file {input} -> {output}")