Join VIMES and EIMES semantics

This commit is contained in:
gamer147
2026-07-23 17:39:08 -04:00
parent 64f9cebd3b
commit 15a5199ce7
13 changed files with 416 additions and 67 deletions

View File

@@ -1,21 +1,23 @@
#!/usr/bin/env python3
"""Extract an ID-dispatched SYS4 message table.
ITMES and SKMES are long chains of:
The shipped tables use two equivalent control-flow shapes:
eq <temporary>, <selected id global>, <record id>
jcc ...
show-text ...
...
jcc <next guard>
<message body>
jmp <shared exit>
This tool discovers the selector global from that repeated shape, reconstructs
the displayed lines (including the surface text of furigana spans), and emits a
reusable ID-to-message JSON table.
or a compact guard block whose successful branches target message bodies stored
later in the script. This tool discovers the selector global, follows either
layout, reconstructs the displayed lines (including the surface text of
furigana spans), and emits a reusable ID-to-message JSON table.
Usage:
py -3.11 -X utf8 tools/extract_message_table.py ITMES
py -3.11 -X utf8 tools/extract_message_table.py SKMES [OUTNAME]
py -3.11 -X utf8 tools/extract_message_table.py VIMES
py -3.11 -X utf8 tools/extract_message_table.py EIMES
"""
from __future__ import annotations
@@ -33,6 +35,11 @@ import sys4load
T_IMM = 0
T_INLINE_STRING = 2
T_GLOBAL_INT = 3
BRANCH_SENTINEL = 0xFFFFFFFF
MESSAGE_LAYOUTS = {
"EIMES.BIN": "enemy-commentary",
}
def resolve(name: str) -> Path:
@@ -77,7 +84,9 @@ def _strings(ins, scr) -> list[str]:
]
def _message_body(scr, start: int, stop: int) -> dict | None:
def _message_body(
scr, start: int, stop: int, layout: str = "title-description"
) -> dict | None:
fragments: list[str] = []
lines: list[str] = []
furigana: list[dict] = []
@@ -102,15 +111,34 @@ def _message_body(scr, start: int, stop: int) -> dict | None:
lines.append("".join(fragments))
if not lines:
return None
message = {
"title": lines[0],
"description": "\n".join(lines[1:]),
}
if layout == "enemy-commentary":
message = {
"summary": lines[0],
"strategy": "\n".join(lines[1:]),
}
else:
message = {
"title": lines[0],
"description": "\n".join(lines[1:]),
}
if furigana:
message["furigana"] = furigana
return message
def _branch_target_index(scr, guard_index: int, offsets: dict[int, int]) -> int | None:
"""Resolve the conditional branch immediately following a dispatch guard."""
if guard_index + 1 >= len(scr.instructions):
return None
branch = scr.instructions[guard_index + 1]
if sys4load.display_label(branch.opcode) != "jcc":
return None
for arg_type, value in branch.args[1:]:
if arg_type == T_IMM and value != BRANCH_SENTINEL and value in offsets:
return offsets[value]
return None
def extract_messages(scr, selector: int | None = None) -> tuple[list[dict], dict]:
"""Extract ordered message records and dispatch metadata from a script."""
discovered_selector, comparison_count = discover_selector(scr)
@@ -122,24 +150,49 @@ def extract_messages(scr, selector: int | None = None) -> tuple[list[dict], dict
]
records: list[dict] = []
seen: set[int] = set()
offsets = {
instruction.offset: index
for index, instruction in enumerate(scr.instructions)
}
message_layout = MESSAGE_LAYOUTS.get(scr.path.name.upper(), "title-description")
dispatch_layout_counts = collections.Counter()
for guard_index, (instruction_index, record_id) in enumerate(guards):
stop = guards[guard_index + 1][0] if guard_index + 1 < len(guards) else len(scr.instructions)
message = _message_body(scr, instruction_index + 1, stop)
message_start = instruction_index + 2
message = _message_body(scr, message_start, stop, message_layout)
dispatch_layout = "fallthrough"
if message is None:
branch_target = _branch_target_index(scr, instruction_index, offsets)
if branch_target is not None:
message_start = branch_target
message = _message_body(
scr, message_start, len(scr.instructions), message_layout
)
dispatch_layout = "branch-target"
if message is None:
continue
if record_id in seen:
raise ValueError(f"{scr.path.name}: duplicate message id {record_id}")
seen.add(record_id)
dispatch_layout_counts[dispatch_layout] += 1
records.append({
"id": record_id,
"dispatch_offset": f"0x{scr.instructions[instruction_index].offset:x}",
"message_offset": f"0x{scr.instructions[message_start].offset:x}",
**message,
})
dispatch_layout = (
next(iter(dispatch_layout_counts))
if len(dispatch_layout_counts) == 1
else "mixed"
)
return records, {
"selector_global": f"0x{selector:x}",
"dispatch_guard_count": len(guards),
"message_count": len(records),
"selector_discovery_count": comparison_count,
"dispatch_layout": dispatch_layout,
"message_layout": message_layout,
}