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:
142
tools/re-capture/isl_dialogue.py
Normal file
142
tools/re-capture/isl_dialogue.py
Normal 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()
|
||||
Reference in New Issue
Block a user