2026-03-25 23:32:36 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import unittest
|
|
|
|
|
|
2026-03-26 00:37:17 +01:00
|
|
|
from tools.poc_crusader_usecode_parser import (
|
2026-03-26 22:10:48 +01:00
|
|
|
format_target_event_reference,
|
2026-03-26 00:37:17 +01:00
|
|
|
get_intrinsic_hints,
|
|
|
|
|
intrinsic_display_name,
|
|
|
|
|
render_partially_structured_blocks,
|
|
|
|
|
render_structured_pseudocode,
|
2026-03-26 22:10:48 +01:00
|
|
|
try_decode_loop_selector,
|
2026-03-26 00:37:17 +01:00
|
|
|
)
|
2026-03-25 23:32:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class UsecodeStructuringTests(unittest.TestCase):
|
|
|
|
|
def test_alarmbox_style_forward_flow_renders_without_block_labels(self) -> None:
|
|
|
|
|
blocks = [
|
|
|
|
|
("entry", ["set_info(0x0211, *(arg_06));", "process_exclude();", "if var goto block_0330;"]),
|
|
|
|
|
("block_026D", ["if !Intrinsic0000() goto block_02CB;"]),
|
|
|
|
|
("block_027C", ["if (Item.getFrame(arg_06) != 0) goto block_029B;"]),
|
|
|
|
|
("block_028B", ["goto block_02BA;"]),
|
|
|
|
|
("block_029B", ["if (Item.getFrame(arg_06) != 1) goto block_02BA;"]),
|
|
|
|
|
("block_02AA", ["goto block_02BA;"]),
|
|
|
|
|
("block_02BA", ["spawn class_0A0C_slot_3B(0x00000000);"]),
|
|
|
|
|
("block_02CB", ["a = Item.getStatus(arg_06);", "if ((a & 4) != 0) goto block_032D;"]),
|
|
|
|
|
("block_02E7", ["if (Item.getMapNum(arg_06) != 0) goto block_032D;"]),
|
|
|
|
|
("block_02F9", ["spawn class_0A18_slot_20(pid, 0, *(arg_06), arg_06);", "suspend;"]),
|
|
|
|
|
("block_032D", ["goto block_03C3;"]),
|
|
|
|
|
("block_0330", ["if Intrinsic0000() goto block_03C3;"]),
|
|
|
|
|
("block_033B", ["if (Item.getFrame(arg_06) != 2) goto block_035A;"]),
|
|
|
|
|
("block_034A", ["goto block_0379;"]),
|
|
|
|
|
("block_035A", ["if (Item.getFrame(arg_06) != 3) goto block_0379;"]),
|
|
|
|
|
("block_0369", ["goto block_0379;"]),
|
|
|
|
|
("block_0379", ["spawn class_0A0C_slot_3C(0x00000000);", "if (Item.getMapNum(arg_06) != 0) goto block_03C3;"]),
|
|
|
|
|
("block_039C", ["spawn class_0A18_slot_20(pid, 1, *(arg_06), arg_06);", "suspend;"]),
|
|
|
|
|
("block_03C3", ["return;"]),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
rendered = render_structured_pseudocode(blocks)
|
|
|
|
|
|
|
|
|
|
self.assertIsNotNone(rendered)
|
|
|
|
|
text = "\n".join(rendered or [])
|
|
|
|
|
self.assertNotIn("block_027C:", text)
|
|
|
|
|
self.assertNotIn("goto block_03C3;", text)
|
|
|
|
|
self.assertIn("if (!var) {", text)
|
|
|
|
|
self.assertIn("if (Intrinsic0000()) {", text)
|
|
|
|
|
self.assertIn("if ((a & 4) == 0) {", text)
|
|
|
|
|
self.assertIn("if (!Intrinsic0000()) {", text)
|
|
|
|
|
|
|
|
|
|
def test_backward_jump_keeps_structured_renderer_disabled(self) -> None:
|
|
|
|
|
blocks = [
|
|
|
|
|
("entry", ["if flag goto block_0010;"]),
|
|
|
|
|
("block_0004", ["return;"]),
|
|
|
|
|
("block_0010", ["goto entry;"]),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
self.assertIsNone(render_structured_pseudocode(blocks))
|
|
|
|
|
|
2026-03-26 00:37:17 +01:00
|
|
|
def test_if_else_branch_renders_as_structured_else(self) -> None:
|
|
|
|
|
blocks = [
|
|
|
|
|
("entry", ["if (Item.getMapNum(arg_06) != 0) goto block_015C;"]),
|
|
|
|
|
("block_00FD", ["if Intrinsic0000() goto block_0132;"]),
|
|
|
|
|
("block_0108", ["spawn class_0A18_slot_20(pid, 0, *(arg_06), arg_06);", "suspend;", "goto block_01C0;"]),
|
|
|
|
|
("block_0132", ["spawn class_0A18_slot_20(pid, 1, *(arg_06), arg_06);", "suspend;", "goto block_01C0;"]),
|
|
|
|
|
("block_015C", ["if Intrinsic0000() goto block_0195;"]),
|
|
|
|
|
("block_0167", ["spawn class_0A18_slot_20(pid, (0 + 0x0080), *(arg_06), arg_06);", "suspend;", "goto block_01C0;"]),
|
|
|
|
|
("block_0195", ["spawn class_0A18_slot_20(pid, (1 + 0x0080), *(arg_06), arg_06);", "suspend;"]),
|
|
|
|
|
("block_01C0", ["return;"]),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
rendered = render_structured_pseudocode(blocks)
|
|
|
|
|
|
|
|
|
|
self.assertIsNotNone(rendered)
|
|
|
|
|
text = "\n".join(rendered or [])
|
|
|
|
|
self.assertIn("if (Item.getMapNum(arg_06) == 0) {", text)
|
|
|
|
|
self.assertIn("else {", text)
|
|
|
|
|
self.assertNotIn("goto block_01C0;", text)
|
|
|
|
|
|
|
|
|
|
def test_loop_header_and_back_edge_render_as_while(self) -> None:
|
|
|
|
|
blocks = [
|
|
|
|
|
("entry", ["/* loopscr value_u8=0x24 */"]),
|
|
|
|
|
("block_0118", ["if condition goto block_0151;"]),
|
|
|
|
|
("block_011B", ["if (Item.getFrame(item) != 0) goto block_014D;"]),
|
|
|
|
|
("block_012D", ["suspend;"]),
|
|
|
|
|
("block_014D", ["/* loopnext */", "goto block_0118;"]),
|
|
|
|
|
("block_0151", ["return;"]),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
rendered = render_structured_pseudocode(blocks)
|
|
|
|
|
|
|
|
|
|
self.assertIsNotNone(rendered)
|
|
|
|
|
text = "\n".join(rendered or [])
|
|
|
|
|
self.assertIn("while (!condition) {", text)
|
|
|
|
|
self.assertNotIn("goto block_0118;", text)
|
|
|
|
|
|
2026-03-26 22:10:48 +01:00
|
|
|
def test_loop_selector_block_renders_as_for_loop(self) -> None:
|
|
|
|
|
blocks = [
|
|
|
|
|
("entry", ["/* loop_selector item in nearby_items(shape=0x04D0, origin=arg_06) */"]),
|
|
|
|
|
("block_0118", ["if condition goto block_0151;"]),
|
|
|
|
|
("block_011B", ["if (Item.getFrame(item) != 0) goto block_014D;", "suspend;"]),
|
|
|
|
|
("block_014D", ["goto block_0118;"]),
|
|
|
|
|
("block_0151", ["return;"]),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
rendered = render_structured_pseudocode(blocks)
|
|
|
|
|
|
|
|
|
|
self.assertIsNotNone(rendered)
|
|
|
|
|
text = "\n".join(rendered or [])
|
|
|
|
|
self.assertIn("for item in nearby_items(shape=0x04D0, origin=arg_06) {", text)
|
|
|
|
|
self.assertNotIn("while (!condition) {", text)
|
|
|
|
|
|
|
|
|
|
def test_loop_selector_renders_in_partial_fallback(self) -> None:
|
|
|
|
|
blocks = [
|
|
|
|
|
("entry", ["/* loop_selector item in nearby_items(shape=0x04D0, origin=arg_06) */"]),
|
|
|
|
|
("block_0118", ["if condition goto block_0151;"]),
|
|
|
|
|
("block_011B", ["if other goto block_014D;", "suspend;"]),
|
|
|
|
|
("block_014D", ["goto block_0118;"]),
|
|
|
|
|
("block_0151", ["goto block_0200;"]),
|
|
|
|
|
("block_0200", ["return;"]),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
rendered = render_partially_structured_blocks(blocks)
|
|
|
|
|
|
|
|
|
|
text = "\n".join(rendered)
|
|
|
|
|
self.assertIn("entry:", text)
|
|
|
|
|
self.assertIn("for item in nearby_items(shape=0x04D0, origin=arg_06) {", text)
|
|
|
|
|
|
|
|
|
|
def test_target_event_reference_prefers_alias_and_class_name(self) -> None:
|
|
|
|
|
target = format_target_event_reference(
|
|
|
|
|
{
|
|
|
|
|
"target_class_id": 0x0A0C,
|
|
|
|
|
"target_class_name_hint": "FREE",
|
|
|
|
|
"target_event_slot": 0x32,
|
|
|
|
|
"target_event_name_hint": None,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(target, "FREE.waitNTimerTicks")
|
|
|
|
|
|
|
|
|
|
def test_selector_0x42_decodes_to_readable_fallback(self) -> None:
|
|
|
|
|
decoded = try_decode_loop_selector(
|
|
|
|
|
[
|
|
|
|
|
{"mnemonic": "loopscr", "operands": {"value_u8": 0x24}},
|
|
|
|
|
{"mnemonic": "push_word_immediate", "operands": {"value_u16": 0x04C8}},
|
|
|
|
|
{"mnemonic": "push_word_immediate", "operands": {"value_u16": 0x01CD}},
|
|
|
|
|
{"mnemonic": "loopscr", "operands": {"value_u8": 0x42}},
|
|
|
|
|
{"mnemonic": "push_byte_immediate", "operands": {"value_u8": 0x32, "value_signed": 50}},
|
|
|
|
|
{"mnemonic": "push_byte_immediate", "operands": {"value_u8": 0x20, "value_signed": 32}},
|
|
|
|
|
{"mnemonic": "mul", "operands": {}},
|
|
|
|
|
{"mnemonic": "push_local_word", "operands": {"bp_offset": 0x0A}},
|
|
|
|
|
{
|
|
|
|
|
"mnemonic": "loop",
|
|
|
|
|
"operands": {"current_var": 0xFE, "string_bytes": 0x6, "loop_type": 0x2},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
0,
|
|
|
|
|
{0xFE: "n", 0x0A: "eventTrigger"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
decoded,
|
|
|
|
|
(
|
|
|
|
|
"n in selector_0x42(arg0=0x04C8, arg1=0x01CD, arg2=(50 * 32), origin=eventTrigger)",
|
|
|
|
|
9,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-26 00:37:17 +01:00
|
|
|
def test_selector_ladder_renders_as_else_if_chain(self) -> None:
|
|
|
|
|
blocks = [
|
|
|
|
|
("entry", ["if (dir != 0) goto block_0358;"]),
|
|
|
|
|
("block_0339", ["x = 0;", "y = -1;", "goto block_0469;"]),
|
|
|
|
|
("block_0358", ["if (dir != 1) goto block_037F;"]),
|
|
|
|
|
("block_0360", ["x = 1;", "y = -1;", "goto block_0469;"]),
|
|
|
|
|
("block_037F", ["if (dir != 2) goto block_03A6;"]),
|
|
|
|
|
("block_0387", ["x = 1;", "y = 0;", "goto block_0469;"]),
|
|
|
|
|
("block_03A6", ["if (dir != 3) goto block_0469;"]),
|
|
|
|
|
("block_03AE", ["x = 1;", "y = 1;", "goto block_0469;"]),
|
|
|
|
|
("block_0469", ["return;"]),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
rendered = render_structured_pseudocode(blocks)
|
|
|
|
|
|
|
|
|
|
self.assertIsNotNone(rendered)
|
|
|
|
|
text = "\n".join(rendered or [])
|
|
|
|
|
self.assertIn("if (dir == 0) {", text)
|
|
|
|
|
self.assertIn("else if (dir == 1) {", text)
|
|
|
|
|
self.assertIn("else if (dir == 2) {", text)
|
|
|
|
|
self.assertIn("else if (dir == 3) {", text)
|
|
|
|
|
self.assertNotIn("goto block_0469;", text)
|
|
|
|
|
|
|
|
|
|
def test_intrinsic_overlay_prefers_crusader_specific_names(self) -> None:
|
|
|
|
|
regret_intrinsics = get_intrinsic_hints("regret")
|
|
|
|
|
|
|
|
|
|
self.assertEqual(intrinsic_display_name(regret_intrinsics.get(0x0013), 0x0013), "UCMachine.rndRange")
|
|
|
|
|
self.assertEqual(intrinsic_display_name(regret_intrinsics.get(0x0027), 0x0027), "SpriteProcess.createSprite")
|
|
|
|
|
|
|
|
|
|
def test_remorse_intrinsic_overlay_uses_local_table(self) -> None:
|
|
|
|
|
remorse_intrinsics = get_intrinsic_hints("remorse")
|
|
|
|
|
|
|
|
|
|
self.assertEqual(intrinsic_display_name(remorse_intrinsics.get(0x0018), 0x0018), "UCMachine.rndRange")
|
|
|
|
|
self.assertEqual(intrinsic_display_name(remorse_intrinsics.get(0x0015), 0x0015), "AudioProcess.playSFXCru")
|
|
|
|
|
|
|
|
|
|
def test_selector_ladder_renders_in_raw_fallback(self) -> None:
|
|
|
|
|
blocks = [
|
|
|
|
|
("entry", ["goto block_0331;"]),
|
|
|
|
|
("block_0331", ["if (dir != 0) goto block_0358;"]),
|
|
|
|
|
("block_0339", ["x = 0;", "y = -1;", "goto block_0469;"]),
|
|
|
|
|
("block_0358", ["if (dir != 1) goto block_037F;"]),
|
|
|
|
|
("block_0360", ["x = 1;", "y = -1;", "goto block_0469;"]),
|
|
|
|
|
("block_037F", ["if (dir != 2) goto block_0469;"]),
|
|
|
|
|
("block_0387", ["x = 1;", "y = 0;", "goto block_0469;"]),
|
|
|
|
|
("block_0469", ["return;"]),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
rendered = render_partially_structured_blocks(blocks)
|
|
|
|
|
|
|
|
|
|
text = "\n".join(rendered)
|
|
|
|
|
self.assertIn("block_0331:", text)
|
|
|
|
|
self.assertIn("if (dir == 0) {", text)
|
|
|
|
|
self.assertIn("else if (dir == 1) {", text)
|
|
|
|
|
self.assertIn("else if (dir == 2) {", text)
|
|
|
|
|
self.assertNotIn("block_0358:", text)
|
|
|
|
|
self.assertNotIn("goto block_0469;", text)
|
|
|
|
|
|
2026-03-25 23:32:36 +01:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|