Usecode pseudocode
This commit is contained in:
parent
f92d1504fa
commit
c12bb39437
1362 changed files with 71072 additions and 38056 deletions
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
|
|
@ -16,6 +17,43 @@ CLASS_EVENT_INDEX = EXTRACTED_ROOT / "class_event_index.tsv"
|
|||
CLASS_LAYOUT_INDEX = EXTRACTED_ROOT / "class_layout_index.tsv"
|
||||
RUNTIME_VM_IR_INDEX = EXTRACTED_ROOT / "runtime_vm_ir.tsv"
|
||||
CHUNKS_DIR = EXTRACTED_ROOT / "chunks"
|
||||
UNKCOFFS_DIR = REPO_ROOT / "tools" / "unkcoffs"
|
||||
DEFAULT_GAME_VARIANT = "regret"
|
||||
INTRINSIC_HINT_PATHS = {
|
||||
"regret": UNKCOFFS_DIR / "regret_ints.py",
|
||||
"remorse": UNKCOFFS_DIR / "remorse_ints.py",
|
||||
}
|
||||
|
||||
|
||||
def resolve_extracted_root(extracted_root: Path | str | None = None) -> Path:
|
||||
if extracted_root is None:
|
||||
return EXTRACTED_ROOT
|
||||
return Path(extracted_root)
|
||||
|
||||
|
||||
def extracted_root_paths(extracted_root: Path | str | None = None) -> tuple[Path, Path, Path, Path]:
|
||||
root = resolve_extracted_root(extracted_root)
|
||||
return (
|
||||
root / "class_event_index.tsv",
|
||||
root / "class_layout_index.tsv",
|
||||
root / "runtime_vm_ir.tsv",
|
||||
root / "chunks",
|
||||
)
|
||||
|
||||
|
||||
def repo_relative_path(path: Path) -> str:
|
||||
try:
|
||||
return str(path.relative_to(REPO_ROOT)).replace("\\", "/")
|
||||
except ValueError:
|
||||
return str(path).replace("\\", "/")
|
||||
|
||||
|
||||
def infer_flex_path(extracted_root: Path | str | None = None) -> str:
|
||||
root = resolve_extracted_root(extracted_root)
|
||||
parent = root.parent
|
||||
if parent == REPO_ROOT:
|
||||
return "EUSECODE.FLX"
|
||||
return f"{repo_relative_path(parent)}/EUSECODE.FLX"
|
||||
|
||||
|
||||
EVENT_NAME_HINTS = {
|
||||
|
|
@ -57,7 +95,7 @@ EVENT_NAME_HINTS = {
|
|||
# Intrinsic table extracted from Pentagram ConvertUsecodeCrusader.h
|
||||
# Source note: "current discovered intrinsics are for regret1.21 only"
|
||||
# This is used as a hint only – ordinal mapping may differ between builds.
|
||||
INTRINSIC_HINTS: dict[int, str] = {
|
||||
BASE_INTRINSIC_HINTS: dict[int, str] = {
|
||||
0x0000: "Intrinsic0000()",
|
||||
0x0001: "Item::getFrame(void)",
|
||||
0x0002: "Item::setFrame(uint16)",
|
||||
|
|
@ -411,6 +449,117 @@ INTRINSIC_HINTS: dict[int, str] = {
|
|||
}
|
||||
|
||||
|
||||
VARIANT_INTRINSIC_CALLSITE_HINTS: dict[str, dict[tuple[int, int], str]] = {
|
||||
"regret": {
|
||||
(0x001E, 0x10): "Item::I_fireWeapon(Item *, x, y, z, byte, int, byte)",
|
||||
},
|
||||
"remorse": {},
|
||||
}
|
||||
|
||||
|
||||
def normalize_game_variant(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = value.strip().lower()
|
||||
if not normalized or normalized == "auto":
|
||||
return None
|
||||
if normalized not in INTRINSIC_HINT_PATHS:
|
||||
raise ValueError(f"Unsupported Crusader variant: {value}")
|
||||
return normalized
|
||||
|
||||
|
||||
def infer_game_variant_from_path(path: Path | None) -> str | None:
|
||||
if path is None:
|
||||
return None
|
||||
lowered_parts = [part.lower() for part in path.parts]
|
||||
if any("regret" in part for part in lowered_parts):
|
||||
return "regret"
|
||||
if any("remorse" in part for part in lowered_parts):
|
||||
return "remorse"
|
||||
return None
|
||||
|
||||
|
||||
def resolve_game_variant(game_variant: str | None = None, source_root: Path | None = None) -> str:
|
||||
normalized = normalize_game_variant(game_variant)
|
||||
if normalized is not None:
|
||||
return normalized
|
||||
inferred = infer_game_variant_from_path(source_root)
|
||||
if inferred is not None:
|
||||
return inferred
|
||||
return DEFAULT_GAME_VARIANT
|
||||
|
||||
|
||||
def load_intrinsic_hints_from_file(path: Path) -> dict[int, str]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
|
||||
try:
|
||||
module = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
except (OSError, SyntaxError):
|
||||
return {}
|
||||
|
||||
for node in module.body:
|
||||
if not isinstance(node, ast.Assign):
|
||||
continue
|
||||
if len(node.targets) != 1 or not isinstance(node.targets[0], ast.Name):
|
||||
continue
|
||||
if node.targets[0].id != "intrinsics":
|
||||
continue
|
||||
try:
|
||||
values = ast.literal_eval(node.value)
|
||||
except (SyntaxError, ValueError):
|
||||
return {}
|
||||
if not isinstance(values, list):
|
||||
return {}
|
||||
return {
|
||||
index: str(value)
|
||||
for index, value in enumerate(values)
|
||||
if isinstance(value, str) and value.strip()
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def normalize_intrinsic_hint(name: str) -> str:
|
||||
normalized = name.strip()
|
||||
normalized = re.sub(r"^(?:unsigned|signed|void|byte|char|short|long|int\d+|uint\d+|sint\d+)\s+(?=[A-Za-z_])", "", normalized)
|
||||
normalized = re.sub(r"(?<![A-Za-z])udioProcess::", "AudioProcess::", normalized)
|
||||
normalized = normalized.replace("MusicProcess:I_", "MusicProcess::I_")
|
||||
normalized = normalized.replace("Somehting", "Something")
|
||||
normalized = normalized.replace("Actor::I_setDead())", "Actor::I_setDead()")
|
||||
return normalized
|
||||
|
||||
|
||||
def build_intrinsic_hints(game_variant: str | None = None, source_root: Path | None = None) -> dict[int, str]:
|
||||
variant = resolve_game_variant(game_variant, source_root)
|
||||
hints = {index: normalize_intrinsic_hint(name) for index, name in BASE_INTRINSIC_HINTS.items()}
|
||||
for index, name in load_intrinsic_hints_from_file(INTRINSIC_HINT_PATHS[variant]).items():
|
||||
normalized = normalize_intrinsic_hint(name)
|
||||
existing = hints.get(index)
|
||||
if existing is None or not normalized.startswith("Intrinsic") or existing.startswith("Intrinsic"):
|
||||
hints[index] = normalized
|
||||
return hints
|
||||
|
||||
|
||||
_INTRINSIC_HINTS_CACHE: dict[str, dict[int, str]] = {}
|
||||
|
||||
|
||||
def get_intrinsic_hints(game_variant: str | None = None, source_root: Path | None = None) -> dict[int, str]:
|
||||
variant = resolve_game_variant(game_variant, source_root)
|
||||
cached = _INTRINSIC_HINTS_CACHE.get(variant)
|
||||
if cached is None:
|
||||
cached = build_intrinsic_hints(variant)
|
||||
_INTRINSIC_HINTS_CACHE[variant] = cached
|
||||
return cached
|
||||
|
||||
|
||||
def get_intrinsic_callsite_hints(game_variant: str | None = None, source_root: Path | None = None) -> dict[tuple[int, int], str]:
|
||||
variant = resolve_game_variant(game_variant, source_root)
|
||||
return VARIANT_INTRINSIC_CALLSITE_HINTS.get(variant, {})
|
||||
|
||||
|
||||
INTRINSIC_HINTS = get_intrinsic_hints(DEFAULT_GAME_VARIANT)
|
||||
|
||||
|
||||
NO_ARG_MNEMONICS = {
|
||||
0x08: "pop_result",
|
||||
0x12: "pop_temp",
|
||||
|
|
@ -587,11 +736,18 @@ def op_record(start: int, absolute_start: int, opcode: int, raw_bytes: bytes, mn
|
|||
}
|
||||
|
||||
|
||||
def parse_one_op(body: bytes, start: int) -> ParseResult:
|
||||
def parse_one_op(
|
||||
body: bytes,
|
||||
start: int,
|
||||
intrinsic_hints: dict[int, str] | None = None,
|
||||
intrinsic_callsite_hints: dict[tuple[int, int], str] | None = None,
|
||||
) -> ParseResult:
|
||||
reader = BodyReader(body, start)
|
||||
opcode = reader.read_u8()
|
||||
operands: dict[str, Any] = {}
|
||||
mnemonic = NO_ARG_MNEMONICS.get(opcode)
|
||||
active_intrinsic_hints = intrinsic_hints or INTRINSIC_HINTS
|
||||
active_callsite_hints = intrinsic_callsite_hints or get_intrinsic_callsite_hints(DEFAULT_GAME_VARIANT)
|
||||
|
||||
if opcode == 0x00:
|
||||
operands = {"bp_offset": reader.read_u8(), "target": bp_repr(body[start + 1])}
|
||||
|
|
@ -656,9 +812,9 @@ def parse_one_op(body: bytes, start: int) -> ParseResult:
|
|||
arg_bytes = reader.read_u8()
|
||||
intrinsic_ordinal = reader.read_u16()
|
||||
operands = {
|
||||
"arg_bytes": arg_bytes,
|
||||
"intrinsic_ordinal": intrinsic_ordinal,
|
||||
"intrinsic_name_hint": INTRINSIC_HINTS.get(intrinsic_ordinal),
|
||||
"arg_bytes": arg_bytes,
|
||||
"intrinsic_name_hint": active_callsite_hints.get((intrinsic_ordinal, arg_bytes), active_intrinsic_hints.get(intrinsic_ordinal)),
|
||||
}
|
||||
mnemonic = "call_intrinsic"
|
||||
elif opcode == 0x10:
|
||||
|
|
@ -842,18 +998,20 @@ def load_tsv_rows(path: Path) -> list[dict[str, str]]:
|
|||
return list(csv.DictReader(handle, delimiter="\t"))
|
||||
|
||||
|
||||
def find_chunk_file(entry_index: int) -> Path:
|
||||
matches = sorted(CHUNKS_DIR.glob(f"chunk_{entry_index:03d}_*.bin"))
|
||||
def find_chunk_file(entry_index: int, extracted_root: Path | str | None = None) -> Path:
|
||||
_, _, _, chunks_dir = extracted_root_paths(extracted_root)
|
||||
matches = sorted(chunks_dir.glob(f"chunk_{entry_index:03d}_*.bin"))
|
||||
if not matches:
|
||||
matches = sorted(CHUNKS_DIR.glob(f"chunk_{entry_index}_*.bin"))
|
||||
matches = sorted(chunks_dir.glob(f"chunk_{entry_index}_*.bin"))
|
||||
if not matches:
|
||||
raise FileNotFoundError(f"No chunk file found for entry_index={entry_index}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def select_rows(class_name: str, slot: int) -> tuple[dict[str, str], dict[str, str]]:
|
||||
event_rows = load_tsv_rows(CLASS_EVENT_INDEX)
|
||||
layout_rows = load_tsv_rows(CLASS_LAYOUT_INDEX)
|
||||
def select_rows(class_name: str, slot: int, extracted_root: Path | str | None = None) -> tuple[dict[str, str], dict[str, str]]:
|
||||
class_event_index, class_layout_index, _, _ = extracted_root_paths(extracted_root)
|
||||
event_rows = load_tsv_rows(class_event_index)
|
||||
layout_rows = load_tsv_rows(class_layout_index)
|
||||
|
||||
event_row = next(
|
||||
(
|
||||
|
|
@ -879,14 +1037,15 @@ def select_rows(class_name: str, slot: int) -> tuple[dict[str, str], dict[str, s
|
|||
return event_row, layout_row
|
||||
|
||||
|
||||
def load_runtime_ir_rows() -> list[dict[str, str]]:
|
||||
return load_tsv_rows(RUNTIME_VM_IR_INDEX)
|
||||
def load_runtime_ir_rows(extracted_root: Path | str | None = None) -> list[dict[str, str]]:
|
||||
_, _, runtime_vm_ir_index, _ = extracted_root_paths(extracted_root)
|
||||
return load_tsv_rows(runtime_vm_ir_index)
|
||||
|
||||
|
||||
def runtime_stage_hints(ops: list[dict[str, Any]]) -> list[dict[str, str]]:
|
||||
def runtime_stage_hints(ops: list[dict[str, Any]], extracted_root: Path | str | None = None) -> list[dict[str, str]]:
|
||||
opcode_values = {op["opcode"] for op in ops}
|
||||
hints: list[dict[str, str]] = []
|
||||
for row in load_runtime_ir_rows():
|
||||
for row in load_runtime_ir_rows(extracted_root):
|
||||
opcode_or_lane = row.get("opcode_or_lane", "")
|
||||
if opcode_or_lane.lower().startswith("opcode 0x"):
|
||||
opcode_value = try_parse_int(opcode_or_lane.split()[1])
|
||||
|
|
@ -898,7 +1057,7 @@ def runtime_stage_hints(ops: list[dict[str, Any]]) -> list[dict[str, str]]:
|
|||
return hints
|
||||
|
||||
|
||||
def annotation_hints(event_row: dict[str, str], payload_shape_hint: str, ops: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
def annotation_hints(event_row: dict[str, str], payload_shape_hint: str, ops: list[dict[str, Any]], extracted_root: Path | str | None = None) -> dict[str, Any]:
|
||||
slot = parse_int(event_row["slot"])
|
||||
return {
|
||||
"runtime_family": "slot-backed-owner-loaded-body",
|
||||
|
|
@ -914,7 +1073,7 @@ def annotation_hints(event_row: dict[str, str], payload_shape_hint: str, ops: li
|
|||
{"address": "000d:2104", "role": "finalize_to_outptr"},
|
||||
{"address": "000d:ebe3", "role": "opcode_sequence_run"},
|
||||
],
|
||||
"runtime_stage_hints": runtime_stage_hints(ops),
|
||||
"runtime_stage_hints": runtime_stage_hints(ops, extracted_root),
|
||||
"slot_taxonomy": {"slot": slot, "event_name_hint": event_row["event_name_hint"] or EVENT_NAME_HINTS.get(slot)},
|
||||
}
|
||||
|
||||
|
|
@ -1002,10 +1161,19 @@ def parse_field_tags(body: bytes, start: int) -> FieldTagParseResult | None:
|
|||
return FieldTagParseResult(field_tags=field_tags, end_offset=end_offset, trailing_bytes=body[end_offset:])
|
||||
|
||||
|
||||
def parse_body_ir(event_row: dict[str, str], layout_row: dict[str, str]) -> dict[str, Any]:
|
||||
def parse_body_ir(
|
||||
event_row: dict[str, str],
|
||||
layout_row: dict[str, str],
|
||||
game_variant: str | None = None,
|
||||
extracted_root: Path | str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
resolved_extracted_root = resolve_extracted_root(extracted_root)
|
||||
entry_index = parse_int(event_row["entry_index"])
|
||||
chunk_file = find_chunk_file(entry_index)
|
||||
chunk_file = find_chunk_file(entry_index, resolved_extracted_root)
|
||||
chunk_bytes = chunk_file.read_bytes()
|
||||
resolved_game_variant = resolve_game_variant(game_variant, chunk_file)
|
||||
intrinsic_hints = get_intrinsic_hints(resolved_game_variant, chunk_file)
|
||||
intrinsic_callsite_hints = get_intrinsic_callsite_hints(resolved_game_variant, chunk_file)
|
||||
|
||||
body_start = parse_int(event_row["derived_body_start"])
|
||||
body_end = parse_int(event_row["derived_body_end"])
|
||||
|
|
@ -1020,7 +1188,7 @@ def parse_body_ir(event_row: dict[str, str], layout_row: dict[str, str]) -> dict
|
|||
field_tags: list[dict[str, Any]] = []
|
||||
|
||||
while offset < len(body):
|
||||
result = parse_one_op(body, offset)
|
||||
result = parse_one_op(body, offset, intrinsic_hints, intrinsic_callsite_hints)
|
||||
if result.op is not None:
|
||||
result.op["absolute_body_offset"] = body_start + result.op["offset"]
|
||||
ops.append(result.op)
|
||||
|
|
@ -1121,9 +1289,10 @@ def parse_body_ir(event_row: dict[str, str], layout_row: dict[str, str]) -> dict
|
|||
return {
|
||||
"schema_version": "crusader-usecode-ir-v1-poc",
|
||||
"source": {
|
||||
"flex_path": "USECODE/EUSECODE.FLX",
|
||||
"extracted_root": "USECODE/EUSECODE_extracted",
|
||||
"chunk_file": str(chunk_file.relative_to(REPO_ROOT)).replace("\\", "/"),
|
||||
"game_variant": resolved_game_variant,
|
||||
"flex_path": infer_flex_path(resolved_extracted_root),
|
||||
"extracted_root": repo_relative_path(resolved_extracted_root),
|
||||
"chunk_file": repo_relative_path(chunk_file),
|
||||
},
|
||||
"class": {
|
||||
"entry_index": entry_index,
|
||||
|
|
@ -1156,7 +1325,7 @@ def parse_body_ir(event_row: dict[str, str], layout_row: dict[str, str]) -> dict
|
|||
"ops": ops,
|
||||
"debug_symbols": debug_symbols,
|
||||
"field_tags": field_tags,
|
||||
"annotation_hints": annotation_hints(event_row, payload_shape, ops),
|
||||
"annotation_hints": annotation_hints(event_row, payload_shape, ops, resolved_extracted_root),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1181,7 +1350,7 @@ def _common_suffix_len(a: bytes, b: bytes, prefix_len: int) -> int:
|
|||
return limit
|
||||
|
||||
|
||||
def compute_family_diff(class_name: str, slot: int) -> dict[str, Any]:
|
||||
def compute_family_diff(class_name: str, slot: int, extracted_root: Path | str | None = None) -> dict[str, Any]:
|
||||
"""
|
||||
Find all event rows that share the same repeated_template_status family tag
|
||||
as the named class/slot row, then decode each body and compute pairwise diff
|
||||
|
|
@ -1193,8 +1362,9 @@ def compute_family_diff(class_name: str, slot: int) -> dict[str, Any]:
|
|||
sibling_count – number of additional rows in the same family
|
||||
members – list of per-member records (entry, class, body stats, diff vs ref)
|
||||
"""
|
||||
event_rows = load_tsv_rows(CLASS_EVENT_INDEX)
|
||||
layout_rows = load_tsv_rows(CLASS_LAYOUT_INDEX)
|
||||
class_event_index, class_layout_index, _, _ = extracted_root_paths(extracted_root)
|
||||
event_rows = load_tsv_rows(class_event_index)
|
||||
layout_rows = load_tsv_rows(class_layout_index)
|
||||
layout_by_entry: dict[int, dict[str, str]] = {}
|
||||
for row in layout_rows:
|
||||
idx = try_parse_int(row.get("entry_index", ""))
|
||||
|
|
@ -1239,7 +1409,7 @@ def compute_family_diff(class_name: str, slot: int) -> dict[str, Any]:
|
|||
if not body_start_str or not body_end_str:
|
||||
return None
|
||||
try:
|
||||
chunk = find_chunk_file(parse_int(row["entry_index"]))
|
||||
chunk = find_chunk_file(parse_int(row["entry_index"]), extracted_root)
|
||||
data = chunk.read_bytes()
|
||||
return data[parse_int(body_start_str):parse_int(body_end_str)]
|
||||
except (FileNotFoundError, ValueError):
|
||||
|
|
@ -1551,6 +1721,9 @@ def intrinsic_display_name(name_hint: str | None, ordinal: int) -> str:
|
|||
if not name_hint:
|
||||
return f"intrinsic_{ordinal:04X}"
|
||||
display = name_hint.replace("::", ".")
|
||||
display = re.sub(r"(?<=\.)I_", "", display)
|
||||
if display.startswith("I_"):
|
||||
display = display[2:]
|
||||
paren = display.find("(")
|
||||
if paren != -1:
|
||||
display = display[:paren]
|
||||
|
|
@ -1962,13 +2135,123 @@ def detect_noop_compare_chain(
|
|||
return None
|
||||
|
||||
|
||||
def last_nonempty_block_index(
|
||||
blocks: list[tuple[str, list[str]]],
|
||||
start_index: int,
|
||||
end_index: int,
|
||||
) -> int | None:
|
||||
for index in range(end_index - 1, start_index - 1, -1):
|
||||
if blocks[index][1]:
|
||||
return index
|
||||
return None
|
||||
|
||||
|
||||
def parse_selector_condition(condition: str) -> tuple[str, str] | None:
|
||||
expr = strip_outer_parens(condition)
|
||||
match = re.fullmatch(r"(.+?)\s*!=\s*(.+)", expr)
|
||||
if match is None:
|
||||
return None
|
||||
return match.group(1).strip(), match.group(2).strip()
|
||||
|
||||
|
||||
def render_selector_chain(
|
||||
blocks: list[tuple[str, list[str]]],
|
||||
label_to_index: dict[str, int],
|
||||
start_index: int,
|
||||
end_index: int,
|
||||
return_labels: set[str],
|
||||
) -> tuple[list[str], int] | None:
|
||||
if not blocks[start_index][1]:
|
||||
return None
|
||||
base_terminal = parse_terminal_statement(blocks[start_index][1][-1])
|
||||
if base_terminal is None or base_terminal.kind != "if":
|
||||
return None
|
||||
|
||||
selector = parse_selector_condition(base_terminal.condition or "")
|
||||
if selector is None:
|
||||
return None
|
||||
selector_expr, _ = selector
|
||||
|
||||
cursor = start_index
|
||||
join_label: str | None = None
|
||||
branches: list[tuple[str, list[str]]] = []
|
||||
|
||||
while cursor < end_index:
|
||||
_, statements = blocks[cursor]
|
||||
if not statements:
|
||||
return None
|
||||
terminal = parse_terminal_statement(statements[-1])
|
||||
if terminal is None or terminal.kind != "if":
|
||||
return None
|
||||
|
||||
parsed = parse_selector_condition(terminal.condition or "")
|
||||
if parsed is None or parsed[0] != selector_expr:
|
||||
return None
|
||||
|
||||
target_label = terminal.target or ""
|
||||
target_index = label_to_index.get(target_label)
|
||||
if target_index is None or target_index <= cursor + 1 or target_index > end_index:
|
||||
return None
|
||||
|
||||
body_tail_index = last_nonempty_block_index(blocks, cursor + 1, target_index)
|
||||
if body_tail_index is None:
|
||||
return None
|
||||
body_tail_terminal = parse_terminal_statement(blocks[body_tail_index][1][-1])
|
||||
if body_tail_terminal is None or body_tail_terminal.kind != "goto":
|
||||
return None
|
||||
|
||||
current_join = body_tail_terminal.target or ""
|
||||
current_join_index = label_to_index.get(current_join)
|
||||
if current_join_index is None or current_join_index > end_index:
|
||||
return None
|
||||
if current_join_index < target_index:
|
||||
return None
|
||||
if current_join_index == target_index and target_label != current_join:
|
||||
return None
|
||||
if join_label is None:
|
||||
join_label = current_join
|
||||
elif current_join != join_label:
|
||||
return None
|
||||
|
||||
body_result = render_structured_region(
|
||||
blocks,
|
||||
label_to_index,
|
||||
cursor + 1,
|
||||
target_index,
|
||||
return_labels,
|
||||
{join_label},
|
||||
)
|
||||
if body_result is None:
|
||||
return None
|
||||
body_lines, _ = body_result
|
||||
branches.append((invert_condition_text(terminal.condition or "condition"), body_lines))
|
||||
|
||||
if target_label == join_label:
|
||||
break
|
||||
cursor = target_index
|
||||
|
||||
if join_label is None:
|
||||
return None
|
||||
|
||||
rendered: list[str] = []
|
||||
for index, (condition, body_lines) in enumerate(branches):
|
||||
branch_head = "if" if index == 0 else "else if"
|
||||
rendered.append(f"{branch_head} ({condition}) {{")
|
||||
rendered.extend(indent_lines(body_lines))
|
||||
rendered.append("}")
|
||||
|
||||
return rendered, label_to_index[join_label]
|
||||
|
||||
|
||||
def render_structured_region(
|
||||
blocks: list[tuple[str, list[str]]],
|
||||
label_to_index: dict[str, int],
|
||||
start_index: int,
|
||||
end_index: int,
|
||||
return_labels: set[str],
|
||||
exit_labels: set[str] | None = None,
|
||||
) -> tuple[list[str], bool] | None:
|
||||
allowed_exit_labels = set(exit_labels or ())
|
||||
lines: list[str] = []
|
||||
index = start_index
|
||||
|
||||
|
|
@ -2001,6 +2284,8 @@ def render_structured_region(
|
|||
if target_label in return_labels:
|
||||
lines.append("return;")
|
||||
return lines, False
|
||||
if target_label in allowed_exit_labels:
|
||||
return lines, False
|
||||
if target_index is None:
|
||||
return None
|
||||
if target_index == index + 1:
|
||||
|
|
@ -2019,6 +2304,74 @@ def render_structured_region(
|
|||
index += 1
|
||||
continue
|
||||
|
||||
selector_chain = render_selector_chain(blocks, label_to_index, index, end_index, return_labels)
|
||||
if selector_chain is not None:
|
||||
selector_lines, selector_join_index = selector_chain
|
||||
lines.extend(selector_lines)
|
||||
index = selector_join_index
|
||||
continue
|
||||
|
||||
if target_index <= end_index:
|
||||
loop_tail_index = last_nonempty_block_index(blocks, index + 1, target_index)
|
||||
if loop_tail_index is not None:
|
||||
loop_tail_terminal = parse_terminal_statement(blocks[loop_tail_index][1][-1])
|
||||
if loop_tail_terminal is not None and loop_tail_terminal.kind == "goto" and loop_tail_terminal.target == blocks[index][0]:
|
||||
loop_body = render_structured_region(
|
||||
blocks,
|
||||
label_to_index,
|
||||
index + 1,
|
||||
target_index,
|
||||
return_labels,
|
||||
{blocks[index][0]},
|
||||
)
|
||||
if loop_body is not None:
|
||||
loop_lines, _ = loop_body
|
||||
lines.append(f"while ({invert_condition_text(terminal.condition or 'condition')}) {{")
|
||||
lines.extend(indent_lines(loop_lines))
|
||||
lines.append("}")
|
||||
index = target_index
|
||||
continue
|
||||
|
||||
true_tail_index = last_nonempty_block_index(blocks, index + 1, target_index)
|
||||
if true_tail_index is not None:
|
||||
true_tail_terminal = parse_terminal_statement(blocks[true_tail_index][1][-1])
|
||||
if true_tail_terminal is not None and true_tail_terminal.kind == "goto":
|
||||
join_label = true_tail_terminal.target or ""
|
||||
join_index = label_to_index.get(join_label)
|
||||
if join_index is not None and join_index > target_index and join_index <= end_index:
|
||||
true_result = render_structured_region(
|
||||
blocks,
|
||||
label_to_index,
|
||||
index + 1,
|
||||
target_index,
|
||||
return_labels,
|
||||
{join_label},
|
||||
)
|
||||
false_result = render_structured_region(
|
||||
blocks,
|
||||
label_to_index,
|
||||
target_index,
|
||||
join_index,
|
||||
return_labels,
|
||||
{join_label},
|
||||
)
|
||||
if true_result is not None and false_result is not None:
|
||||
true_lines, _ = true_result
|
||||
false_lines, _ = false_result
|
||||
lines.append(f"if ({invert_condition_text(terminal.condition or 'condition')}) {{")
|
||||
lines.extend(indent_lines(true_lines))
|
||||
lines.append("}")
|
||||
if false_lines:
|
||||
if false_lines[0].startswith("if "):
|
||||
lines.append(f"else {false_lines[0]}")
|
||||
lines.extend(false_lines[1:])
|
||||
else:
|
||||
lines.append("else {")
|
||||
lines.extend(indent_lines(false_lines))
|
||||
lines.append("}")
|
||||
index = join_index
|
||||
continue
|
||||
|
||||
inner_result = render_structured_region(blocks, label_to_index, index + 1, target_index, return_labels)
|
||||
if inner_result is None:
|
||||
return None
|
||||
|
|
@ -2053,6 +2406,40 @@ def render_structured_pseudocode(blocks: list[tuple[str, list[str]]]) -> list[st
|
|||
return structured[0]
|
||||
|
||||
|
||||
def render_partially_structured_blocks(blocks: list[tuple[str, list[str]]]) -> list[str]:
|
||||
if not blocks:
|
||||
return []
|
||||
|
||||
label_to_index = {label: index for index, (label, _) in enumerate(blocks)}
|
||||
return_labels = {
|
||||
label
|
||||
for label, statements in blocks
|
||||
if len(statements) == 1 and statements[0] == "return;"
|
||||
}
|
||||
|
||||
lines: list[str] = []
|
||||
index = 0
|
||||
while index < len(blocks):
|
||||
label, statements = blocks[index]
|
||||
selector_chain = render_selector_chain(blocks, label_to_index, index, len(blocks), return_labels)
|
||||
if selector_chain is not None:
|
||||
selector_lines, selector_join_index = selector_chain
|
||||
lines.append(f" {label}:")
|
||||
for statement in selector_lines:
|
||||
lines.append(f" {statement}" if statement else "")
|
||||
lines.append("")
|
||||
index = selector_join_index
|
||||
continue
|
||||
|
||||
lines.append(f" {label}:")
|
||||
for statement in statements:
|
||||
lines.append(f" {statement}")
|
||||
lines.append("")
|
||||
index += 1
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def render_pseudocode(ir: dict[str, Any]) -> str:
|
||||
slot_name = sanitize_identifier(ir["event"]["event_name_hint"] or f"slot_{ir['event']['slot']:02X}")
|
||||
lines = [
|
||||
|
|
@ -2076,11 +2463,7 @@ def render_pseudocode(ir: dict[str, Any]) -> str:
|
|||
for statement in structured_lines:
|
||||
lines.append(f" {statement}" if statement else "")
|
||||
else:
|
||||
for label, statements in rendered_blocks:
|
||||
lines.append(f" {label}:")
|
||||
for statement in statements:
|
||||
lines.append(f" {statement}")
|
||||
lines.append("")
|
||||
lines.extend(render_partially_structured_blocks(rendered_blocks))
|
||||
|
||||
lines.append("}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
|
@ -2140,6 +2523,8 @@ def main() -> None:
|
|||
parser = argparse.ArgumentParser(description="Proof-of-concept Crusader USECODE parser over extracted owner-loaded artifacts")
|
||||
parser.add_argument("--class", dest="class_name", required=True, help="Class name from class_event_index.tsv, for example NPCTRIG")
|
||||
parser.add_argument("--slot", required=True, help="Event slot, for example 0x0A")
|
||||
parser.add_argument("--extracted-root", default=str(EXTRACTED_ROOT), help="Extracted USECODE root containing class_event_index.tsv and chunks/")
|
||||
parser.add_argument("--variant", choices=["auto", "regret", "remorse"], default="auto", help="Crusader intrinsic numbering to apply (default: auto, fallback regret)")
|
||||
parser.add_argument("--output", help="Write IR JSON to this file instead of stdout")
|
||||
parser.add_argument("--emit-text", action="store_true", help="Emit a readable text listing beside the JSON")
|
||||
parser.add_argument("--text-output", help="Write the text listing to this file")
|
||||
|
|
@ -2153,8 +2538,9 @@ def main() -> None:
|
|||
args = parser.parse_args()
|
||||
|
||||
slot = parse_int(args.slot)
|
||||
event_row, layout_row = select_rows(args.class_name, slot)
|
||||
ir = parse_body_ir(event_row, layout_row)
|
||||
extracted_root = Path(args.extracted_root)
|
||||
event_row, layout_row = select_rows(args.class_name, slot, extracted_root)
|
||||
ir = parse_body_ir(event_row, layout_row, None if args.variant == "auto" else args.variant, extracted_root)
|
||||
|
||||
rendered_json = json.dumps(ir, indent=2)
|
||||
if args.output:
|
||||
|
|
@ -2184,7 +2570,7 @@ def main() -> None:
|
|||
print(rendered_pseudocode)
|
||||
|
||||
if args.family_diff:
|
||||
diff = compute_family_diff(args.class_name, slot)
|
||||
diff = compute_family_diff(args.class_name, slot, extracted_root)
|
||||
diff_json = json.dumps(diff, indent=2)
|
||||
if args.family_diff_output:
|
||||
Path(args.family_diff_output).write_text(diff_json + "\n", encoding="utf-8")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue