Project Sylpheed · Arc of Deception — reverse-engineering reference
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.
Nothing on this disc is found by path at runtime. Every lookup is a hash, 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.
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 do not resolve get their own section, because a port has to survive them.
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 case sensitivity.
| Hash | Keys | Case | Verified | |
|---|---|---|---|---|
| name_hash | IPFB archive TOC entries — a file path inside a .pak | insensitive | recovers paths | confirmed |
| tag_hash | IDXD record names and field keys | sensitive | 1 271 462 / 1 271 462 | confirmed |
| ixud_hash | IXUD localised-text record and field keys | sensitive | 628 165 / 628 165 | confirmed |
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.
Two archives hold most of the disc. sound.pak is 1.07 GB across five data segments — more than every other archive combined — and GP_HANGAR_ARSENAL.pak is large for a menu because it is stage-scoped: 168 of its objects are 28 missions × 6 languages, each a complete Hangar configuration.
| Archive | Entries | Stored | What it is |
|---|---|---|---|
| sound.pak | 9 519 | 1.07 GB | every XMA1 bank — music, jingles, SFX, both voice languages |
| GP_HANGAR_ARSENAL.pak | 1 538 | 67.2 MB | 28 stages × 6 languages of Hangar config, plus item text |
| GP_MAIN_GAME_{D,E,F,I,J,S} | 1 119 ea. | 4.6 MB ea. | the mission data set — stages, units, routes, dialogue |
| GP_READY_ROOM.pak | 1 106 | 67.6 MB | largest UI pak; ISL script bytecode + link map |
| GP_MAIN_GAME_*2D unnamed | 711 ea. | 15.2 MB ea. | six paks, 0 % of names recoverable — see §06 |
| DefTables.pak hidden/ | 1 465 | — | definition tables; 804 of 1 465 names resolved |
| movie/{eng,jpn,deu,esp,fra,ita} | 117 ea. | 1.8 MB ea. | subtitles, telop overlays and fonts — not voice |
| tables.pak | 79 | 330 KB | the registries: sound cues, the cutscene manifest |
| MiscBin.pak hidden/ | 40 | — | 0 names resolved |
| fonts.pak | 3 | 2.4 MB | the three shipped typefaces |
| Magic | Content | State | Reach |
|---|---|---|---|
| IDXD | self-describing record/field table — the game's whole data layer | confirmed | 7 750 / 7 750 objects |
| IXUD | the same container, UTF-16BE, offsets in chars | confirmed | 1 104 / 1 104 objects |
| T8aD | 2D texture — a list of arbitrary sub-rectangles, not a tile grid | confirmed | 19 216 / 19 216 |
| RATC | UI bundle; children are sprites, layout records and primitives | confirmed | 10 144 / 10 148 refs |
| LSTA | sprite display list — count covers T8aD and PRMD | confirmed | 64 / 64 |
| XBG7 | mesh; index pool then vertex pool, layout declared per sub-mesh | 99.25 % | 6 247 / 6 294 |
| XPR2 | texture package — de-tile, A8R8G8B8 and DXT1 | partial | channel order confirmed |
| SLB | XACT bank of XMA1 sub-waves; two layouts, one headerless | 35/36 shared | JNGL_001 fails |
| GDHA | save file — zlib payload, chunk stream, round-trips byte-identically | ~11 ❔ fields | 545 bytes |
| TTF / OTF | stock OpenType | confirmed | 54 |
| ISB | ISL mission-script bytecode | partial | builtins documented |
| PRT | telop — on-screen text overlay for cutscenes | no parser | 22 bound |
| BIN | collision meshes, CollisionSet_S<NN>.bin | no parser | documented only |
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.
One IDXD object per stage in every language pak. Its StageResource record is the hub: nineteen fields naming the 3D packages, the enemy roster, the routes, the collision set and the localised objective strings.
%%{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_<lang>.pak"] --> B["Stage_S<NN> record"]
B --> C["StageResource"]
C --> D["Stage_S<NN>.xpr
BG_<place>.xpr"]
C --> E["EnumUnit_S<NN>.tbl"]
C --> F["UnitGroup_S<NN>.tbl
squadron roster"]
C --> G["Route_S<NN>.tbl
FormationSet_S<NN>.tbl"]
C --> H["CollisionSet_S<NN>.bin"]
C --> I["EnumLocalString_S<NN>.tbl"]
B --> J["Stage\script.tbl"]
J --> K["StageNN.ssb
ISL bytecode"]
The Hangar lists 59 weapons; the disc's Weapon table has 131, and the two name sets overlap in zero values. That is not a mismatch to be reconciled — it is the wrong join. An arsenal item names a hardpoint slot, and the slot carries the weapon.
Arm1 / Arm2 / Arm3 / Nose — which are not items either.Type; the candidate items live in its positional, unnamed fields, in order.Turret_050 — a slot on the player craft's own unit table. 59 of 59 do; 0 of 59 name a weapon.Weapon record.One IDXD object in tables.pak is the whole cue index. Its SOUNDS 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.
%%{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
cue name → sound id"]
S --> F["FILES
5 135 bank paths"]
F --> H{{"name_hash"}}
H --> P["sound.pak
9 519 entries"]
P --> X["XMA1 sub-waves"]
S -.-> C1["VOICE 1500–7331"]
S -.-> C2["SE 1–901"]
S -.-> C3["DEMO 8000–8408"]
S -.-> C4["BR 8500–8600"]
S -.-> C5["BGM 1001–1109"]
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.
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1e2029','primaryTextColor':'#e8e9ef','primaryBorderColor':'#4fd0e0','lineColor':'#8a8fa3','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
flowchart LR
MF["cutscene manifest
tables.pak"] --> MV["MOVIE
dat/movie/<name>.wmv"]
MF --> TL["TELOP
<lang>.pak+*.prt"]
MF --> SB["SUBTITLE
<lang>.pak+SUBTITLE_*.tbl"]
MF --> VT["VOICETRACK
cue name"]
SB --> IX["IXUD caption keys"]
IX --> TX["GP_MAIN_GAME_<lang>
8 800 text keys"]
VT --> SC["sound cue → .slb region"]
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.
Two things are not what they look like. The resting position is neither the first nor the last keyframe — it is the plateau. And the declaration table is not 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.
545 bytes: a GDHA container, a zlib payload, then a chunk stream. The 54-byte develop blob has the alphabet 0 locked / 2 developable / 4 developed, and is indexed by strings.tbl order — the display order plus the cut items only the localisation file lists. weapon.tbl'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.
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.
| Reference | Where | Status |
|---|---|---|
| pmbase.t32 | named by pmbase.rat in all four GP_STAGE_CLEAR language builds | nowhere on disc |
| SUBTITLE_S12B.tbl | bound by the manifest, in all six languages | resolves in none |
| logo1…logo4, SYLPH_HD720p_8M-CBR_2ch | manifest-bound movies | no .wmv |
| dat\GP_TEST\ | a debug archive the script manifest points at | not shipped |
| 6 × GP_MAIN_GAME_*2D | 711 entries each; eleven other paks are at 100 % by the same method | 0 % named |
| GP_READY_ROOM.pak | the largest UI pak on the disc | 6 of 1 106 named |
| 42 ISL script-symbol keys | hash-only field keys in <lang>\script\ID.tbl | provably unrecoverable |
| 9 of 101 cutscenes | no English transcript resolves | unexplained |
| JNGL_001.slb | headerless bank; payload is not a whole number of XMA1 packets | does not decode |