Files
Sylpheed/docs/re/disc-atlas.html
Sylpheed RE agent 452625d98d docs/re: the disc atlas -- one map of how the assets reference each other
A reference page covering the four layers (disc media, IPFB archives, container
formats, domain chains) and the three hashes that join them: name_hash for
archive TOC keys (case-insensitive), tag_hash for IDXD records and fields
(case-sensitive), ixud_hash for localised text.

Counts were measured on the retail extract for this page rather than copied
forward from the corpus -- 41 archives, 26443 entries, 166 xpr, 97 wmv -- and
where they overlap with docs/re they reproduce it independently (104 cutscene
slots / 101 movies / 99 / 99 / 22).

It gives the dangling references their own section, because they are properties
of the shipped disc and a port has to survive them: pmbase.t32, SUBTITLE_S12B,
the four boot logos, GP_TEST, the six 2D paks at exactly 0% named, and the 42
provably unrecoverable ISL symbol keys.

Also published as an artifact for reading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:51:45 +02:00

922 lines
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<title>Sylpheed Disc Atlas</title>
<style>
/* ───────────────────────────────────────────────────────────────────────────
Palette. Three of these hex values are literal constants in the codebase:
--ground is ComposeOptions::default().backdrop = [14,14,20], the colour the
UI compositor paints behind every reassembled screen; --warn and --refuted
are the viewer's own egui warning/error colours, rgb(224,168,86) and
rgb(224,86,122). --confirm is the cyan glow measured off the intro video.
Neutrals are biased blue to sit with the ground rather than against it.
─────────────────────────────────────────────────────────────────────────── */
:root {
--ground: #0e0e14;
--confirm: #3fb6c6;
--warn: #b8792c;
--refuted: #c23f61;
--accent: #c23f61;
--bg: #eceef3;
--surface: #f6f7fa;
--surface-2: #e2e5ee;
--text: #16171f;
--text-muted: #5c6076;
--text-faint: #878ca3;
--rule: #c9cddb;
--rule-soft: #dbdfe9;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--confirm: #4fd0e0;
--warn: #e0a856;
--refuted: #e0567a;
--accent: #e0567a;
--bg: #0e0e14;
--surface: #16171f;
--surface-2: #1e2029;
--text: #e8e9ef;
--text-muted: #9296ab;
--text-faint: #6d7186;
--rule: #2b2e3a;
--rule-soft: #22242e;
}
}
:root[data-theme="dark"] {
--confirm: #4fd0e0;
--warn: #e0a856;
--refuted: #e0567a;
--accent: #e0567a;
--bg: #0e0e14;
--surface: #16171f;
--surface-2: #1e2029;
--text: #e8e9ef;
--text-muted: #9296ab;
--text-faint: #6d7186;
--rule: #2b2e3a;
--rule-soft: #22242e;
}
/* Type roles: mono for every label and datum (this subject is hex and paths),
serif for prose so the two never blur into each other. */
:root {
--mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Menlo,
Consolas, "Liberation Mono", monospace;
--serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
Georgia, serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--serif);
font-size: 17px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 1180px;
margin: 0 auto;
padding: 0 24px 120px;
}
/* Prose stays near 66ch; panels break out to the full width. */
.col {
max-width: 66ch;
display: flex;
flex-direction: column;
gap: 1.1em;
}
/* ── Masthead ─────────────────────────────────────────────────────────────── */
header.mast {
border-bottom: 1px solid var(--rule);
padding: 72px 0 34px;
margin-bottom: 44px;
}
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--text-faint);
margin: 0 0 20px;
}
h1 {
font-family: var(--mono);
font-size: clamp(30px, 5.4vw, 52px);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.04;
margin: 0 0 22px;
text-wrap: balance;
}
h1 .dim { color: var(--text-faint); font-weight: 400; }
.standfirst {
font-size: 20px;
line-height: 1.5;
color: var(--text-muted);
max-width: 60ch;
margin: 0;
text-wrap: pretty;
}
/* ── Stat strip ───────────────────────────────────────────────────────────── */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
gap: 1px;
background: var(--rule-soft);
border: 1px solid var(--rule-soft);
margin: 40px 0 0;
}
.stat { background: var(--bg); padding: 16px 18px; }
.stat .n {
font-family: var(--mono);
font-size: 26px;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
display: block;
line-height: 1.1;
}
.stat .l {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.11em;
text-transform: uppercase;
color: var(--text-faint);
margin-top: 7px;
display: block;
line-height: 1.4;
}
/* ── Sections ─────────────────────────────────────────────────────────────── */
section { margin: 78px 0 0; }
h2 {
font-family: var(--mono);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text);
margin: 0 0 6px;
padding-bottom: 12px;
border-bottom: 1px solid var(--rule);
display: flex;
align-items: baseline;
gap: 14px;
}
h2 .idx { color: var(--accent); font-weight: 400; }
h2 .rest { color: var(--text-faint); font-weight: 400; letter-spacing: 0.1em; }
h3 {
font-family: var(--mono);
font-size: 14px;
font-weight: 700;
letter-spacing: 0.03em;
margin: 42px 0 4px;
}
p { margin: 0; }
.col > p + p { margin-top: 0; }
a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:focus-visible, button:focus-visible { outline: 2px solid var(--confirm); outline-offset: 3px; }
code, .m {
font-family: var(--mono);
font-size: 0.86em;
background: var(--surface-2);
padding: 1px 5px;
border-radius: 2px;
word-break: break-word;
}
strong { font-weight: 700; }
em { font-style: italic; color: var(--text); }
/* ── Confidence chips ─────────────────────────────────────────────────────── */
.chip {
font-family: var(--mono);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
padding: 2px 7px;
border: 1px solid currentColor;
border-radius: 2px;
white-space: nowrap;
display: inline-block;
line-height: 1.5;
}
.c-ok { color: var(--confirm); }
.c-part { color: var(--warn); }
.c-no { color: var(--refuted); }
.c-none { color: var(--text-faint); }
/* ── Panels (full-width breakouts) ────────────────────────────────────────── */
.panel {
margin: 30px 0 0;
border: 1px solid var(--rule);
background: var(--surface);
}
.panel-head {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--text-faint);
padding: 11px 16px;
border-bottom: 1px solid var(--rule-soft);
}
.panel-body { padding: 4px 0; overflow-x: auto; }
/* The schematic is a screen, not a page element: it keeps the game's own
ground in both themes, the way an instrument panel does. */
.screen {
background: var(--ground);
border: 1px solid var(--rule);
margin: 30px 0 0;
overflow-x: auto;
}
.screen svg { display: block; min-width: 760px; width: 100%; height: auto; }
figcaption {
font-family: var(--mono);
font-size: 11.5px;
line-height: 1.65;
color: var(--text-faint);
margin-top: 12px;
max-width: 76ch;
}
figure { margin: 0; }
/* ── Tables ───────────────────────────────────────────────────────────────── */
.tbl-scroll { overflow-x: auto; margin: 26px 0 0; }
table {
border-collapse: collapse;
width: 100%;
font-family: var(--mono);
font-size: 12.5px;
min-width: 620px;
}
th {
text-align: left;
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-faint);
font-weight: 700;
padding: 0 14px 9px 0;
border-bottom: 1px solid var(--rule);
white-space: nowrap;
}
td {
padding: 8px 14px 8px 0;
border-bottom: 1px solid var(--rule-soft);
vertical-align: top;
}
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; padding-right: 22px; }
tbody tr:hover { background: var(--surface); }
td .note { color: var(--text-muted); font-family: var(--serif); font-size: 13.5px; }
/* ── Mermaid ──────────────────────────────────────────────────────────────── */
.dia {
margin: 24px 0 0;
padding: 18px 16px;
background: var(--surface);
border: 1px solid var(--rule-soft);
overflow-x: auto;
}
.dia pre.mermaid { margin: 0; text-align: left; }
/* ── Callout ──────────────────────────────────────────────────────────────── */
.note-box {
border-left: 2px solid var(--accent);
padding: 2px 0 2px 18px;
margin: 26px 0 0;
color: var(--text-muted);
max-width: 64ch;
}
.note-box .lbl {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--accent);
display: block;
margin-bottom: 6px;
}
ul.plain { margin: 0; padding-left: 1.15em; display: flex; flex-direction: column; gap: 0.55em; }
ul.plain li::marker { color: var(--text-faint); }
/* ── Chain (numbered hops — the numbering is the hop order, which is real) ─── */
.hops { display: flex; flex-direction: column; gap: 0; margin: 24px 0 0; }
.hop {
display: grid;
grid-template-columns: 34px 1fr;
gap: 14px;
padding: 13px 0;
border-bottom: 1px solid var(--rule-soft);
align-items: baseline;
}
.hop:first-child { border-top: 1px solid var(--rule); }
.hop .n {
font-family: var(--mono);
font-size: 11px;
font-weight: 700;
color: var(--accent);
font-variant-numeric: tabular-nums;
}
.hop .b { font-family: var(--mono); font-size: 12.5px; line-height: 1.7; }
.hop .b .d { color: var(--text-muted); font-family: var(--serif); font-size: 14px; display: block; margin-top: 3px; }
footer {
margin-top: 96px;
padding-top: 26px;
border-top: 1px solid var(--rule);
font-family: var(--mono);
font-size: 11.5px;
color: var(--text-faint);
line-height: 1.8;
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}
</style>
<div class="wrap">
<header class="mast">
<p class="eyebrow">Project Sylpheed · Arc of Deception — reverse-engineering reference</p>
<h1>Sylpheed Disc Atlas</h1>
<p class="standfirst">
What is on the disc, and how every piece of it points at every other piece.
Four layers deep: the media, the archives, the container formats, and the
domain chains a port has to walk.
</p>
<div class="stats">
<div class="stat"><span class="n">41</span><span class="l">IPFB archives</span></div>
<div class="stat"><span class="n">26 443</span><span class="l">archive entries</span></div>
<div class="stat"><span class="n">166</span><span class="l">.xpr packages</span></div>
<div class="stat"><span class="n">97</span><span class="l">.wmv cutscenes</span></div>
<div class="stat"><span class="n">9 519</span><span class="l">sound banks</span></div>
<div class="stat"><span class="n">3</span><span class="l">hash functions</span></div>
</div>
</header>
<div class="col">
<p>
Nothing on this disc is found by path at runtime. Every lookup is a
<em>hash</em>, and the whole disc is joined together by three of them with
different rules. Read that first, or the rest of the map reads as a pile of
unrelated tables.
</p>
<p>
Counts here were measured on the retail extract for this page, not copied
forward. Where a claim is contested or partial, the chip says so — and the
things that <em>do not</em> resolve get their own section, because a port
has to survive them.
</p>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<section>
<h2><span class="idx">01</span> <span>The join algebra</span> <span class="rest">three hashes, three rules</span></h2>
<div class="col">
<p>
These are not interchangeable, and mixing them up is the single most
productive mistake this project has made. They differ in what they hash,
and crucially in <strong>case sensitivity</strong>.
</p>
</div>
<div class="tbl-scroll">
<table>
<thead>
<tr><th>Hash</th><th>Keys</th><th>Case</th><th>Verified</th><th></th></tr>
</thead>
<tbody>
<tr>
<td><strong>name_hash</strong></td>
<td>IPFB archive TOC entries — a file path inside a <span class="m">.pak</span></td>
<td>insensitive</td>
<td class="num">recovers paths</td>
<td><span class="chip c-ok">confirmed</span></td>
</tr>
<tr>
<td><strong>tag_hash</strong></td>
<td>IDXD record names and field keys</td>
<td><em>sensitive</em></td>
<td class="num">1 271 462 / 1 271 462</td>
<td><span class="chip c-ok">confirmed</span></td>
</tr>
<tr>
<td><strong>ixud_hash</strong></td>
<td>IXUD localised-text record and field keys</td>
<td><em>sensitive</em></td>
<td class="num">628 165 / 628 165</td>
<td><span class="chip c-ok">confirmed</span></td>
</tr>
</tbody>
</table>
</div>
<div class="note-box">
<span class="lbl">The trap that cost the most</span>
A 24-bit modulus cannot uniquely name an identifier of eight characters or
more. Exhaustive preimage search recovers <span class="m">"Stage01"</span>
from its own hash, but at seven characters one real target already has
<strong>1 176</strong> preimages. Forty-two field keys on this disc are
hash-only with no stored name, and they are <em>provably</em> unrecoverable
— not merely unrecovered. Everything else carries its name inline: an IDXD
field's middle word points at its own name string, which is why the
containers are self-describing and the hash almost never has to be inverted.
</div>
</section>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<section>
<h2><span class="idx">02</span> <span>The four layers</span> <span class="rest">disc → archive → container → domain</span></h2>
<div class="col">
<p>
Reading up from the bottom: a domain chain names an asset by string, that
string hashes into an archive TOC, the TOC yields a compressed blob, and
the blob's first four bytes say which container format it is. Every arrow
in this diagram is a hash lookup or a magic-byte test — there are no
directory scans.
</p>
</div>
<figure>
<div class="screen">
<svg viewBox="0 0 1000 640" role="img" aria-label="Four-layer schematic of the Project Sylpheed disc: media, archives, container formats and domain chains, joined by three hash functions.">
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#4f5468"/>
</marker>
<style>
.bl { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10px; letter-spacing: .16em; fill: #6d7186; text-transform: uppercase; }
.bx { fill: #16171f; stroke: #2b2e3a; stroke-width: 1; }
.bxa { fill: #16171f; stroke: #4fd0e0; stroke-width: 1; }
.tt { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; fill: #e8e9ef; }
.ts { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10px; fill: #6d7186; }
.tn { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11px; fill: #4fd0e0; }
.jn { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10.5px; fill: #e0a856; letter-spacing: .06em; }
.ln { stroke: #4f5468; stroke-width: 1; }
.rl { stroke: #22242e; stroke-width: 1; }
</style>
</defs>
<!-- band rules -->
<line class="rl" x1="0" y1="118" x2="1000" y2="118"/>
<line class="rl" x1="0" y1="268" x2="1000" y2="268"/>
<line class="rl" x1="0" y1="418" x2="1000" y2="418"/>
<!-- ── BAND 1 · disc media ── -->
<text class="bl" x="24" y="40">Disc</text>
<g>
<rect class="bx" x="130" y="30" width="160" height="58" rx="2"/>
<text class="tt" x="145" y="54">default.xex</text>
<text class="ts" x="145" y="70">executable + PE</text>
<rect class="bx" x="302" y="30" width="160" height="58" rx="2"/>
<text class="tt" x="317" y="54">dat/</text>
<text class="ts" x="317" y="70">37 archives</text>
<rect class="bx" x="474" y="30" width="160" height="58" rx="2"/>
<text class="tt" x="489" y="54">hidden/</text>
<text class="ts" x="489" y="70">DefTables · MiscBin</text>
<rect class="bx" x="646" y="30" width="160" height="58" rx="2"/>
<text class="tt" x="661" y="54">dat/movie/</text>
<text class="ts" x="661" y="70">97 wmv · 6 paks</text>
<rect class="bx" x="818" y="30" width="160" height="58" rx="2"/>
<text class="tt" x="833" y="54">resource3d/</text>
<text class="ts" x="833" y="70">166 xpr · 1.4 GB</text>
</g>
<!-- join 1 -->
<line class="ln" x1="382" y1="88" x2="382" y2="176" marker-end="url(#ar)"/>
<line class="ln" x1="554" y1="88" x2="554" y2="176" marker-end="url(#ar)"/>
<line class="ln" x1="726" y1="88" x2="726" y2="176" marker-end="url(#ar)"/>
<line class="ln" x1="898" y1="88" x2="898" y2="220" marker-end="url(#ar)"/>
<text class="jn" x="130" y="112">name_hash keys the TOC — case-insensitive</text>
<!-- ── BAND 2 · archives ── -->
<text class="bl" x="24" y="190">Archive</text>
<g>
<rect class="bxa" x="130" y="176" width="560" height="72" rx="2"/>
<text class="tt" x="146" y="200">IPFB · .pak index + .p00….pNN data segments</text>
<text class="ts" x="146" y="218">12-byte TOC entry { name_hash, offset, comp_size } · Z1/zlib payloads</text>
<text class="tn" x="146" y="236">41 archives · 26 443 entries</text>
<rect class="bx" x="706" y="176" width="272" height="72" rx="2"/>
<text class="tt" x="722" y="200">raw files (not archived)</text>
<text class="ts" x="722" y="218">.wmv video · .xpr resource packages</text>
<text class="tn" x="722" y="236">263 files</text>
</g>
<!-- join 2 -->
<line class="ln" x1="200" y1="248" x2="200" y2="292" marker-end="url(#ar)"/>
<line class="ln" x1="410" y1="248" x2="410" y2="292" marker-end="url(#ar)"/>
<line class="ln" x1="620" y1="248" x2="620" y2="292" marker-end="url(#ar)"/>
<line class="ln" x1="842" y1="248" x2="842" y2="292" marker-end="url(#ar)"/>
<text class="jn" x="130" y="264">payload magic decides the format — nothing declares a type</text>
<!-- ── BAND 3 · containers ── -->
<text class="bl" x="24" y="330">Container</text>
<g>
<rect class="bxa" x="130" y="292" width="77" height="62" rx="2"/>
<text class="tt" x="140" y="316">IDXD</text><text class="ts" x="140" y="332">tables</text><text class="tn" x="140" y="347">6325</text>
<rect class="bxa" x="215" y="292" width="77" height="62" rx="2"/>
<text class="tt" x="225" y="316">T8aD</text><text class="ts" x="225" y="332">2D tex</text><text class="tn" x="225" y="347">4525</text>
<rect class="bxa" x="300" y="292" width="77" height="62" rx="2"/>
<text class="tt" x="310" y="316">RATC</text><text class="ts" x="310" y="332">UI bundle</text><text class="tn" x="310" y="347">2985</text>
<rect class="bxa" x="385" y="292" width="77" height="62" rx="2"/>
<text class="tt" x="395" y="316">IXUD</text><text class="ts" x="395" y="332">text</text><text class="tn" x="395" y="347">1104</text>
<rect class="bxa" x="470" y="292" width="77" height="62" rx="2"/>
<text class="tt" x="480" y="316">LSTA</text><text class="ts" x="480" y="332">sprites</text><text class="tn" x="480" y="347">64</text>
<rect class="bxa" x="555" y="292" width="77" height="62" rx="2"/>
<text class="tt" x="565" y="316">SLB</text><text class="ts" x="565" y="332">XMA1</text><text class="tn" x="565" y="347">9519</text>
<rect class="bxa" x="640" y="292" width="77" height="62" rx="2"/>
<text class="tt" x="650" y="316">XBG7</text><text class="ts" x="650" y="332">mesh</text><text class="tn" x="650" y="347">6294</text>
<rect class="bxa" x="725" y="292" width="77" height="62" rx="2"/>
<text class="tt" x="735" y="316">XPR2</text><text class="ts" x="735" y="332">tex pkg</text><text class="tn" x="735" y="347">166</text>
<rect class="bxa" x="810" y="292" width="77" height="62" rx="2"/>
<text class="tt" x="820" y="316">TTF</text><text class="ts" x="820" y="332">fonts</text><text class="tn" x="820" y="347">54</text>
<rect class="bx" x="895" y="292" width="77" height="62" rx="2" stroke="#e0567a"/>
<text class="tt" x="905" y="316">ISB</text><text class="ts" x="905" y="332">PRT · BIN</text>
<text class="ts" x="905" y="347" fill="#e0567a">no parser</text>
</g>
<!-- join 3 -->
<line class="ln" x1="200" y1="354" x2="200" y2="442" marker-end="url(#ar)"/>
<line class="ln" x1="440" y1="354" x2="440" y2="442" marker-end="url(#ar)"/>
<line class="ln" x1="680" y1="354" x2="680" y2="442" marker-end="url(#ar)"/>
<line class="ln" x1="900" y1="354" x2="900" y2="442" marker-end="url(#ar)"/>
<text class="jn" x="130" y="410">tag_hash / ixud_hash key records + fields — values are asset NAMES, hashed back up</text>
<!-- ── BAND 4 · domains ── -->
<text class="bl" x="24" y="480">Domain</text>
<g>
<rect class="bx" x="130" y="442" width="112" height="66" rx="2"/>
<text class="tt" x="141" y="466">Stage</text><text class="ts" x="141" y="482">28 missions</text><text class="ts" x="141" y="496">10 sub-tables</text>
<rect class="bx" x="252" y="442" width="112" height="66" rx="2"/>
<text class="tt" x="263" y="466">Unit</text><text class="ts" x="263" y="482">159 fields</text><text class="ts" x="263" y="496">→ turret slot</text>
<rect class="bx" x="374" y="442" width="112" height="66" rx="2"/>
<text class="tt" x="385" y="466">Arsenal</text><text class="ts" x="385" y="482">59 items</text><text class="ts" x="385" y="496">4-hop chain</text>
<rect class="bx" x="496" y="442" width="112" height="66" rx="2"/>
<text class="tt" x="507" y="466">Audio</text><text class="ts" x="507" y="482">5798 cues</text><text class="ts" x="507" y="496">5 families</text>
<rect class="bx" x="618" y="442" width="112" height="66" rx="2"/>
<text class="tt" x="629" y="466">Cutscene</text><text class="ts" x="629" y="482">104 slots</text><text class="ts" x="629" y="496">4 bindings</text>
<rect class="bx" x="740" y="442" width="112" height="66" rx="2"/>
<text class="tt" x="751" y="466">UI screen</text><text class="ts" x="751" y="482">965 builds</text><text class="ts" x="751" y="496">decl + place</text>
<rect class="bx" x="862" y="442" width="112" height="66" rx="2"/>
<text class="tt" x="873" y="466">Save</text><text class="ts" x="873" y="482">545 bytes</text><text class="ts" x="873" y="496">GDHA + zlib</text>
</g>
<!-- the cross-domain edge: the save indexes the arsenal's string order -->
<path class="ln" d="M 918 508 L 918 560 L 430 560 L 430 508" fill="none" stroke-dasharray="3 3" marker-end="url(#ar)"/>
<text class="jn" x="560" y="578">the save's 54-byte blob is indexed by strings.tbl order — not weapon.tbl</text>
<text class="ts" x="130" y="614" fill="#4f5468">Counts measured on the retail extract. Container counts exclude sound.pak and entries over 8 MB.</text>
</svg>
</div>
<figcaption>
The three amber lines are the joins. Each is a hash lookup, and each is a
place a port can silently take the wrong branch: an archive TOC miss
returns nothing, and a field the disc never values reads as
<span class="m">0.0</span> rather than as an error.
</figcaption>
</figure>
</section>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<section>
<h2><span class="idx">03</span> <span>Archives</span> <span class="rest">where the mass actually sits</span></h2>
<div class="col">
<p>
Two archives hold most of the disc. <span class="m">sound.pak</span> is
1.07 GB across five data segments — more than every other archive
combined — and <span class="m">GP_HANGAR_ARSENAL.pak</span> is large for a
menu because it is <em>stage-scoped</em>: 168 of its objects are 28
missions × 6 languages, each a complete Hangar configuration.
</p>
</div>
<div class="tbl-scroll">
<table>
<thead>
<tr><th>Archive</th><th class="num">Entries</th><th class="num">Stored</th><th>What it is</th></tr>
</thead>
<tbody>
<tr><td>sound.pak</td><td class="num">9 519</td><td class="num">1.07 GB</td><td><span class="note">every XMA1 bank — music, jingles, SFX, both voice languages</span></td></tr>
<tr><td>GP_HANGAR_ARSENAL.pak</td><td class="num">1 538</td><td class="num">67.2 MB</td><td><span class="note">28 stages × 6 languages of Hangar config, plus item text</span></td></tr>
<tr><td>GP_MAIN_GAME_{D,E,F,I,J,S}</td><td class="num">1 119 ea.</td><td class="num">4.6 MB ea.</td><td><span class="note">the mission data set — stages, units, routes, dialogue</span></td></tr>
<tr><td>GP_READY_ROOM.pak</td><td class="num">1 106</td><td class="num">67.6 MB</td><td><span class="note">largest UI pak; ISL script bytecode + link map</span></td></tr>
<tr><td>GP_MAIN_GAME_*2D <span class="chip c-no">unnamed</span></td><td class="num">711 ea.</td><td class="num">15.2 MB ea.</td><td><span class="note">six paks, 0 % of names recoverable — see §06</span></td></tr>
<tr><td>DefTables.pak <span class="m">hidden/</span></td><td class="num">1 465</td><td class="num"></td><td><span class="note">definition tables; 804 of 1 465 names resolved</span></td></tr>
<tr><td>movie/{eng,jpn,deu,esp,fra,ita}</td><td class="num">117 ea.</td><td class="num">1.8 MB ea.</td><td><span class="note">subtitles, telop overlays and fonts — <em>not</em> voice</span></td></tr>
<tr><td>tables.pak</td><td class="num">79</td><td class="num">330 KB</td><td><span class="note">the registries: sound cues, the cutscene manifest</span></td></tr>
<tr><td>MiscBin.pak <span class="m">hidden/</span></td><td class="num">40</td><td class="num"></td><td><span class="note">0 names resolved</span></td></tr>
<tr><td>fonts.pak</td><td class="num">3</td><td class="num">2.4 MB</td><td><span class="note">the three shipped typefaces</span></td></tr>
</tbody>
</table>
</div>
<div class="note-box">
<span class="lbl">One entry lies about its size</span>
<span class="m">Static.slb</span>, the sound-effect bank, sits at the highest
offset in <span class="m">sound.pak</span> and declares
<strong>616 768 bytes more than the disc holds</strong>. This is not a bad
extract — <span class="m">sound.p04</span> matches the ISO's own directory
record — and a sweep of every archive finds this entry over-running and no
other. The last entry's size field is an allocation size. A reader must
allow a short read there, and <em>only</em> there.
</div>
</section>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<section>
<h2><span class="idx">04</span> <span>Container formats</span> <span class="rest">what each blob turns into</span></h2>
<div class="tbl-scroll">
<table>
<thead>
<tr><th>Magic</th><th>Content</th><th>State</th><th>Reach</th></tr>
</thead>
<tbody>
<tr><td><strong>IDXD</strong></td><td><span class="note">self-describing record/field table — the game's whole data layer</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">7 750 / 7 750 objects</td></tr>
<tr><td><strong>IXUD</strong></td><td><span class="note">the same container, UTF-16BE, offsets in <em>chars</em></span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">1 104 / 1 104 objects</td></tr>
<tr><td><strong>T8aD</strong></td><td><span class="note">2D texture — a list of arbitrary sub-rectangles, not a tile grid</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">19 216 / 19 216</td></tr>
<tr><td><strong>RATC</strong></td><td><span class="note">UI bundle; children are sprites, layout records and primitives</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">10 144 / 10 148 refs</td></tr>
<tr><td><strong>LSTA</strong></td><td><span class="note">sprite display list — count covers T8aD <em>and</em> PRMD</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">64 / 64</td></tr>
<tr><td><strong>XBG7</strong></td><td><span class="note">mesh; index pool then vertex pool, layout declared per sub-mesh</span></td><td><span class="chip c-part">99.25 %</span></td><td class="num">6 247 / 6 294</td></tr>
<tr><td><strong>XPR2</strong></td><td><span class="note">texture package — de-tile, A8R8G8B8 and DXT1</span></td><td><span class="chip c-part">partial</span></td><td class="num">channel order confirmed</td></tr>
<tr><td><strong>SLB</strong></td><td><span class="note">XACT bank of XMA1 sub-waves; two layouts, one headerless</span></td><td><span class="chip c-part">35/36 shared</span></td><td class="num"><span class="m">JNGL_001</span> fails</td></tr>
<tr><td><strong>GDHA</strong></td><td><span class="note">save file — zlib payload, chunk stream, round-trips byte-identically</span></td><td><span class="chip c-part">~11 ❔ fields</span></td><td class="num">545 bytes</td></tr>
<tr><td>TTF / OTF</td><td><span class="note">stock OpenType</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">54</td></tr>
<tr><td><strong>ISB</strong></td><td><span class="note">ISL mission-script bytecode</span></td><td><span class="chip c-part">partial</span></td><td class="num">builtins documented</td></tr>
<tr><td><strong>PRT</strong></td><td><span class="note">telop — on-screen text overlay for cutscenes</span></td><td><span class="chip c-none">no parser</span></td><td class="num">22 bound</td></tr>
<tr><td><strong>BIN</strong></td><td><span class="note">collision meshes, <span class="m">CollisionSet_S&lt;NN&gt;.bin</span></span></td><td><span class="chip c-none">no parser</span></td><td class="num">documented only</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<section>
<h2><span class="idx">05</span> <span>The domain chains</span> <span class="rest">what references what</span></h2>
<div class="col">
<p>
Each of these starts from something the player can see and ends at bytes on
the disc. Where a chain has a hop that looks like it should be direct and
is not, that hop is called out — those are where a naive port breaks.
</p>
</div>
<h3>Mission</h3>
<div class="col">
<p>
One IDXD object per stage in every language pak. Its
<span class="m">StageResource</span> record is the hub: nineteen fields
naming the 3D packages, the enemy roster, the routes, the collision set
and the localised objective strings.
</p>
</div>
<div class="dia">
<pre class="mermaid">
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1e2029','primaryTextColor':'#e8e9ef','primaryBorderColor':'#4fd0e0','lineColor':'#8a8fa3','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px','clusterBkg':'#16171f','clusterBorder':'#2b2e3a'}}}%%
flowchart LR
A["GP_MAIN_GAME_&lt;lang&gt;.pak"] --> B["Stage_S&lt;NN&gt; record"]
B --> C["StageResource"]
C --> D["Stage_S&lt;NN&gt;.xpr<br/>BG_&lt;place&gt;.xpr"]
C --> E["EnumUnit_S&lt;NN&gt;.tbl"]
C --> F["UnitGroup_S&lt;NN&gt;.tbl<br/>squadron roster"]
C --> G["Route_S&lt;NN&gt;.tbl<br/>FormationSet_S&lt;NN&gt;.tbl"]
C --> H["CollisionSet_S&lt;NN&gt;.bin"]
C --> I["EnumLocalString_S&lt;NN&gt;.tbl"]
B --> J["Stage\script.tbl"]
J --> K["StageNN.ssb<br/>ISL bytecode"]
</pre>
</div>
<h3>Arsenal — the four-hop trap</h3>
<div class="col">
<p>
The Hangar lists 59 weapons; the disc's <span class="m">Weapon</span> table
has 131, and the two name sets overlap in <strong>zero</strong> values.
That is not a mismatch to be reconciled — it is the wrong join. An arsenal
item names a <em>hardpoint slot</em>, and the slot carries the weapon.
</p>
</div>
<div class="hops">
<div class="hop"><span class="n">01</span><span class="b">loadout record &nbsp;<span style="color:var(--text-faint)">— flight position × pilot, e.g. Rhino2-Ellen</span><span class="d">Names <code>Arm1</code> / <code>Arm2</code> / <code>Arm3</code> / <code>Nose</code> — which are not items either.</span></span></div>
<div class="hop"><span class="n">02</span><span class="b">per-slot allow-list record<span class="d">Its only <em>named</em> field is <code>Type</code>; the candidate items live in its positional, unnamed fields, in order.</span></span></div>
<div class="hop"><span class="n">03</span><span class="b">arsenal item &nbsp;<span style="color:var(--text-faint)">→ .PlayerWeapon</span><span class="d">Resolves to <code>Turret_050</code> — a slot on the player craft's own unit table. 59 of 59 do; 0 of 59 name a weapon.</span></span></div>
<div class="hop"><span class="n">04</span><span class="b">turret slot &nbsp;<span style="color:var(--text-faint)">→ .WeaponID</span><span class="d">And <em>this</em> is the <code>Weapon</code> record.</span></span></div>
</div>
<h3>Audio</h3>
<div class="col">
<p>
One IDXD object in <span class="m">tables.pak</span> is the whole cue
index. Its <span class="m">SOUNDS</span> record is the only record found on
the disc that is entirely named fields — 5 798 of them — and the names are
the join key. Cue ids are partitioned by family with no overlap.
</p>
</div>
<div class="dia">
<pre class="mermaid">
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1e2029','primaryTextColor':'#e8e9ef','primaryBorderColor':'#4fd0e0','lineColor':'#8a8fa3','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
flowchart LR
M["script message id"] --> S["SOUNDS<br/>cue name → sound id"]
S --> F["FILES<br/>5 135 bank paths"]
F --> H{{"name_hash"}}
H --> P["sound.pak<br/>9 519 entries"]
P --> X["XMA1 sub-waves"]
S -.-> C1["VOICE 15007331"]
S -.-> C2["SE 1901"]
S -.-> C3["DEMO 80008408"]
S -.-> C4["BR 85008600"]
S -.-> C5["BGM 10011109"]
</pre>
</div>
<div class="note-box">
<span class="lbl">Two things the paths do not tell you</span>
The 36 music, jingle and SFX banks sit at the table <em>root</em> with no
language component, so both <span class="m">sounds.tbl</span> files name
them. And a <span class="m">.slb</span> need not hold the track its name
claims: the movie voices are one continuous stream chunked into TOC entries
whose boundaries do not line up with the cues.
</div>
<h3>Cutscene</h3>
<div class="col">
<p>
One manifest record binds four assets per slot. Measured for this page:
104 slots over 101 distinct movies, 99 with a subtitle track, 99 with a
voice track, 22 with a telop overlay.
</p>
</div>
<div class="dia">
<pre class="mermaid">
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1e2029','primaryTextColor':'#e8e9ef','primaryBorderColor':'#4fd0e0','lineColor':'#8a8fa3','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
flowchart LR
MF["cutscene manifest<br/>tables.pak"] --> MV["MOVIE<br/>dat/movie/&lt;name&gt;.wmv"]
MF --> TL["TELOP<br/>&lt;lang&gt;.pak+*.prt"]
MF --> SB["SUBTITLE<br/>&lt;lang&gt;.pak+SUBTITLE_*.tbl"]
MF --> VT["VOICETRACK<br/>cue name"]
SB --> IX["IXUD caption keys"]
IX --> TX["GP_MAIN_GAME_&lt;lang&gt;<br/>8 800 text keys"]
VT --> SC["sound cue → .slb region"]
</pre>
</div>
<h3>UI screen</h3>
<div class="col">
<p>
One archive per screen; each RATC bundle is one context × language build.
A bundle's declaration table lists every element with its parent and pivot;
the placement region right after it gives each element a keyframe group.
Both the tutorial pause menu and the title main menu rebuild
pixel-accurately from the disc alone.
</p>
<p>
Two things are <em>not</em> what they look like. The resting position is
neither the first nor the last keyframe — it is the plateau. And the
declaration table is <strong>not</strong> the paint order: a per-draw
capture of the running title screen paints element 13 first and elements
0 and 1 late. The real order is a second, reordered child array the screen
object keeps at runtime — deriving it from the bundle is still open.
</p>
</div>
<h3>Save</h3>
<div class="col">
<p>
545 bytes: a <span class="m">GDHA</span> container, a zlib payload, then a
chunk stream. The 54-byte develop blob has the alphabet <span class="m">0
locked / 2 developable / 4 developed</span>, and is indexed by
<span class="m">strings.tbl</span> order — the display order <em>plus</em>
the cut items only the localisation file lists. <span class="m">weapon.tbl</span>'s
id list is not the index space; that it is also 54 long is a coincidence,
and the two agree only to index 32.
</p>
</div>
<div class="note-box">
<span class="lbl">The stale-summary trap</span>
The in-game Details panel reads a <em>summary copy</em> held in the container
header, not the payload. Edit the payload alone and the panel keeps showing
the old values — which reads exactly like a failed parse.
</div>
</section>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<section>
<h2><span class="idx">06</span> <span>References that go nowhere</span> <span class="rest">a port has to survive these</span></h2>
<div class="col">
<p>
These are not gaps in the reverse engineering. They are properties of the
shipped disc, and each one was found by a check that expected the opposite.
</p>
</div>
<div class="tbl-scroll">
<table>
<thead><tr><th>Reference</th><th>Where</th><th>Status</th></tr></thead>
<tbody>
<tr><td><span class="m">pmbase.t32</span></td><td><span class="note">named by <span class="m">pmbase.rat</span> in all four <span class="m">GP_STAGE_CLEAR</span> language builds</span></td><td><span class="chip c-no">nowhere on disc</span></td></tr>
<tr><td><span class="m">SUBTITLE_S12B.tbl</span></td><td><span class="note">bound by the manifest, in all six languages</span></td><td><span class="chip c-no">resolves in none</span></td></tr>
<tr><td><span class="m">logo1…logo4</span>, <span class="m">SYLPH_HD720p_8M-CBR_2ch</span></td><td><span class="note">manifest-bound movies</span></td><td><span class="chip c-no">no .wmv</span></td></tr>
<tr><td><span class="m">dat\GP_TEST\</span></td><td><span class="note">a debug archive the script manifest points at</span></td><td><span class="chip c-no">not shipped</span></td></tr>
<tr><td>6 × <span class="m">GP_MAIN_GAME_*2D</span></td><td><span class="note">711 entries each; eleven other paks are at 100 % by the same method</span></td><td><span class="chip c-no">0 % named</span></td></tr>
<tr><td><span class="m">GP_READY_ROOM.pak</span></td><td><span class="note">the largest UI pak on the disc</span></td><td><span class="chip c-no">6 of 1 106 named</span></td></tr>
<tr><td>42 ISL script-symbol keys</td><td><span class="note">hash-only field keys in <span class="m">&lt;lang&gt;\script\ID.tbl</span></span></td><td><span class="chip c-no">provably unrecoverable</span></td></tr>
<tr><td>9 of 101 cutscenes</td><td><span class="note">no English transcript resolves</span></td><td><span class="chip c-part">unexplained</span></td></tr>
<tr><td><span class="m">JNGL_001.slb</span></td><td><span class="note">headerless bank; payload is not a whole number of XMA1 packets</span></td><td><span class="chip c-part">does not decode</span></td></tr>
</tbody>
</table>
</div>
<div class="note-box">
<span class="lbl">Why the 2D paks matter more than they look</span>
Six archives at <em>exactly</em> 0.0 %, all six, 711 entries each — while
eleven menu paks hit 100 % in the same run. That control is what makes it a
finding rather than a failed guess: the naming method works, and these
archives are outside it. Whatever names their contents is not a path hashed
the way every other archive's is.
</div>
</section>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<section>
<h2><span class="idx">07</span> <span>Still open</span></h2>
<div class="col">
<ul class="plain">
<li><strong>UI paint order from the bundle.</strong> Solved at runtime, not statically — and static is what a port needs.</li>
<li><strong>The <span class="m">.prt</span> telop format.</strong> 22 cutscenes bind one; no parser exists.</li>
<li><strong>Collision meshes.</strong> <span class="m">CollisionSet_S&lt;NN&gt;.bin</span> is documented and unparsed.</li>
<li><strong>47 XBG7 resources</strong> still miss — mostly pose/proxy composites and damage variants, not a threshold away.</li>
<li><strong>~11 GHAD save fields</strong> unnamed, and difficulty-versus-stage undecided: three fields hold the value 2.</li>
<li><strong>The naming of the 2D and Ready Room archives</strong>, which is the largest single block of unreachable content on the disc.</li>
</ul>
</div>
</section>
<footer>
Counts measured on the retail extract (USA/Europe, En/Ja). Container-format
counts exclude <span class="m">sound.pak</span> and entries over 8 MB.<br/>
Confidence follows the corpus convention — confirmed · partial · refuted —
and is per-claim, never per-document.
</footer>
</div>