#!/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 [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 `__`. 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 import struct import sys import isl NO_NAME = 0xFFFFFFFF # A caption id is keyed `__`. 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 \ '/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] 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()