Join VIMES and EIMES semantics
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user