Usecode pseudocode

This commit is contained in:
MaddoScientisto 2026-03-26 00:37:17 +01:00
commit c12bb39437
1362 changed files with 71072 additions and 38056 deletions

View file

@ -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")