From 0d7e8dc18cbe29a6ff7b254a77c7032650ff5e88 Mon Sep 17 00:00:00 2001 From: dominik-korsa <29484605+dominik-korsa@users.noreply.github.com> Date: Sat, 19 Jul 2025 02:13:17 +0200 Subject: [PATCH] Move some prefab processing from JavaScript to Python script --- tools/parse-prefabs.py | 135 +++++++++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 39 deletions(-) diff --git a/tools/parse-prefabs.py b/tools/parse-prefabs.py index db161be..85e884b 100755 --- a/tools/parse-prefabs.py +++ b/tools/parse-prefabs.py @@ -1,8 +1,19 @@ +#!/usr/bin/env -S uv run --script +# +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "pyyaml", +# "scipy", +# ] +# /// + import json import os import re import sys import traceback +from scipy.spatial.transform import Rotation from pathlib import Path import yaml @@ -98,38 +109,42 @@ def try_parse_track_transform(component, ancestor_level=0): if "Transform" not in component: return None transform = component["Transform"] - parent = None if "Transform" in transform["m_Father"]: parent = try_parse_track_transform(transform["m_Father"], True) + else: + parent = { + "local_position": [0, 0, 0], + "local_rotation": Rotation.identity(), + "negate_radius": False, + } if transform["m_LocalScale"]["x"] not in {-1, 1} or transform["m_LocalScale"]["y"] != 1 or transform["m_LocalScale"]["z"] != 1: eprint("Unexpected scale in track transform") - if ancestor_level > 0: - if transform["m_LocalPosition"]["x"] != 0 or transform["m_LocalPosition"]["y"] != 0 or transform["m_LocalPosition"]["z"] != 0: - eprint("Unexpected position change in ancestor track transform") - if transform["m_LocalRotation"]["x"] != 0 or transform["m_LocalRotation"]["y"] != 0 or transform["m_LocalRotation"]["z"] != 0 or transform["m_LocalRotation"]["w"] != 1: - eprint("Unexpected rotation change in ancestor track transform") + local_position = [transform["m_LocalRotation"][key] for key in ["x", "y", "z"]] + local_rotation = Rotation.from_quat([transform["m_LocalRotation"][key] for key in ["x", "y", "z", "w"]]) + negate_radius = parent["negate_radius"] - mirror_x = transform["m_LocalScale"]["x"] == -1 - if mirror_x and ancestor_level != 1: - eprint("Expected mirror_x only in the direct parent of the track transform") + if transform["m_LocalScale"]["x"] == -1: + negate_radius = not negate_radius + rot_vet = local_rotation.as_rotvec() + # Flip the x-axis to apply the mirroring and then negate the whole vector to change the rotation direction + rot_vet[1] = -rot_vet[1] + rot_vet[2] = -rot_vet[2] + local_rotation = Rotation.from_rotvec(rot_vet) - parent_mirror_x = False - if parent is not None: - parent_mirror_x = parent["mirror_x"] + if parent is not None and parent["negate_radius"]: + negate_radius = not negate_radius return { - "position": transform["m_LocalPosition"], - "rotation": transform["m_LocalRotation"], - "scale": transform["m_LocalScale"], - "mirror_x": mirror_x, - "parent_mirror_x": parent_mirror_x, - "parent": parent, + "local_position": parent["local_position"] + parent["local_rotation"].apply(local_position), + "local_rotation": parent["local_rotation"] * local_rotation, + "negate_radius": negate_radius, } -def parse_track(track): +def parse_track(track, data_index): + track_id = track["__component_id"] track_shape = None track_transform = None @@ -146,21 +161,38 @@ def parse_track(track): if maybe_track_transform is not None: track_transform = maybe_track_transform - prev_id = None - next_id = None - if "__component_id" in track["MonoBehaviour"]["prevTrack"]: - prev_id = track["MonoBehaviour"]["prevTrack"]["__component_id"] - if "__component_id" in track["MonoBehaviour"]["nextTrack"]: - next_id = track["MonoBehaviour"]["nextTrack"]["__component_id"] + connections = [] + + def append_connection(end, field): + if "__component_id" in track["MonoBehaviour"][field]: + other_id = track["MonoBehaviour"][field]["__component_id"] + if other_id == track_id: + # Ignore connections to self, TD2 has them in Rkp switches + return + connections.append({ + "type": "", + "end": end, + "internalId": other_id + }) + else: + connections.append({ + "type": "", + "end": end, + }) + + append_connection("", "prevTrack") + append_connection("", "nextTrack") + + if track_transform["negate_radius"]: + track_shape["radius"] = -track_shape["radius"] return { - "id": track["__component_id"], - "prev_id": prev_id, - "next_id": next_id, - "shape": track_shape, - "mirror_x": track_transform["parent_mirror_x"], - "position": track_transform["position"], - "rotation": track_transform["rotation"], + "id": track_id, + "dataIndex": data_index, + **track_shape, + "pos": f"", + "rot": f"", + "connections": connections, } @@ -170,10 +202,31 @@ def find_tracks(prefab): switch = try_parse_switch_component(component) if switch is None: continue - for track in switch["tracks"]: - tracks.append(parse_track(track)) + for data_index, track in enumerate(switch["tracks"]): + tracks.append(parse_track(track, data_index)) + add_missing_connections(tracks) return tracks +def add_missing_connections(tracks): + track_map = {track["id"]: track for track in tracks} + for track in tracks: + for connection in track["connections"]: + if connection["type"] == "": + continue + other_track = track_map.get(connection["internalId"], None) + if other_track is None: + eprint(f"Warning: Track {track['id']} has a connection to an unknown track {connection['internalId']}") + continue + reverse_connections = [conn for conn in other_track["connections"] if conn["type"] == "" and conn["internalId"] == track["id"]] + if len(reverse_connections) > 1: + eprint(f"Warning: Track {other_track['id']} has multiple connections to {track["id"]}") + if len(reverse_connections) == 0: + other_track["connections"].append({ + "type": "", + "end": "" if connection["end"] == "" else "", # Heuristic + "internalId": track["id"] + }) + return tracks def parse_prefab_component(component_yaml, component_type, component_id): loaded = yaml.load(component_yaml, yaml.SafeLoader) @@ -203,11 +256,15 @@ def resolve_references(value, component_map, visited_ids): resolve_references(value[key], component_map, visited_ids) -def format_tracks(prefab_name, tracks): - print(f"\"{prefab_name}\": [") +def format_prefab(prefab_name, tracks): + print(f'"{prefab_name}": {{') + print(' "tracks": [') for track in tracks: - print(f" {json.dumps(track)},") - print("],") + track_object = json.dumps(track) + track_object = re.sub(r'(?"]+)>"', r'\1', track_object) + print(f" {track_object},") + print(" ],") + print("},") def process_file(file_path): @@ -223,7 +280,7 @@ def process_file(file_path): with open(file_path, encoding="utf8") as infile: prefab = parse_prefab(infile) tracks = find_tracks(prefab) - format_tracks(Path(file_path).stem, tracks) + format_prefab(Path(file_path).stem, tracks) except Exception: eprint(f"Error processing {file_path}:", file=sys.stderr) traceback.print_exc(file=sys.stderr)