Project Sylpheed · Arc of Deception — reverse-engineering reference

Sylpheed Disc Atlas

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.

41IPFB archives
26 443archive entries
166.xpr packages
97.wmv cutscenes
9 519sound banks
3hash functions

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.

01 The join algebra three hashes, three rules

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.

HashKeysCaseVerified
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
The trap that cost the most A 24-bit modulus cannot uniquely name an identifier of eight characters or more. Exhaustive preimage search recovers "Stage01" from its own hash, but at seven characters one real target already has 1 176 preimages. Forty-two field keys on this disc are hash-only with no stored name, and they are provably 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.

02 The four layers disc → archive → container → domain

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.

Disc default.xex executable + PE dat/ 37 archives hidden/ DefTables · MiscBin dat/movie/ 97 wmv · 6 paks resource3d/ 166 xpr · 1.4 GB name_hash keys the TOC — case-insensitive Archive IPFB · .pak index + .p00….pNN data segments 12-byte TOC entry { name_hash, offset, comp_size } · Z1/zlib payloads 41 archives · 26 443 entries raw files (not archived) .wmv video · .xpr resource packages 263 files payload magic decides the format — nothing declares a type Container IDXDtables6325 T8aD2D tex4525 RATCUI bundle2985 IXUDtext1104 LSTAsprites64 SLBXMA19519 XBG7mesh6294 XPR2tex pkg166 TTFfonts54 ISBPRT · BIN no parser tag_hash / ixud_hash key records + fields — values are asset NAMES, hashed back up Domain Stage28 missions10 sub-tables Unit159 fields→ turret slot Arsenal59 items4-hop chain Audio5798 cues5 families Cutscene104 slots4 bindings UI screen965 buildsdecl + place Save545 bytesGDHA + zlib the save's 54-byte blob is indexed by strings.tbl order — not weapon.tbl Counts measured on the retail extract. Container counts exclude sound.pak and entries over 8 MB.
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 0.0 rather than as an error.

03 Archives where the mass actually sits

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.

ArchiveEntriesStoredWhat it is
sound.pak9 5191.07 GBevery XMA1 bank — music, jingles, SFX, both voice languages
GP_HANGAR_ARSENAL.pak1 53867.2 MB28 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.pak1 10667.6 MBlargest UI pak; ISL script bytecode + link map
GP_MAIN_GAME_*2D unnamed711 ea.15.2 MB ea.six paks, 0 % of names recoverable — see §06
DefTables.pak hidden/1 465definition 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.pak79330 KBthe registries: sound cues, the cutscene manifest
MiscBin.pak hidden/400 names resolved
fonts.pak32.4 MBthe three shipped typefaces
One entry lies about its size Static.slb, the sound-effect bank, sits at the highest offset in sound.pak and declares 616 768 bytes more than the disc holds. This is not a bad extract — sound.p04 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 only there.

04 Container formats what each blob turns into

MagicContentStateReach
IDXDself-describing record/field table — the game's whole data layerconfirmed7 750 / 7 750 objects
IXUDthe same container, UTF-16BE, offsets in charsconfirmed1 104 / 1 104 objects
T8aD2D texture — a list of arbitrary sub-rectangles, not a tile gridconfirmed19 216 / 19 216
RATCUI bundle; children are sprites, layout records and primitivesconfirmed10 144 / 10 148 refs
LSTAsprite display list — count covers T8aD and PRMDconfirmed64 / 64
XBG7mesh; index pool then vertex pool, layout declared per sub-mesh99.25 %6 247 / 6 294
XPR2texture package — de-tile, A8R8G8B8 and DXT1partialchannel order confirmed
SLBXACT bank of XMA1 sub-waves; two layouts, one headerless35/36 sharedJNGL_001 fails
GDHAsave file — zlib payload, chunk stream, round-trips byte-identically~11 ❔ fields545 bytes
TTF / OTFstock OpenTypeconfirmed54
ISBISL mission-script bytecodepartialbuiltins documented
PRTtelop — on-screen text overlay for cutscenesno parser22 bound
BINcollision meshes, CollisionSet_S<NN>.binno parserdocumented only

05 The domain chains what references what

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.

Mission

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"]

Arsenal — the four-hop trap

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.

01loadout record  — flight position × pilot, e.g. Rhino2-EllenNames Arm1 / Arm2 / Arm3 / Nose — which are not items either.
02per-slot allow-list recordIts only named field is Type; the candidate items live in its positional, unnamed fields, in order.
03arsenal item  → .PlayerWeaponResolves to Turret_050 — a slot on the player craft's own unit table. 59 of 59 do; 0 of 59 name a weapon.
04turret slot  → .WeaponIDAnd this is the Weapon record.

Audio

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"]
Two things the paths do not tell you The 36 music, jingle and SFX banks sit at the table root with no language component, so both sounds.tbl files name them. And a .slb 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.

Cutscene

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"]

UI screen

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.

Save

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.

The stale-summary trap The in-game Details panel reads a summary copy 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.

06 References that go nowhere a port has to survive these

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.

ReferenceWhereStatus
pmbase.t32named by pmbase.rat in all four GP_STAGE_CLEAR language buildsnowhere on disc
SUBTITLE_S12B.tblbound by the manifest, in all six languagesresolves in none
logo1…logo4, SYLPH_HD720p_8M-CBR_2chmanifest-bound moviesno .wmv
dat\GP_TEST\a debug archive the script manifest points atnot shipped
6 × GP_MAIN_GAME_*2D711 entries each; eleven other paks are at 100 % by the same method0 % named
GP_READY_ROOM.pakthe largest UI pak on the disc6 of 1 106 named
42 ISL script-symbol keyshash-only field keys in <lang>\script\ID.tblprovably unrecoverable
9 of 101 cutscenesno English transcript resolvesunexplained
JNGL_001.slbheaderless bank; payload is not a whole number of XMA1 packetsdoes not decode
Why the 2D paks matter more than they look Six archives at exactly 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.

07 Still open

  • UI paint order from the bundle. Solved at runtime, not statically — and static is what a port needs.
  • The .prt telop format. 22 cutscenes bind one; no parser exists.
  • Collision meshes. CollisionSet_S<NN>.bin is documented and unparsed.
  • 47 XBG7 resources still miss — mostly pose/proxy composites and damage variants, not a threshold away.
  • ~11 GHAD save fields unnamed, and difficulty-versus-stage undecided: three fields hold the value 2.
  • The naming of the 2D and Ready Room archives, which is the largest single block of unreachable content on the disc.