Nothing here changes what a tool computes; it changes where tools look. - tools/re-capture: 33 censuses globbed /work/sylph_extract, a path that has existed nowhere since /work became a clone, so they matched nothing and printed empty results. They now resolve the disc through a new disc.py from $SYLPHEED_DISC and exit loudly without it (the #44 fix, generalised). Nine scripts that imported siblings from the retired Reborn checkout or an old session scratchpad now import from their own directory. unitgroup.py only needs the variable when --pak is not given. - sylpheed-xex: the loader only ever uses the XEX2 retail key. The dead devkit key and a doc comment claiming a devkit fallback that does not exist are gone; Project Sylpheed is a retail XEX2, so no XEX1 key either. - sylpheed-viewer: real_font_rasterizes looked for /tmp/sylph_extract and so always skipped. It reads $SYLPHEED_DISC now, and passes against the disc. - Comments and docs that named xenia-rs, the Reborn repository or /work/*.pe as places to look now name sylpheed.db, Canary's ppc_context.h and the flat .pe; docs/re/README.md no longer says the native Canary build does not run. Historical records keep their original paths: findings that were measured against /work/xenia-rs/sylpheed.db still say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
165 lines
5.1 KiB
Python
165 lines
5.1 KiB
Python
#!/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. 356 of the 1338 distinct
|
|
message names span more than one page; every page is printed.
|
|
"""
|
|
import collections
|
|
from disc import disc_root
|
|
import struct
|
|
import sys
|
|
|
|
import isl
|
|
|
|
NO_NAME = 0xFFFFFFFF
|
|
|
|
# A caption id is keyed `<id>_<page>_<line>`. A page is one subtitle box of up
|
|
# to four wrapped lines; successive pages are successive utterances. Measured
|
|
# maxima on the English pack: 8 pages, 4 lines.
|
|
MAX_PAGES = 16
|
|
MAX_LINES = 8
|
|
|
|
|
|
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 \
|
|
disc_root() + '/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]
|
|
pages = []
|
|
for page in range(MAX_PAGES):
|
|
box = [cap[k] for k in
|
|
('%s_%03d_%02d' % (name, page, i) for i in range(MAX_LINES))
|
|
if k in cap]
|
|
if not box:
|
|
break
|
|
pages.append(' '.join(box))
|
|
lines.append((off, name, pages))
|
|
|
|
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()
|
|
multi = sum(1 for _, _, p in lines if len(p) > 1)
|
|
print('%d message calls, %d with text, %d spanning more than one page.'
|
|
% (len(lines), sum(1 for _, _, p in lines if p), multi))
|
|
print()
|
|
print('A page is one subtitle box (measured maximum 4 wrapped lines).')
|
|
print('Successive pages are successive utterances and may be different')
|
|
print('speakers, so a multi-page id is a whole exchange, not one line.')
|
|
print()
|
|
seen = set()
|
|
for off, nm, pages in lines:
|
|
if off in seen:
|
|
continue
|
|
seen.add(off)
|
|
print('%06X %-22s %s' % (off, nm, pages[0] if pages else ''))
|
|
for extra in pages[1:]:
|
|
print('%6s %-22s %s' % ('', '', extra))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|