#!/usr/bin/env python3 """Generate typed 1000:0270 collision-call data from preserved raw evidence.""" from __future__ import annotations import argparse from pathlib import Path ROOT = Path(__file__).resolve().parent.parent RAW = ROOT / "TDKPIN_GHIDRA_COMPLETE_RAW.c" OUTPUT = ROOT / "reconstructed" / "tdkpin_game_setup_collision_data.inc" def calls_from_raw() -> list[list[int]]: text = RAW.read_text(encoding="utf-8") start = text.index("/* ===== 1000:0270 ") end = text.index("/* ===== 1000:51f5 ", start) text = text[start:end] needle = "FUN_1008_0138(" calls: list[list[int]] = [] position = 0 while True: begin = text.find(needle, position) if begin < 0: break cursor = begin + len(needle) depth = 1 while depth: character = text[cursor] if character == "(": depth += 1 elif character == ")": depth -= 1 cursor += 1 arguments = text[begin + len(needle) : cursor - 1] words = [ int(item.strip(), 0) & 0xFFFF for item in arguments.replace("\n", "").split(",") ] if len(words) != 40: raise RuntimeError(f"expected 40 words, found {len(words)}") calls.append(words) position = cursor if len(calls) != 177: raise RuntimeError(f"expected 177 calls, found {len(calls)}") return calls def u16(value: int) -> str: return f"0x{value:04x}u" def i32(low: int, high: int) -> str: value = low | high << 16 return f"(int32_t)UINT32_C(0x{value:08x})" def real48(words: list[int]) -> str: bytes_ = [] for word in words: bytes_.extend((word & 0xFF, word >> 8)) return "{{" + ", ".join(f"0x{byte:02x}" for byte in bytes_) + "}}" def initializer(words: list[int], ordinal: int) -> str: fields = [ f".render_bottom={u16(words[0])}", f".render_right={u16(words[1])}", f".render_top={u16(words[2])}", f".render_left={u16(words[3])}", f".fallback_y_adjust={i32(words[4], words[5])}", f".fallback_x_adjust={i32(words[6], words[7])}", f".layer_mask={u16(words[8])}", f".score_low={u16(words[9])}", f".score_high={u16(words[10])}", f".contact_state={u16(words[11])}", f".flags={u16(words[12])}", f".response_kick={real48(words[13:16])}", f".response_auxiliary={real48(words[16:19])}", f".response_tangent={real48(words[19:22])}", f".response_normal={real48(words[22:25])}", f".radius={i32(words[25], words[26])}", f".point2_y={i32(words[27], words[28])}", f".point2_x={i32(words[29], words[30])}", f".point1_y={i32(words[31], words[32])}", f".point1_x={i32(words[33], words[34])}", f".coordinate_mode={u16(words[35])}", f".subtype={u16(words[36])}", f".type={u16(words[37])}", f".active={u16(words[38])}", f".index={u16(words[39])}", ] return ( f" /* call {ordinal:03d}, record {words[39]:3d} */ " "{" + ", ".join(fields) + "}," ) def generated_text() -> str: calls = calls_from_raw() lines = [ "/* Generated by tools/generate_game_setup_collision_data.py.", " * Source: TDKPIN_GHIDRA_COMPLETE_RAW.c, 1000:0270 call order.", " * Do not hand-edit; run the generator and its --check mode.", " */", "static const TdkpinCollisionInitArgs g_game_setup_collision_calls[177] = {", ] lines.extend(initializer(words, index + 1) for index, words in enumerate(calls)) lines.append("};") lines.append("") return "\n".join(lines) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--check", action="store_true") arguments = parser.parse_args() generated = generated_text() if arguments.check: if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != generated: raise SystemExit(f"generated collision data is stale: {OUTPUT}") return 0 OUTPUT.write_text(generated, encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main())