re: mission scripts are readable as dialogue (2683/2683 message calls resolve)

Built-in 64's slot-0 operand is a symbol-table-1 type-6 message id, and every
one of them now has caption text: 2683 of 2683 call sites across the 28 stage
scripts, 1338 distinct names, no residue of any kind.

This only became reachable once build_caption_text was switched to the IXUD
field table (537 -> 8800 lines); before that most of these names had nothing
to resolve to.

Adds isl_dialogue.py plus a committed Stage 02 sample. Does not settle which
recording plays for a given line, multi-page captions, or the other five
languages.
This commit is contained in:
Sylpheed RE agent
2026-08-26 03:09:10 +00:00
parent 253b960f56
commit 7c4595cbf9
3 changed files with 432 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Render a mission script's dialogue: every `request_script_message` in order,
with the caption text it plays.
isl_dialogue.py <StageNN.ssb> [GP_MAIN_GAME_E.pak]
Built-in 64 takes a symbol-table-1 (type 6) message name; the caption text lives
in the pack's IXUD blocks under `<name>_<page>_<line>`. All 2683 call sites
across the 28 stages resolve, so this is a total mapping rather than a sample.
"""
import collections
import struct
import sys
import isl
NO_NAME = 0xFFFFFFFF
def be32(b, o):
return struct.unpack_from('>I', b, o)[0]
def wstr(b, pool, off):
base = pool + 2 * off
e = base
while b[e:e + 2] != b'\0\0':
e += 2
return ''.join(chr(c) for c in struct.unpack('>%dH' % ((e - base) // 2), b[base:e]))
def captions(pak_entries):
"""`MSG_*` key -> text, read from the IXUD record/field table."""
out = {}
for b in pak_entries.values():
if b[:4] != b'IXUD':
continue
n = be32(b, 4)
o = 8 + 16 * n
m = be32(b, o)
o += 4
fields = [struct.unpack_from('>III', b, o + 12 * i) for i in range(m)]
o += 12 * m
pool = o + 4
for (_k, no, vo) in fields:
if no == NO_NAME:
continue
try:
key, val = wstr(b, pool, no), wstr(b, pool, vo)
except Exception:
continue
if key.startswith('MSG_') and val.strip():
out.setdefault(key, val)
return out
def walk(b, base, nxt, sink):
staged, pending, off = {}, None, base
while off + 4 <= nxt:
w = be32(b, off)
op, ln = w & 0xFF, (w >> 8) & 0xFF
k1, k0 = (w >> 24) & 0xFF, (w >> 16) & 0xFF
if ln < 4 or ln % 4:
return
ws = [be32(b, off + i) for i in range(4, max(ln, 4), 4) if off + i + 4 <= len(b)]
if op in (0, 1) and len(ws) >= 2:
if k0 == 2 and k1 == 1:
pending = ws[1]
elif k0 == 3 and k1 == 2 and pending is not None:
staged[ws[0]] = pending
elif k0 == 3 and k1 == 1:
staged[ws[0]] = ws[1]
if op == 19 and ws:
sink(off, ws[0], dict(staged))
staged = {}
off += ln
def main():
ssb = sys.argv[1]
pak = sys.argv[2] if len(sys.argv) > 2 else \
'/work/sylph_extract/dat/GP_MAIN_GAME_E.pak'
sys.path.insert(0, sys.path[0])
from unitgroup import read_entry # noqa: F401 (kept for parity)
import zlib
import os
def pak_entries(path):
d = open(path, 'rb').read()
assert d[:4] == b'IPFB'
segs, base, i = b'', path[:-4], 0
while os.path.exists('%s.p%02d' % (base, i)):
segs += open('%s.p%02d' % (base, i), 'rb').read()
i += 1
out = {}
for k in range(be32(d, 4)):
h, off, cs = struct.unpack_from('>III', d, 0x10 + 12 * k)
raw = segs[off:off + cs]
if raw[:2] == b'Z1':
raw = zlib.decompress(raw[10:])
out[h] = raw
return out
cap = captions(pak_entries(pak))
b = isl.load(ssb)
s1 = isl.symbols(b, 1)
end = be32(b, 0x0C)
lines = []
def sink(off, bid, st):
if bid != 64:
return
v = st.get(0)
if not isinstance(v, int) or v not in s1:
return
name = s1[v][1]
text = [cap[k] for k in
('%s_000_%02d' % (name, i) for i in range(4)) if k in cap]
lines.append((off, name, ' '.join(text)))
bases = isl.phase_bases(b) + [end]
for i in range(len(bases) - 1):
walk(b, bases[i], bases[i + 1], sink)
name = ssb.replace('\\', '/').split('/')[-1]
print('# %s — dialogue, in script order' % name)
print()
print('Generated by `tools/re-capture/isl_dialogue.py`. Each line is one')
print('`request_script_message` (built-in 64) and the caption it plays.')
print()
print('%d message calls, %d with text.' % (len(lines), sum(1 for _, _, t in lines if t)))
print()
seen = set()
for off, nm, text in lines:
if off in seen:
continue
seen.add(off)
print('%06X %-22s %s' % (off, nm, text))
if __name__ == '__main__':
main()