2024-07-30 17:44:12 +02:00
|
|
|
import xml.etree.ElementTree as ET
|
2023-07-05 23:22:15 +02:00
|
|
|
import json
|
2023-08-08 09:46:07 +02:00
|
|
|
import os
|
2024-07-30 17:44:12 +02:00
|
|
|
import sys
|
2024-07-30 20:15:39 +02:00
|
|
|
import fxconv
|
2024-07-30 17:44:12 +02:00
|
|
|
|
|
|
|
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)
|
2024-07-30 20:15:39 +02:00
|
|
|
columns_str = self.root.get("columns")
|
|
|
|
if columns_str == None: raise Exception("columns not found!")
|
|
|
|
self.columns = int(columns_str)
|
2024-07-30 17:44:12 +02:00
|
|
|
|
|
|
|
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")
|
2024-07-30 20:15:39 +02:00
|
|
|
if self.type == None:
|
|
|
|
self.type = ""
|
|
|
|
if VERBOSE: print("WARNING: Type attribute missing!")
|
2024-07-30 17:44:12 +02:00
|
|
|
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!")
|
2023-08-21 22:53:33 +02:00
|
|
|
|
2023-07-05 23:22:15 +02:00
|
|
|
def convert(input, output, params, target):
|
2024-07-30 17:44:12 +02:00
|
|
|
if params["custom-type"] == "tmx":
|
|
|
|
convert_map(input, output, params, target)
|
2024-07-28 12:40:39 +02:00
|
|
|
return 0
|
2024-07-30 17:44:12 +02:00
|
|
|
elif params["custom-type"] == "json":
|
|
|
|
convert_dialog(input, output, params, target)
|
2024-07-28 12:40:39 +02:00
|
|
|
return 0
|
|
|
|
|
2024-07-30 17:44:12 +02:00
|
|
|
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 = {}
|
|
|
|
|
2024-07-30 20:15:39 +02:00
|
|
|
map_struct = fxconv.Structure()
|
|
|
|
|
2024-07-30 17:44:12 +02:00
|
|
|
# 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")]
|
2024-07-28 12:40:39 +02:00
|
|
|
else:
|
2024-07-30 17:44:12 +02:00
|
|
|
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
|
2024-07-30 20:15:39 +02:00
|
|
|
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)
|
2024-07-30 17:44:12 +02:00
|
|
|
|
|
|
|
def convert_dialog(input, output, params, target):
|
|
|
|
if VERBOSE: print(f"INFO: Converting dialog file {input} -> {output}")
|