Collab_RPG/assets/converters.py

336 lines
13 KiB
Python
Raw Normal View History

"""
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
"""
2024-07-30 17:44:12 +02:00
import xml.etree.ElementTree as ET
import json
import os
2024-07-30 17:44:12 +02:00
import sys
# Add the assets folder to the path, to be able to import the tiled script.
sys.path.append("../assets/")
2024-07-30 20:15:39 +02:00
import fxconv
from tinytiled import *
2024-07-30 17:44:12 +02:00
# If the output of the converter should be verbose.
2024-07-30 17:44:12 +02:00
VERBOSE = 1
# The sign types, used to find the sign icon.
2024-07-31 12:54:40 +02:00
SIGN_TYPES = ["SGN", "INFO"]
# The NPC faces, used to find the face id.
2024-07-31 12:54:40 +02:00
FACES = ["MALE", "FEMALE", "MILKMAN", "POLICE"]
# The precision of the fixed point numbers.
# WARNING: The PRECISION define in config.h should contain the same value!
2024-07-31 14:38:29 +02:00
PRECISION = 8
2024-07-30 17:44:12 +02:00
def convert(input: str, output: str, params: dict, target):
"""
This method gets called by fxconv for each asset to convert.
"""
2024-07-30 17:44:12 +02:00
if params["custom-type"] == "tmx":
convert_map(input, output, params, target)
return 0
elif params["custom-type"] == "dialog":
2024-07-30 17:44:12 +02:00
convert_dialog(input, output, params, target)
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 = []
map_x = 0
map_y = 0
2024-07-30 17:44:12 +02:00
width = 0
height = 0
outdoor_tileset = None
walkable_tileset = None
dialog_num = 0
dialog_ids = []
2024-07-30 17:44:12 +02:00
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)
2024-07-31 12:54:40 +02:00
# 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:
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:
sys.stderr.write(f"ERROR: Failed to get informations about dialogs.\n"
+ f" Error message: {e}\n")
sys.exit(1)
2024-07-30 17:44:12 +02:00
# 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":
2024-07-31 14:12:55 +02:00
path = [0, 0]
2024-07-30 17:44:12 +02:00
if int(object.get_property("hasPath")):
if object.get_property("path") in npc_paths:
path = npc_paths[object.get_property("path")]
else:
2024-07-30 17:44:12 +02:00
raise Exception("Path required but not found!")
2024-07-31 13:53:47 +02:00
dialog_id = 0
has_dialog = 0
try:
dialog_id = int(object.get_property("dialogID"))
has_dialog = 1
except:
pass
2024-07-30 17:44:12 +02:00
data = {
"position": object.get_data(),
2024-07-31 12:54:40 +02:00
"name": object.name,
"needAction": int(object.get_property("needAction")),
2024-07-31 13:53:47 +02:00
"dialogID": dialog_id,
"hasDialog": has_dialog,
2024-07-31 12:54:40 +02:00
"face": FACES.index(object.get_property("face")),
2024-07-30 17:44:12 +02:00
"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 = {
2024-07-31 12:54:40 +02:00
"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)
2024-07-30 17:44:12 +02:00
}
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
map_struct += fxconv.u32(map_x)
map_struct += fxconv.u32(map_y)
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)
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)
# Load NPCs
2024-07-31 13:53:47 +02:00
map_struct += fxconv.u32(len(npcs))
npc_struct = fxconv.Structure()
for i in npcs.values():
2024-07-31 14:38:29 +02:00
npc_struct += fxconv.u32((i["position"][0]+i["path"][0])<<PRECISION)
npc_struct += fxconv.u32((i["position"][1]+i["path"][1])<<PRECISION)
2024-07-31 13:53:47 +02:00
npc_struct += fxconv.u32(i["position"][0])
npc_struct += fxconv.u32(i["position"][1])
npc_struct += fxconv.u16(i["face"])
npc_struct += fxconv.u8(0)
npc_struct += fxconv.u8(i["hasDialog"])
npc_struct += fxconv.u32(i["dialogID"])
npc_struct += fxconv.u32(i["needAction"])
npc_struct += fxconv.string(i["name"])
npc_struct += fxconv.u32(len(i["path"]) > 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
2024-07-31 13:53:47 +02:00
map_struct += fxconv.u32(len(signs))
2024-07-31 12:54:40 +02:00
sign_struct = fxconv.Structure()
for i in signs.values():
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
2024-07-30 20:15:39 +02:00
map_struct += fxconv.u32(0) # TODO: Portal support in-game
map_struct += fxconv.ptr(bytes())
map_struct += fxconv.u32(dialog_num)
dialog_name = os.path.splitext(os.path.basename(dialog_file))[0]
map_struct += fxconv.ref(f"_{dialog_name}")
2024-07-30 20:15:39 +02:00
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}")
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)
dialog_struct = fxconv.Structure()
try:
for i in dialog_data["dialogs"]:
dialog_id = i["ID"]
dialog_struct += fxconv.u32(dialog_id)
dialog_struct += fxconv.string(i["dialog"])
dialog_struct += fxconv.u32(i["isQuestion"])
2024-07-31 12:54:40 +02:00
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:
sys.stderr.write(f"ERROR: Failed convert dialogs.\n"
+ f" Error message: {e}\n")
sys.exit(1)