29 Commits

Author SHA1 Message Date
Sylpheed RE agent
27e8c51978 re: a keyframe time is worth half a rendered frame, and the ramp is linear
Q1 of the menu port, measured against the running game rather than reasoned
about. The developer-logo splash is the cheap target: it is the first thing the
guest draws and its bundle declares short, unambiguous ramps.

Two results, both frame-exact and both emulator-speed-independent (frame numbers
are VdSwap counts, the guest's own frames):

  * the ramp is LINEAR. A declared 15-unit fade lands on round(255*k/15) for all
    seven of its samples with zero error, k stepping 2,4,6,8,10,12,14. No ease
    can reproduce a constant step of 34 at both ends.
  * the animation clock advances 2.000 time units per submitted frame, over six
    consecutive intervals with no residual, with 1 unit as the quantum
    underneath (one frame in the fade-out advances by 1).

The conversion to seconds is one step further and is flagged as such: 300 frames
took 10.87 s = 27.6 present-frames/second, which reads as a 30 Hz title at 92 %
under the emulator and gives 1 unit = 1/60 s -- the title build 4.2 s, the main
menu build 1.1 s. That reading is not proven, because the rate was measured
while the guest was still streaming from the ISO; the page names the one test
that would settle it and says what changes if it goes the other way.

Committed beside it: the raw draw capture and the per-frame quad CSV, so the
numbers can be re-derived without a disc or an emulator.
2026-08-28 15:56:33 +00:00
Sylpheed RE agent
8b6dbcfead formats: move media assembly out of the viewer, where it could not be reused
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 8m42s
CI / WASM — Web (push) Failing after 7m33s
CI / Formatting (push) Failing after 1m15s
CI / Native — windows-latest (push) Has been cancelled
CI / Native — macos-latest (push) Has been cancelled
The trickiest reading on the disc lived in the Bevy viewer: resolving a
cutscene's voice to a continuous byte REGION of the sound stream, because the
movie voices are one XMA stream chunked into VOICE_*.slb entries whose
boundaries do not match the cues -- a cue routinely spans two chunks, so a .slb
need not hold the track its name claims.

That put the logic most likely to be re-derived incorrectly in the crate least
likely to be reused. The Godot port's exporter needs the same answers, and there
must be one implementation of them.

New `sylpheed_formats::media` owns every case where the bytes of one playable
thing are not one archive entry: segment-spanning reads, multi-sub-wave banks,
and the voice-region resolution. Callers supply bytes through a `DiscSource`
trait, so the viewer keeps its ISO/directory abstraction and a headless consumer
gets `DirectorySource` for free.

The seam is deliberate: this module returns XMA RIFFs, not PCM. Decoding means
shelling out to FFmpeg, which is native-only and a policy decision for the
consumer -- everything up to "here are the bytes that belong together" is disc
knowledge, everything after it is a codec choice.

The four moved functions were previously untested; `tests/media_disc.rs` now
pins them, including the negative the corpus paid for -- an unbound movie must
stay unvoiced rather than borrow a neighbour's clip, which was tried and played
the WRONG recording.

The algorithm is unchanged, moved verbatim (same window sizes, same fallbacks).
The new disc tests pass; the broader audio suite was not re-run in this pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:49:33 +02:00
Sylpheed RE agent
69291a315f port: add Q10 -- what a music bank's sub-waves actually are
Some checks failed
CI / Native — windows-latest (push) Has been cancelled
CI / WASM — Web (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Native — macos-latest (push) Has been cancelled
CI / Native — ubuntu-latest (push) Has been cancelled
BGM_001.slb is three sub-waves (10 KB, 4.47 MB, 4.67 MB) and the decoder
concatenates them into one 347 s track. That is a default nobody chose, not a
decision: two near-equal halves could be intro + loop, two variations, or two
halves of one piece, and a menu that loops its music needs to know which.

Found while wiring the Audio Library up to the shared banks. Recorded in
HANDOFF.md as a trap too, so the port does not build looping on top of the
concatenated track before the question is answered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:43:25 +02:00
Sylpheed RE agent
4734797e50 agent: stop the launcher claiming it cannot push when it can
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 8m23s
CI / WASM — Web (push) Failing after 7m9s
CI / Formatting (push) Failing after 1m12s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
The `loose` footer announced "cannot push -- no git credentials are mounted"
unconditionally. It was written before the credential mount existed and then
went stale, so with credentials correctly mounted it told the operator their
work would be lost -- the exact failure the mount was added to prevent.

It now reports what is actually true, and keeps the real warning for the case
where the file genuinely is missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:22:02 +02:00
Sylpheed RE agent
ecbd70f69a port: the container agent answers the questions, it does not build the port
Some checks failed
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
CI / WASM — Web (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Native — ubuntu-latest (push) Has been cancelled
Corrects the split I got wrong in b5a1938. The container is for reverse
engineering, now focused on the menu port; a SEPARATE agent builds the port from
its investigation results. My previous version had the container agent writing
the exporter and the Godot project, which is not the intent.

This lands on the research/engineering line that was already in the estimate:
the container agent takes the research half, the port agent the engineering half.

docs/port/MISSION.md is now a list of open QUESTIONS (Q1-Q9) rather than build
milestones, ordered by what blocks the port earliest -- the keyframe time unit,
which build is which screen state, paint order for the six screens, button ->
GamePart, navigation semantics, the boot sequence driver, transitions, menu
audio bindings, and video binding. Each is done when a written result with
evidence exists, not when something compiles. S1, the Ready Room probe, stays
gated at one iteration and a go/no-go.

Most of these are BEHAVIOUR questions -- timing, transitions, what a d-pad press
does at the end of a list -- so the mission and the loop prompt both push hard
on measuring the oracle rather than reasoning from the file.

docs/port/FORMAT.md is deleted. The export schema is the port agent's design and
was not mine to specify. It is replaced by docs/port/HANDOFF.md, the single page
the port agent reads: a status table, what is already settled and can be relied
on today, and the facts that will trip the port up (the WMV3/WMA Pro intro, the
Static.slb size over-declaration, the voice-vs-music downmix, JNGL_001).

The derived/authored idea survives as the thing it always was -- a finding, not
a design. Every answer must be classified DECODED, MEASURED or UNDECODABLE-with-
reach, and never a fourth thing, because measured and undecodable both mean the
port agent is authoring that value and has to know it. Labelling a guess as a
decode would put it into the port wearing the badge of a measurement.

Reverts the Godot install from the RE container, its AGENT.md section, and the
export/ gitignore entry -- none of that belongs on this side of the wall.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:16:47 +02:00
Sylpheed RE agent
b5a193839c port: make the Godot menu shell the agent's primary objective
Sets a new mission: boot the real disc through developer splash -> intro video
-> title -> main menu -> submenus in Godot 4, interactively, with no gameplay,
no 3D and no emulator.

docs/port/MISSION.md defines it -- eight gated milestones, each finished by an
ARTIFACT rather than by compiling, plus the Ready Room as an explicitly gated
stretch goal with a one-iteration probe that decides go/no-go. GP_READY_ROOM is
1106 entries with 6 recoverable names and is ISL-scripted, so it is either a
week or a quarter, and the agent must not start it on its own authority.

Architecture, per the user's decision: the Godot project is INDEPENDENT of the
Rust viewer and never reads a disc format. An offline Rust exporter converts the
disc into open formats; Godot reads only those. No GDExtension, no Rust in the
Godot project, and sylpheed-viewer is off limits -- it stays the human's
verification tool with its static-data rule intact.

docs/port/FORMAT.md specifies the open format, versioned, because modding is the
port's second goal and that makes the layout a deliverable rather than a temp
directory: JSON over XML (Godot parses JSON natively; its XMLParser is SAX),
names never hashes, provenance in every generated file, and unknowns listed
rather than guessed.

The discipline the whole thing rests on is the derived/authored split. `export/`
is regenerated wholesale and never hand-edited; `authored/` is hand-written and
survives a re-export. Three things this milestone needs are NOT on the disc in
any decoded form -- which button does what, paint order, and menu sound cues --
so they live in `authored/` with a stated `why`. Deleting an authored entry
because the exporter can now emit it IS the measure of progress.

`export/` is gitignored: it is generated from the user's own disc and this stays
a clean-room repo.

Container: adds a pinned Godot 4 (windowed under Xvfb for screenshots, plus a
headless wrapper). ffmpeg already carries libtheora, which is the video target --
Godot 4 plays only Ogg Theora natively and the disc's ADV.wmv is WMV3/WMA Pro.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:11:35 +02:00
Sylpheed RE agent
5cdff5e515 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
Sylpheed RE agent
77cd58202b viewer: a Cutscenes browser -- the manifest was invisible plumbing
`movie_manifest` has been parsed since the movie-voice work and rendered
nowhere: it resolved a voice bank and that was all. So the only way to find a
cutscene was to hunt `.wmv` files in the ISO tree, where nothing tells you which
mission a file belongs to, whether it has subtitles, or what is said in it.

View ▸ Cutscenes lists all 104 manifest slots with mission/phase, kind, movie,
subtitle track, voice token and telop, and -- the part that needed no new
parsing, only a route -- resolves the captions to a readable TRANSCRIPT with a
language selector. Subtitles were previously burned into the video during
playback and reachable no other way.

Three negatives are shown rather than smoothed over:

* 5 manifest-bound movies have no `.wmv` (logo1-4 and an encoder test clip).
  They are marked and get no Play button instead of one that would fail.
* 9 of 101 movies resolve no English transcript.
* the `.prt` telop overlay is named by the manifest and we have no parser, so
  the reference is shown labelled "not decoded" rather than omitted.

`cutscene_catalog_binds_movies_and_transcripts` pins all of it against the disc
-- 104/101/99/99/22, the exact absent-movie list, 92 transcripts -- because a
browser that quietly dropped these would look complete and be wrong. The counts
independently reproduce docs/re/movie-subtitle-link.md.

Play routes through the normal FileSelected path, so the existing video player
handles it exactly as it would from the tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:47:46 +02:00
Sylpheed RE agent
306a8a5661 viewer: open the whole sound bank, not just the voice half
The library enumerator kept only names containing VOICE or \Briefing\, and read
eng\sounds.tbl unconditionally. So the Explorer could reach 4382 of the 9519
banks in sound.pak: no music, no jingles, no sound effects, and no Japanese
voice at all -- roughly half the disc's audio had no route to the UI.

`slb::list_audio_entries` now returns every named bank with the category its
path implies (Music / Jingles / Sound effects / Radio / Dialogue / Movie voice /
Briefing). `list_voice_clips` is that, restricted to the spoken categories, so
its existing test still guards the old behaviour. The 36 root banks carry no
language component and appear whichever table is read; the window gets an
English/Japanese switch that re-reads the other sounds.tbl, since the table name
IS the selector.

Two defects the decode found, both recorded in
docs/re/structures/sound-pak-contents.md:

* `Static.slb` -- the SFX bank -- declares 616768 bytes more than sound.p04
  holds. Not our extraction: p04 matches the ISO's own directory record, and a
  sweep of every pak on the disc finds this one entry over-running and no other.
  It is the highest-offset entry, so its comp_size is an allocation size. A
  short read is now allowed for the tail entry ONLY; any other overrun stays an
  error, because clamping it would hide real damage behind a half-decoded asset.
  The bank went from unreadable to 514 s of audio.

* the left-channel downmix was applied to everything. Right for voice (mono
  content however stored), wrong for music (a real stereo mix, half of it
  discarded). The caller now decides from the category.

35 of the 36 shared banks decode; JNGL_001 does not, and says so in the player
instead of the panel silently closing. Its payload is not a whole number of XMA1
packets from any known data offset, so it is likely not a plain headerless
stream -- written up rather than papered over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:28:20 +02:00
Sylpheed RE agent
fb70511242 viewer: make the two screen presenters agree, and expose the compose options
The UI Screens window enumerated with `is_build` while the PAK browser's
inline preview composes anything `parse_build` accepts. So the browser drew
screens this window flatly refused to list -- most visibly `palogo`, the
publisher splash, which declares its sprites directly and has no `.rat`
layout child.

`compose_screen` now gates on `is_composable`, the documented superset (every
`is_build` bundle passes it), so anything the list offers is drawable and the
two presenters share one rule. A "Fragments" toggle widens the enumeration to
it as well, off by default: the extra ~1786 bundles are mostly two-element
fragments (a button beside its glow) that would bury the real screens. It
re-scans, which renumbers the display ordinal -- harmless, because the pak
ENTRY index is the locator and the stale catalog is cleared.

The toggle sets a `rescan` flag rather than self-sending RequestScreenCatalog:
a system that both reads and writes one event type is a B0002 panic at
startup, which is how the Save browser broke.

Also plumbs the last two ComposeOptions the CLI had and the UI did not --
`black backdrop` (what a framebuffer capture must be compared against) and
`primitives` (decoded, but paint order unsolved; hover text says so).

Verified: `screen render --all --build 13 GP_TITLE.pak` draws the SQUARE ENIX
splash, 2/3 elements, the third being the .prm quad. Workspace builds, viewer
reaches steady state, disc-gated suite 20/20 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:14:23 +02:00
Sylpheed RE agent
47e4e43310 viewer: stop the UI Screens browser loading forever
Three compounding causes, found by tracing every place a `loading` flag is set
against every place it is cleared.

THE COST. `compose_screen` inflated EVERY entry in the pak and held every build
in memory at once, purely to index into the result -- then did it again on every
checkbox click, re-opening the ISO from scratch each time. But the catalog
already records each build's pak ENTRY INDEX, so the locator was there all
along: compose now reads exactly one entry. `RequestScreenCompose` carries
`entry` (the locator) beside `build` (the display ordinal).

`build_screen_catalog` had no budget at all, while the pak browser has capped
exactly this work since it was written. It now skips oversized entries and stops
at a ceiling.

That ceiling is 384 MB, not the pak browser's 64 MB, and the difference is the
point: GP_HANGAR_ARSENAL inflates past 160 MB and holds ~390 builds, so a 64 MB
cap would have quietly hidden most of them -- trading a hang for a wrong answer.
When the ceiling IS hit the pak is marked `truncated` and the UI says so.

THE LATCH. `poll_loader_channel` treated a disconnected channel exactly like an
empty one, so if a worker died every in-flight spinner stayed up for the life of
the process -- and the `if loading { return }` guard at the top of each handler
then refused every retry. Disconnect now clears the flags and reports it.

Verified: the workspace builds, the viewer reaches steady state, and one
composite still draws 11/11 elements of the tutorial pause menu.
2026-08-28 15:59:00 +02:00
Sylpheed RE agent
1054d43676 docs/re: finish the INDEX merge I left unresolved
Commit 03ece95 ("Merge branch 'auto/idxd-unnamed-keys'") shipped INDEX.md with
live conflict markers. My check was worthless in a way worth naming: I ran

  git add docs/re/INDEX.md ; git status --short | grep -E '^(UU|AA)'

in that order. Staging a conflicted file is exactly what marks it resolved, so
the grep found nothing and reported "no unmerged" -- the control passed BECAUSE
of the action it was meant to gate. Both halves of the hunk then went into the
tree verbatim, leaving two competing IDXD rows.

Resolved as a union, which is what it always was:
  * HEAD's "IDXD object/table" row -- the 2026-08-25 record/index-region decode
    -- supersedes the branch's older one-line version of the same row;
  * the branch's "IDXD nameless field keys" row is new and is kept.
2026-08-28 15:55:18 +02:00
Sylpheed RE agent
e1dcc689bc agent: move the loop prompt's memory into the corpus, and let it push
The autonomous agent's loop prompt had grown to ~9 000 words of accumulated
findings, refutations and traps. That is a symptom, not a style: it was doing
the job the corpus should do, in the one place that dies with the container.
Three concrete failures followed from it, and each gets a structural fix rather
than a louder instruction.

1. IT REPEATED WORK IT HAD ALREADY DONE.
   The "do not revive" list and the method traps existed ONLY in the prompt, so
   nothing pointed a fresh iteration at them. Extracted verbatim into two
   tracked files:

     docs/re/REFUTED.md  105 claims tested and dead, grouped by subject so a
                         grep for your noun finds the neighbourhood
     docs/re/METHOD.md   the traps already paid for -- controls, inference,
                         searching, reading data, runtime

   Both are linked from INDEX.md, and the loop prompt now opens by requiring
   them to be read. This is the fix for "re-derived something already known":
   the knowledge is now where the next iteration looks, not in a context window.

2. IT FORGOT TO ARM THE NEXT WAKEUP.
   The prompt tried to solve this by shouting at itself in the first line. The
   real fix is to stop asking: `loose` now defaults to a FIXED interval (45m),
   so the harness owns the cadence and a forgotten ScheduleWakeup cannot end the
   run. SYLPH_LOOP_INTERVAL= (empty) restores self-pacing.

3. IT COULD NOT PUBLISH, SO THE WORK ONLY EXISTED IN THE CONTAINER.
   New `push-work`, plus a read-only credentials mount
   (SYLPH_GIT_CREDENTIALS, default ~/.sylph-git-credentials). It pushes the
   CURRENT branch only, refuses anything that is not auto/*, and never
   force-pushes -- so the consolidated line stays a human's decision and a
   confused iteration cannot rewrite history. The loop prompt now requires a
   push on every iteration that commits, rather than at the end of some longer
   arc, which is exactly when a container dies.

The prompt itself drops from ~9 000 words to 85 lines and, more importantly,
stops accumulating: findings go in the corpus, and the prompt points at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 15:46:59 +02:00
Sylpheed RE agent
03ece95c06 Merge branch 'auto/idxd-unnamed-keys'
# Conflicts:
#	docs/re/INDEX.md
2026-08-28 15:27:51 +02:00
Sylpheed RE agent
1fce3f9c71 Merge branch 'auto/slb-loader'
# Conflicts:
#	docs/re/INDEX.md
#	docs/re/structures/slb-data-offset.md
2026-08-28 15:27:46 +02:00
Sylpheed RE agent
8124b85fc7 Merge branch 'auto/regn-reader'
# Conflicts:
#	docs/re/BACKLOG.md
#	docs/re/INDEX.md
#	docs/re/structures/regn-map-grid.md
#	tools/re-capture/regn_decode.py
2026-08-28 15:26:35 +02:00
Sylpheed RE agent
245b73243b Merge branch 'auto/isl-builtins-26-28-29'
# Conflicts:
#	docs/re/INDEX.md
#	tools/re-capture/isl.py
2026-08-28 15:24:55 +02:00
Sylpheed RE agent
0cbef2023f re: withdraw "everything was read 16 bytes early" — the planes were right
The coordinator refuted the broad form of my claim and was correct to. The
older reading put section 2 at chunk+offset with the normal at intra-record
+16, which addresses exactly the same bytes as chunk+0x10+offset with the
normal at +0: on 3506e972 both put face record 0's normal at 0x1c700. Its
unit-normal and n·p+d results were measuring real planes and stand.

What is actually wrong is the record BOUNDARY, and it decides which record the
four integer words belong to — the face adjacency, i.e. the field the whole
correlation search was looking for. Settled: requiring own==f and
tet[A].faces[i]==f gives 100.000% with the block from record f and 0.000% from
record f+1, on all 11 objects.

Also audits my own numbers for the same defect the coordinator found in the
point-in-bbox test: most of them are content tests that address identical bytes
under either convention and have no power over the origin. The four that do
have power are listed, including that POF0 at base+0 would relocate the u16
counts and leave the section pointers unrelocated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 07:58:45 +00:00
Sylpheed RE agent
50625b5a9e re: REGN is a tetrahedral navigation mesh — the cell→geometry link, decoded
Found the reader. It is the deserialiser, not a consumer, and it answers the
question twice: the fixup base is chunk+0x10 (82465198 addi r3,r31,16), so
every offset previously recorded was read 16 bytes early — which is why twenty
correlation tests sat at chance — and the POF0 table names every pointer word
in the file.

Six sections, not four. cell {count,item*} → item {n@+0x10, refs*@+0x14} →
array of pointers into section 1 → a 96-byte tetrahedron. Section 2 is a face:
plane, its three vertices, the two tetrahedra either side (0xFFFF = hull) and
their face slots.

All 11 objects: face passes through exactly 3 of its tet's 4 vertices in
253 722/253 722 (random control 0.07–2.2 %); portal cost == face-centroid
distance in 380 460/380 460; sphere reaches its cell 98.7–100 % vs 18–28 %
with transposed axes.

Refuted and kept: 'REGN'/'MCOL' are never built as constants in the executable
(0x474E occurs zero times in 1.87 M instructions), so no magic-dispatch site
exists; and "zero portal-pair float marks a hull edge" shows no lift at all.
Still open: the runtime consumer of the grid, the second portal float, and the
four flag bytes at tetrahedron +0x54.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 07:53:13 +00:00
Sylpheed RE agent
e5c6c27e6a re: regn_decode.py — read a REGN object through its own POF0 fixup table
The retail deserialiser (sub_82465110 / sub_82465138 / sub_82465200) relocates
a chunk with a fixup base of chunk+0x10, and the POF0 table it walks is an
exact list of which words are pointers. Decoding that table gives the pointer
graph with no guessing.

`verify` reproduces every number quoted in the doc, each against a control:
pointer-slot shape, reference-array packing, face/vertex incidence vs a random
face, cell agreement vs transposed axes, and the portal-cost identity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 07:53:13 +00:00
Claude
a7d5a3bd12 re: the .slb data offset is the .pNN segment's 2048 grid phase
Closes the open question at the bottom of slb-data-offset.md.

  X = (cumulative start of the .pNN segment holding the wave) mod 2048

The XMA packet grid is 2048-aligned inside each individual segment file,
but the .pak TOC addresses entries in the flat concatenation at offsets
that are themselves multiples of 2048. The segment files are not multiples
of 2048 long, so each join shifts the grid by size % 2048 — and the four
disc-wide values are exactly the running sums:

  1392 = |p00| % 2048;  1468 = +76;  1600 = +132;  1728 = +128

Exact for 7620/7620 banks with a RIFF and 1163/1163 RIFF-less ones via
their seek chunk, 0 mismatches. Supersedes both heuristics (the 99.62 %
packet scan and the 99.97 % seek-residue rule) and dissolves the 28 ties.
The refutation test — an entry straddling a segment join must show two
phases in one file — passes on all 3 straddlers.

The leading bytes are the previous bank's audio, not a header: byte
diversity per offset is indistinguishable from a known packet (101.06 vs
101.90, no fixed field anywhere), the seek packet counts chain exactly
across consecutive entries, and the inter-entry bytes no TOC entry claims
are 1903/1928 non-zero.

Also recorded: the real bank header layout (id, block size 0x800, header
size in blocks, XMAWAVEFORMAT), and the loader search — a null result.
None of the four values exists as an immediate, a table or a float
anywhere in default.xex, which is what a pack-time artifact predicts.
Sound subsystem addresses mapped for the next pass.

Withdraws the 🟡 "most banks declare more data than they store" finding:
declared data sizes are exact (260/260), the bytes are just outside the
TOC window. Also withdraws my own "the header is unique, so nothing is
shared" inference — the windows tile, they do not overlap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 05:01:26 +00:00
Claude
1f4524f9fe tools: slb_segment_phase.py — derive the .slb data offset exactly
Static, disc-only probe for why a .slb bank's XMA1 stream starts at
1392 / 1468 / 1600 / 1728 bytes in. Subcommands:

  phases  the sound.p00..p04 size table and the running sums mod 2048
  verify  predicted vs measured offset over every bank on the disc
  bank    structural dump of one entry (bank headers, waves, seek chunks)
  chain   the tiling arithmetic across consecutive entries

Reads nothing but dat/sound.pak, dat/sound.p00..p04 and tables.pak.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 05:01:26 +00:00
Sylpheed RE agent
3b67a451c2 tools(isl): apply the three new built-in names, with 28 flagged as probable
26 damage_unit -> set_unit_hp_pct, 29 -> set_unit_damage_taken_pct,
101 -> all_units_invulnerable, and 28 -> set_unit_damage_dealt_pct with an
inline note that its label rests on a single consumer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 03:43:56 +00:00
Sylpheed RE agent
fa3818c58c re: ISL built-ins 26 / 29 / 101 named end to end; 28's field pinned, label 🟡
The three unnamed built-ins are one family, and the chain from the dispatch
table to the field write is now followed for all of them:

  builtin -> ScriptPhase vtable slot -> interpreter command word 0xAB<op>BA
          -> command-table thunk -> opcode handler -> unit message 0xED08nnDE
          -> the GROUP pump sub_8232C4C0, which rebroadcasts to each child as
             0xED09nnDE
          -> the entity base handler sub_82398CC0, which writes the field.

  26 -> [unit+532]  = min(max(def.HP * pct, 0), def.HP)   = set_unit_hp_pct
  29 -> [unit+676]  = pct, a multiplier on damage TAKEN   = set_unit_damage_taken_pct
  28 -> [unit+672]  = pct, default 1.0                    = 🟡 damage DEALT
 101 -> the same message as 29 with a hard-wired 0.0, broadcast to every unit
        = all_units_invulnerable

`damage_unit` is WITHDRAWN for 26. The handler sets an absolute value rather
than subtracting one, and 100 heals to full -- which no damage primitive does.
It is pinned three ways: [unit+496] is the unit definition (the constructor
sub_82393868 fills it from the same std::map::find built-in 15 uses), [def+84]
is HP in unit_definition_layout.txt and the constructor seeds [unit+532] from
it, and crossing zero loads [def+584] = Delay and raises a flag, i.e. the
destruction sequence. So 0 destroys, with the datasheet's own death delay.

29 is the strongest of the three: [unit+676] has three independent readers
(sub_8237B020, sub_823800A8, sub_82398CC0) and every one multiplies a damage
amount immediately before it reduces [unit+532].

28 is deliberately left 🟡. The write and the 1.0 default are certain, but the
field has exactly ONE reader in the whole image -- the craft update's projectile
spawn, where it ends up as a multiplicative term in the damage message. That is
the mirror of 29 and it is tidy, which is exactly the shape that produced the
wrong names this file has already had to withdraw. What is not established is
that it reaches every weapon; the sibling damage sender sub_82388FF8 has no
+672 term at all.

Three usage tests, all measured over the 28 stages:
  * operand ceilings -- 26 is 97/97 inside [0,100] and 29 is 164/164, while 28
    (identical signature, identical x0.01 conversion) reaches 2000;
  * the craft cross-tab -- 26 splits cleanly into disposable props at 0,
    warships at 30-80 and the tutorial player craft at 100; 29 lands on the
    player, the tutorial boxes and the escorted TCAF hulls; 28 orders
    boss > ace > elite > line > prop;
  * the setup idiom -- activate_unit, then 15/29/28 as a speed/toughness/
    firepower trio, with 26 added wherever a unit must arrive pre-damaged.

Refutations attempted are recorded, including the two that turned into
confirmations (Stage 28 makes each tutorial box invulnerable with 29 and then
removes it with 26) and the offset-search trap that produced three false
readers, because projectiles have their own fields at 672 and 676.

Also corrected: the state guards. 26 rejects states 3 and 4; 28 and 29 reject
1, 3 and 4. This file said otherwise for both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 03:43:56 +00:00
Sylpheed RE agent
1ba16da9a4 re(data): every call site of built-ins 26, 28, 29 and 101, with craft types
804 rows across all 28 stages, each joined to its squadron's craft through
`UnitGroup_S<NN>.tbl` -- 671/671 of the three unit-taking built-ins resolve,
none unknown. Regenerable from the committed tool.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 03:43:31 +00:00
Sylpheed RE agent
e9c06e2e87 tools: an ISL call-site dumper that carries the operands and the neighbours
`isl.py --calls` scans on the encoding, so it can see that a call happened but
not what it was called with -- the arguments live in the `local[]` staging of
the preceding instructions, not in the call. Naming a built-in needs those, and
it needs the calls either side, because the fixed idioms are what identify a
primitive (`116 -> 101 -> 100 -> 124 -> 93` is a phase teardown, and that is how
101 was named).

So this walks each stage's whole code region linearly, tracking both staging
forms, and prints stage/offset/id/operands plus the neighbouring call ids.
The linear walk is safe rather than assumed: it lands on every call site the
independent encoding scan finds, 2846/2846 in Stage 02.

`--craft` cross-tabs the operand against the squadron's craft type through
`unitgroup.py --all`, which is the test that separated built-in 15's classes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 03:43:31 +00:00
Sylpheed RE agent
eb75b62f82 re: INDEX row for the nameless IDXD field keys
Records the census, the 504-entries-not-504-names correction, and the null
result on cracking with the arithmetic that makes it a null result rather
than a shrug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 03:37:05 +00:00
Sylpheed RE agent
e1e4f7bc1d re: the 42 hash-keyed nameless fields are an ISL script link map — names not cracked
All 42 live in six byte-identical copies of <lang>\script\ID.tbl inside
GP_READY_ROOM.pak, the artefact of the pass the executable announces as
"PrepareScript - isl script prescanning start". Two records, FILE and
OFFSET, carry the same 42 keys: symbol hash -> (.isb file, offset).

Confirmed independently, not inferred: 41 of the 42 keys occur verbatim as
little-endian words inside the .isb bytecode, and where they occur is a
coherent call graph -- main.isb calls the 30 stage/challenge/tutorial
symbols, those call the five helpers in function.isb, the tutorials call the
six in function_tutorial.isb, and main.isb's own symbol is called by nothing.
Ordinals do not appear as call targets in code; hashes do.

30 of the 42 names have their last characters PINNED by the hash's own
algebra. Bumping the character k places from the end moves the low 24 bits
by 256^k mod M and the top byte by 1, and the measured deltas are exactly
+0x01000001 across stage01..09, stage10..16 and challenge01..06, and
+0x01010000 across tutorial0101..0601. So those names end in the same digits
as their .isb filename. The prefixes are not recovered.

NULL RESULT on the names, and quantified rather than asserted. Seven attacks
(1.2M disc + executable strings, printf substitution, a hand guess list,
two-token composition over a mined vocabulary at S = 3.5e8, exhaustive
meet-in-the-middle preimage search) produced zero hits above the noise floor.
The search is not broken -- fed tag_hash("Stage01") it returns "Stage01" plus
three collisions, as 63^6 / 2^32 predicts. It returns nothing for the real
targets at <=6 characters, so the prefixes are longer, and at 7 characters
one target already has 1 176 measured preimages. A 24-bit modulus cannot
name an 8+ character identifier uniquely; only a corpus holding the actual
string can, and the disc does not hold it.

Refutations kept rather than deleted: the filename-stem hypothesis (direct),
the shared-prefix-plus-stem hypothesis (algebraic -- A's byte sum is pinned
twice and the two values disagree, 9 pairs, 9 contradictions), names inside
the .isb payloads (there are no ASCII names there at all), the hash constants
sitting in the executable (absent in both endiannesses), and IXUD holding
more of the same (8 unnamed tags, all ordinals 0..7).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 03:36:40 +00:00
Sylpheed RE agent
8af6a3f5d5 re: measure the nameless IDXD field keys — 504 is 504 entries, not 504 names
Walks every IDXD object on the disc (7 750 objects, 190 782 records,
2 757 039 field entries, zero parse failures) and separates the field keys
that carry no name in the pool from those that do.

The headline correction: the "504 fields disc-wide that are hash-keyed with
no name" is a count of field ENTRIES. It is 42 distinct keys x 6 language
copies of one object x 2 records (FILE and OFFSET) = 504. The brute-force
target was never 504 names.

7 094 distinct keys are never named anywhere. 7 052 of them are not hashes
at all: they are author-assigned element ids, equal to the field's own index
in 1 404 924 of 1 485 577 cases and hand-numbered with gaps elsewhere.
Refuted directly rather than assumed -- tag_hash("BGM_001") is 0xC662435B
while the key of the field valued "BGM_001.slb" is 0x000003E9.

The split is measured, not stipulated: every key that DOES carry a name and
has a zero checksum byte sits at 0x0002677C or above, the ordinal band tops
out at 0x2198, and not one key falls in the gap between.

tag_hash reproduces 1 271 462 / 1 271 462 named field keys disc-wide
(`idxd_unnamed_keys.py selftest`), which is the gate everything else rests on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-26 03:36:23 +00:00
34 changed files with 16765 additions and 541 deletions

View File

@@ -67,6 +67,10 @@ pub mod movie_manifest;
pub mod movie_voice;
/// Assembling media whose bytes are not one archive entry — segment-spanning
/// reads, multi-sub-wave banks, and the continuous cutscene-voice stream.
pub mod media;
pub mod game_data;
pub mod localization;

View File

@@ -0,0 +1,305 @@
//! Assembling media that does **not** sit in one place on the disc.
//!
//! Most assets are one archive entry and are read with [`crate::pak`] alone.
//! Audio is not, and this module owns every case where the bytes of one playable
//! thing have to be gathered from somewhere other than a single entry:
//!
//! * **An entry spans segment files.** A `.pak` TOC offset addresses the
//! *concatenated* `.p00….pNN` stream, so one entry routinely straddles two
//! files on disc. [`DiscSource::read_segment_range`] is the seam for that.
//! * **A bank holds several sub-waves.** A `.slb` is an XACT bank; its sub-waves
//! are either alternate takes or sequential segments of one line, and only
//! concatenating them all and clamping to the known length gets both right.
//! * **A cutscene voice is not in its own bank.** The movie voices are one
//! continuous XMA stream chunked into `VOICE_*.slb` TOC entries whose
//! boundaries do **not** match the cutscene cues. A cue routinely spans two
//! chunks, so *a `.slb` need not hold the track its name claims*.
//! [`resolve_movie_voice_region`] resolves a movie to a byte region of the
//! stream instead, which is the only reading that produces the right audio.
//!
//! ## Why this lives in `sylpheed-formats` and not in a viewer
//!
//! It used to live in the Bevy viewer, which meant the one piece of logic most
//! likely to be re-derived incorrectly was in the crate least likely to be
//! reused. Anything that reads the disc — the viewer, a CLI, an asset exporter
//! for a port — needs the same answers, and there must be one implementation of
//! them.
//!
//! ## What deliberately stays out
//!
//! Decoding. This module returns **XMA `RIFF`s**, not PCM: turning XMA into
//! samples means shelling out to FFmpeg, which is a native-only dependency and
//! a policy decision for the consumer. The seam is "here are the bytes that
//! belong together" — everything up to that point is disc knowledge, everything
//! after it is a codec choice.
use crate::pak::PakArchive;
use crate::slb::VoiceLang;
/// Where disc bytes come from. Implemented over an extracted directory, an ISO,
/// or anything else that can serve the same three questions.
///
/// It is a trait rather than a concrete type because the callers differ in ways
/// this module should not know about: a viewer reads from whichever source the
/// user opened, a headless exporter reads from a fixed extract, and a test reads
/// from a fixture.
pub trait DiscSource {
/// Read a whole file by disc-relative path, e.g. `dat/sound.pak`.
fn read_file(&self, path: &str) -> Result<Vec<u8>, String>;
/// Open an IPFB archive by disc-relative path, with its `.pNN` segments.
fn open_pak(&self, path: &str) -> Result<PakArchive, String>;
/// Read `len` bytes at `offset` into the concatenated `<stem>.p00….pNN`
/// stream, where `stem` is a disc-relative path without extension
/// (`dat/sound`). The range may cross a segment boundary; that is the point.
fn read_segment_range(&self, stem: &str, offset: u64, len: usize) -> Result<Vec<u8>, String>;
}
/// Read one `sound.pak` bank by name-hash, taking only its byte range from the
/// segments rather than inflating the 1.07 GB archive.
pub fn read_sound_bank<S: DiscSource + ?Sized>(
source: &S,
name_hash: u32,
) -> Result<Vec<u8>, String> {
let toc = source.read_file("dat/sound.pak")?;
let entries = PakArchive::parse_toc(&toc).map_err(|e| e.to_string())?;
let idx = entries
.binary_search_by_key(&name_hash, |e| e.name_hash)
.map_err(|_| "not present in sound.pak".to_string())?;
let e = &entries[idx];
source.read_segment_range("dat/sound", e.offset as u64, e.comp_size as usize)
}
/// The XMA `RIFF`s of one named bank, in the order they must be concatenated.
///
/// Every sub-wave is returned, not just the first. The two bank shapes need
/// this for opposite reasons: a **segment** bank (`VOICE_RT07A` = 24 s + 14 s +
/// 11 s ≈ the 50 s movie) is only complete when all of them are joined, and an
/// **alternate-take** bank (`VOICE_S00A`, whose sub-wave 0 already spans the
/// whole movie) is trimmed by the caller's length clamp. Taking sub-wave 0 alone
/// dropped two thirds of the dialogue on segment banks — that was a real bug.
pub fn sound_bank_riffs<S: DiscSource + ?Sized>(
source: &S,
clip_name: &str,
) -> Result<Vec<Vec<u8>>, String> {
let bytes = read_sound_bank(source, crate::hash::name_hash(clip_name))?;
Ok(riffs_of(&bytes))
}
/// The XMA `RIFF`s of a continuous byte region of the voice stream, as returned
/// by [`resolve_movie_voice_region`].
pub fn voice_region_riffs<S: DiscSource + ?Sized>(
source: &S,
start: u64,
end: u64,
) -> Result<Vec<Vec<u8>>, String> {
let bytes = source.read_segment_range("dat/sound", start, (end - start) as usize)?;
Ok(riffs_of(&bytes))
}
/// Sub-wave `RIFF`s of a bank's bytes, with the single-stream fallback.
///
/// Some banks — the data-before-header `\etc\` radio clips — defeat the
/// multi-sub-wave scanner, and the robust single-stream reader handles them. An
/// empty result here means genuinely undecodable, not "scanner confused".
fn riffs_of(bytes: &[u8]) -> Vec<Vec<u8>> {
let riffs = crate::slb::to_xma_riffs(bytes);
if riffs.is_empty() {
crate::slb::to_xma_riff_best(bytes).into_iter().collect()
} else {
riffs
}
}
/// Resolve a movie's voice bank **name** through the manifest in `tables.pak`.
///
/// Only the manifest's DIRECT bindings are trusted. Extending this to unbound
/// resupply movies by shared demo line was tried and verified WRONG — it played
/// the wrong recording — so an unbound movie stays unvoiced rather than play a
/// guess. `None` therefore means "this cutscene has no voice-over", which is a
/// real answer for most `hokyu_*` movies.
pub fn resolve_movie_voice_clip<S: DiscSource + ?Sized>(
source: &S,
movie: &str,
lang: VoiceLang,
) -> Option<String> {
let pak = source.open_pak("dat/tables.pak").ok()?;
let manifest = find_manifest(&pak)?;
let sounds = pak
.read_by_name(&format!("{}\\sounds.tbl", lang.code_pub()))?
.ok()?;
crate::movie_manifest::resolve_voice_entry(&manifest, &sounds, movie, lang)
}
/// The manifest has no stable name, so it is found by shape among the entries.
fn find_manifest(pak: &PakArchive) -> Option<Vec<u8>> {
pak.entries().iter().find_map(|e| {
pak.read(e)
.ok()
.filter(|b| crate::movie_manifest::is_manifest(b))
})
}
/// Voice token for a hokyu (resupply) cutscene the manifest leaves unbound.
///
/// Only 5 of the 18 hokyu movies carry an explicit `VOICETRACK`; the rest reuse
/// those recordings. The selector is the cutscene's **demo id** (from its
/// subtitle track), NOT the ship category: `hokyu_LS_s02A` and `hokyu_LS_s11A`
/// are both LS/carrier but use demos 600 vs 601, whose lines differ. So the map
/// is derived from the 5 bound hokyu — each of which has both a subtitle demo id
/// and a `VOICETRACK` — and the target movie's demo id is looked up in it.
pub fn hokyu_voice_token<S: DiscSource + ?Sized>(
source: &S,
movie: &str,
lang: VoiceLang,
manifest: &[u8],
) -> Option<String> {
use crate::movie_subtitle as ms;
if !movie.starts_with("hokyu_") {
return None;
}
let lang_pak = source
.open_pak(&format!("dat/movie/{}.pak", lang.code_pub()))
.ok()?;
let want = ms::track_voice_cues(&lang_pak, movie).first().map(|&(d, _)| d)?;
crate::movie_manifest::parse(manifest)
.into_iter()
.find_map(|e| {
let tok = e.voice_token.filter(|_| e.movie.starts_with("hokyu_"))?;
ms::track_voice_cues(&lang_pak, &e.movie)
.iter()
.any(|&(d, _)| d == want)
.then_some(tok)
})
}
/// Resolve a movie's cutscene voice to a continuous `[start, end)` byte region
/// of the voice stream — **the reading that produces the right audio**.
///
/// The chain is movie → cue token (manifest) → sound id (master registry) →
/// region (scan the stream for two trailers). Each cue ends at an inline
/// `(sound_id, 0x11, …)` trailer, so cue *N* is the bytes between trailer *N-1*
/// and trailer *N*.
///
/// Returns `None` for movies whose voice is not a `\Movie\` bank — the hokyu
/// `\etc\` clips — which the caller then resolves the per-clip way via
/// [`resolve_movie_voice_clip`].
pub fn resolve_movie_voice_region<S: DiscSource + ?Sized>(
source: &S,
movie: &str,
lang: VoiceLang,
) -> Option<(u64, u64)> {
use crate::{hash::name_hash, movie_manifest, movie_voice};
let code = lang.code_pub();
let tpak = source.open_pak("dat/tables.pak").ok()?;
let manifest = find_manifest(&tpak)?;
let token = movie_manifest::voice_token(&manifest, movie)
.or_else(|| hokyu_voice_token(source, movie, lang, &manifest))?;
// token → sound id, via the large per-language IDXD entry carrying the
// `<lang>\Movie\VOICE_*.slb` paths. Located by content, like the manifest.
let marker = format!("{code}\\Movie\\VOICE_ADV.slb");
let registry = tpak.entries().iter().find_map(|e| {
tpak.read(e)
.ok()
.filter(|b| b.windows(marker.len()).any(|w| w == marker.as_bytes()))
})?;
let id = *movie_voice::registry_voice_ids(&registry).get(&token)?;
// Physical anchor: the TOC offset of this token's own `.slb` chunk. That is a
// start point NEAR the cue's trailers, not the cue itself — the cue may sit
// before or after it, which is the whole reason a region is needed. The
// token's subdirectory varies by kind.
let stoc = source.read_file("dat/sound.pak").ok()?;
let entries = PakArchive::parse_toc(&stoc).ok()?;
let anchor = ["Movie", "etc", "Voice"].iter().find_map(|dir| {
let h = name_hash(&format!("{code}\\{dir}\\{token}.slb"));
entries
.binary_search_by_key(&h, |e| e.name_hash)
.ok()
.map(|i| entries[i].offset as u64)
})?;
// Scan both directions from the anchor. The window must span the largest
// bank (ADV ≈ 3.6 MB) or the predecessor trailer falls outside it.
let win_start = anchor.saturating_sub(2 * 1024 * 1024) & !3;
let window = source
.read_segment_range("dat/sound", win_start, 8 * 1024 * 1024)
.ok()?;
let end_local = movie_voice::find_descriptor(&window, id)?;
let end = win_start + end_local as u64;
// Start = the predecessor trailer. Prefer the exact `id-1`; where the id
// sequence has a gap (VOICE_D_453 → 454) fall back to the nearest trailer
// below — but only within one bank (~1.5 MB), else this is the first cue in
// its block and the audio starts at the anchor itself.
let start = movie_voice::find_descriptor(&window, id.wrapping_sub(1))
.or_else(|| movie_voice::find_descriptor_before(&window, end_local))
.map(|o| win_start + o as u64)
.filter(|&s| s < end && end - s < 1_500_000)
.unwrap_or(anchor);
Some((start, end))
}
/// A [`DiscSource`] over an **extracted** disc directory.
///
/// Provided here rather than left to each caller because every headless
/// consumer — the CLI, the disc tests, an asset exporter for a port — wants
/// exactly this and would otherwise re-derive the segment-spanning read, which
/// is the part that is easy to get subtly wrong.
#[cfg(not(target_arch = "wasm32"))]
pub struct DirectorySource {
root: std::path::PathBuf,
}
#[cfg(not(target_arch = "wasm32"))]
impl DirectorySource {
pub fn new(root: impl Into<std::path::PathBuf>) -> Self {
Self { root: root.into() }
}
}
#[cfg(not(target_arch = "wasm32"))]
impl DiscSource for DirectorySource {
fn read_file(&self, path: &str) -> Result<Vec<u8>, String> {
std::fs::read(self.root.join(path)).map_err(|e| format!("{path}: {e}"))
}
fn open_pak(&self, path: &str) -> Result<PakArchive, String> {
PakArchive::open(self.root.join(path)).map_err(|e| format!("{path}: {e}"))
}
/// Walks `<stem>.p00`, `.p01`, … skipping whole segments until the offset is
/// inside one, then reads across as many as the length needs. A range that
/// straddles a boundary is the normal case, not an edge case.
fn read_segment_range(&self, stem: &str, offset: u64, len: usize) -> Result<Vec<u8>, String> {
use std::io::{Read, Seek, SeekFrom};
let mut out = Vec::with_capacity(len);
let (mut skip, mut need) = (offset, len);
for i in 0..100u32 {
if need == 0 {
break;
}
let path = self.root.join(format!("{stem}.p{i:02}"));
let Ok(meta) = std::fs::metadata(&path) else { break };
let seg_len = meta.len();
if skip >= seg_len {
skip -= seg_len;
continue;
}
let mut f = std::fs::File::open(&path).map_err(|e| e.to_string())?;
f.seek(SeekFrom::Start(skip)).map_err(|e| e.to_string())?;
let take = need.min((seg_len - skip) as usize);
let start = out.len();
out.resize(start + take, 0);
f.read_exact(&mut out[start..]).map_err(|e| e.to_string())?;
need -= take;
skip = 0;
}
if need != 0 {
return Err(format!("segment range short by {need} bytes"));
}
Ok(out)
}
}

View File

@@ -25,8 +25,10 @@ use crate::pak::PakArchive;
/// Subtitle language. `pak_code` selects `dat/movie/<code>.pak`; `game_code`
/// selects `dat/GP_MAIN_GAME_<code>.pak` (the caption text).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SubLang {
/// Default only because the disc's own default is English.
#[default]
English,
Japanese,
German,

View File

@@ -225,16 +225,34 @@ impl PakArchive {
}
/// The raw stored bytes for an entry (still `"Z1"`-wrapped / compressed).
///
/// One entry on the retail disc declares more bytes than the segments hold:
/// `sound.pak`'s `Static.slb` (the SFX bank) claims 8 970 240 bytes at the
/// highest offset in the archive, 616 768 past the end of `sound.p04`. It is
/// not corruption and it is not our extraction — `sound.p04` is byte-for-byte
/// the size the ISO's own directory record gives, and a sweep of **every**
/// `.pak` on the disc finds this one entry and no other. So the last entry's
/// `comp_size` is an allocation size, not a stored size.
///
/// A short read is therefore allowed **only** for the highest-offset entry,
/// which is the shape the evidence supports. Any other overrun is still an
/// error: that would be real damage, and clamping it would hide the damage
/// behind a half-decoded asset.
pub fn stored_bytes(&self, entry: &PakEntry) -> Result<&[u8], PakError> {
let start = entry.offset as usize;
let end = start + entry.comp_size as usize;
self.data
.get(start..end)
.ok_or(PakError::OffsetOutOfRange {
offset: entry.offset,
size: entry.comp_size,
data_len: self.data.len(),
})
if let Some(b) = self.data.get(start..end) {
return Ok(b);
}
let is_tail = self.entries.iter().all(|e| e.offset <= entry.offset);
if is_tail && start < self.data.len() {
return Ok(&self.data[start..]);
}
Err(PakError::OffsetOutOfRange {
offset: entry.offset,
size: entry.comp_size,
data_len: self.data.len(),
})
}
/// Decompress an entry to its raw payload bytes. Handles the `"Z1"` container

View File

@@ -29,13 +29,25 @@ pub const XMA1_PACKET: usize = 2048;
/// Voice language for cutscene audio. Only English and Japanese voice exist on
/// the disc (subtitles cover more languages, voice does not).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VoiceLang {
/// The default only because the disc's own default audio track is English;
/// nothing else about the code should assume it.
#[default]
English,
Japanese,
}
impl VoiceLang {
pub const ALL: [VoiceLang; 2] = [VoiceLang::English, VoiceLang::Japanese];
pub fn label(self) -> &'static str {
match self {
VoiceLang::English => "English",
VoiceLang::Japanese => "Japanese",
}
}
fn code(self) -> &'static str {
match self {
VoiceLang::English => "eng",
@@ -67,14 +79,122 @@ pub struct VoiceClip {
pub display: String,
}
/// Enumerate the voice/dialog clips named in a decompressed `sounds.tbl` (the
/// IDXD in `tables.pak`). Extracts every `<lang>\{Voice,etc,Movie,Briefing}\…`
/// path ending in `.slb` for `lang`, parsed into `(name, speaker, display)`.
pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
/// What kind of audio a `sounds.tbl` entry names.
///
/// The split is the on-disc path shape, not a guess: the 36 language-independent
/// banks sit at the table root (`BGM_###.slb`, `JNGL_00#.slb`, `Static.slb`),
/// while everything else is under `<lang>\<dir>\`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AudioCategory {
/// `BGM_###.slb` — 32 music tracks, language-independent.
Music,
/// `JNGL_00#.slb` — 3 short jingles (mission clear / fail stings).
Jingle,
/// `Static.slb` — the sound-effect bank, one 9 MB multi-wave bank.
Sfx,
/// `<lang>\Voice\` — in-mission radio chatter, by speaker.
Radio,
/// `<lang>\etc\` — the other spoken lines (cutscene dialogue, system).
Dialogue,
/// `<lang>\Movie\VOICE_<movie>.slb` — a cutscene's continuous voice track.
MovieVoice,
/// `<lang>\Briefing\BR<NN>_<MM>.slb` — mission briefing lines.
Briefing,
/// A `.slb` whose path matched no known shape.
Other,
}
impl AudioCategory {
pub const ALL: [AudioCategory; 8] = [
AudioCategory::Music,
AudioCategory::Jingle,
AudioCategory::Sfx,
AudioCategory::Radio,
AudioCategory::Dialogue,
AudioCategory::MovieVoice,
AudioCategory::Briefing,
AudioCategory::Other,
];
pub fn label(self) -> &'static str {
match self {
AudioCategory::Music => "Music",
AudioCategory::Jingle => "Jingles",
AudioCategory::Sfx => "Sound effects",
AudioCategory::Radio => "Radio",
AudioCategory::Dialogue => "Dialogue",
AudioCategory::MovieVoice => "Movie voice",
AudioCategory::Briefing => "Briefing",
AudioCategory::Other => "Other",
}
}
/// True for the categories that are spoken lines — the set
/// [`list_voice_clips`] returns.
pub fn is_voice(self) -> bool {
matches!(
self,
AudioCategory::Radio
| AudioCategory::Dialogue
| AudioCategory::MovieVoice
| AudioCategory::Briefing
)
}
/// True when the bank is language-independent, so it appears whichever
/// `<lang>\sounds.tbl` is read.
pub fn is_shared(self) -> bool {
matches!(
self,
AudioCategory::Music | AudioCategory::Jingle | AudioCategory::Sfx
)
}
fn classify(name: &str) -> AudioCategory {
let leaf = name.rsplit('\\').next().unwrap_or(name);
if !name.contains('\\') {
return if leaf.starts_with("BGM_") {
AudioCategory::Music
} else if leaf.starts_with("JNGL_") {
AudioCategory::Jingle
} else if leaf.eq_ignore_ascii_case("Static.slb") {
AudioCategory::Sfx
} else {
AudioCategory::Other
};
}
match name.rsplit('\\').nth(1) {
Some("Voice") => AudioCategory::Radio,
Some("etc") => AudioCategory::Dialogue,
Some("Movie") => AudioCategory::MovieVoice,
Some("Briefing") => AudioCategory::Briefing,
_ => AudioCategory::Other,
}
}
}
/// One playable bank named in `sounds.tbl`, with the category its path implies.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AudioEntry {
pub clip: VoiceClip,
pub category: AudioCategory,
}
/// Enumerate **every** `.slb` bank named in a decompressed `sounds.tbl` (the
/// IDXD in `tables.pak`): the language-independent music/jingle/SFX banks at
/// the table root, plus every `<lang>\…` spoken line.
///
/// Measured on the retail disc: `eng\sounds.tbl` names 4 418 banks (36 shared +
/// 2 382 Radio + 1 821 Dialogue + 101 Briefing + 78 Movie voice) and
/// `jpn\sounds.tbl` names 5 136 (the same 36 shared + 5 100 Japanese lines).
/// Every one of the 36 shared names resolves to a `sound.pak` TOC entry under
/// [`crate::hash::name_hash`], which is the check that they are real banks and
/// not stale table text.
pub fn list_audio_entries(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<AudioEntry> {
let prefix = format!("{}\\", lang.code());
let mut seen = std::collections::BTreeSet::new();
let mut out = Vec::new();
// Scan for printable-ASCII runs; keep those that look like a voice path.
// Scan for printable-ASCII runs; keep those that name a `.slb`.
let mut i = 0;
while i < sounds_tbl.len() {
let start = i;
@@ -83,15 +203,14 @@ pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
}
if i - start >= 6 {
if let Ok(s) = std::str::from_utf8(&sounds_tbl[start..i]) {
// Every spoken-line category, so the standalone player covers them
// all: in-mission radio (`\Voice\`, `\etc\`) and bound movie voices
// (`\Movie\`) all carry `VOICE_`; mission-briefing lines live in
// `\Briefing\` as `BR<NN>_<MM>.slb` (no `VOICE` in the name).
let is_voice = s.contains("VOICE") || s.contains("\\Briefing\\");
if s.starts_with(&prefix) && s.ends_with(".slb") && is_voice {
if seen.insert(s.to_string()) {
out.push(parse_voice_clip(s));
}
// Take this language's entries plus the root (shared) banks; a
// path under the OTHER language would be a table artefact.
let mine = s.starts_with(&prefix) || !s.contains('\\');
if mine && s.ends_with(".slb") && seen.insert(s.to_string()) {
out.push(AudioEntry {
category: AudioCategory::classify(s),
clip: parse_voice_clip(s),
});
}
}
}
@@ -100,6 +219,21 @@ pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
out
}
/// Enumerate just the spoken-line clips — [`list_audio_entries`] restricted to
/// [`AudioCategory::is_voice`].
///
/// In-mission radio (`\Voice\`, `\etc\`) and bound movie voices (`\Movie\`) all
/// carry `VOICE_`; mission-briefing lines live in `\Briefing\` as
/// `BR<NN>_<MM>.slb` and carry no `VOICE` at all, which is why the category —
/// i.e. the directory — decides this and not the filename.
pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
list_audio_entries(sounds_tbl, lang)
.into_iter()
.filter(|e| e.category.is_voice())
.map(|e| e.clip)
.collect()
}
fn parse_voice_clip(name: &str) -> VoiceClip {
// `<lang>\<cat>\VOICE_<SPK>_<NNN>.slb` or `..\VOICE_<movie>.slb`.
let stem = name
@@ -525,6 +659,56 @@ mod tests {
);
}
#[test]
fn list_audio_entries_categorises_root_banks_and_keeps_them_language_shared() {
// The three root banks carry no language component, so BOTH sounds.tbl
// files name them; a language filter that only accepted `<lang>\` would
// silently drop all the music, which is what it used to do.
let mut tbl = Vec::new();
for s in [
"BGM_001.slb",
"JNGL_002.slb",
"Static.slb",
"eng\\Voice\\VOICE_ADAN_010.slb",
"eng\\etc\\VOICE_D_450.slb",
"eng\\Movie\\VOICE_S13A.slb",
"eng\\Briefing\\BR01_01.slb",
] {
tbl.extend_from_slice(s.as_bytes());
tbl.push(0);
}
let by = |lang| {
list_audio_entries(&tbl, lang)
.into_iter()
.map(|e| (e.clip.name, e.category))
.collect::<Vec<_>>()
};
let eng = by(VoiceLang::English);
let want = [
("BGM_001.slb", AudioCategory::Music),
("JNGL_002.slb", AudioCategory::Jingle),
("Static.slb", AudioCategory::Sfx),
("eng\\Voice\\VOICE_ADAN_010.slb", AudioCategory::Radio),
("eng\\etc\\VOICE_D_450.slb", AudioCategory::Dialogue),
("eng\\Movie\\VOICE_S13A.slb", AudioCategory::MovieVoice),
("eng\\Briefing\\BR01_01.slb", AudioCategory::Briefing),
];
assert_eq!(eng.len(), want.len());
for (n, c) in want {
assert!(
eng.iter().any(|(en, ec)| en == n && *ec == c),
"{n} not categorised as {c:?}"
);
}
// Reading the Japanese table yields the shared banks and none of the
// English lines.
let jpn = by(VoiceLang::Japanese);
assert_eq!(jpn.len(), 3, "only the shared banks: {jpn:?}");
assert!(jpn.iter().all(|(_, c)| c.is_shared()));
// And the voice view is exactly the non-shared half.
assert_eq!(list_voice_clips(&tbl, VoiceLang::English).len(), 4);
}
#[test]
fn rebuilds_riff_from_headerless() {
let mut slb = vec![0u8; HEADERLESS_DATA_OFFSET];

View File

@@ -0,0 +1,88 @@
//! Real-disc tests for media assembly. Skipped without `SYLPHEED_DISC`.
//!
//! This logic used to live in the Bevy viewer, where it had no test at all. It
//! is the trickiest reading on the disc — a cutscene's voice is a byte region of
//! a continuous stream, not the bank its name points at — so it gets pinned here
//! before anything else is built on top of it.
use std::path::PathBuf;
use sylpheed_formats::media::{self, DirectorySource, DiscSource};
use sylpheed_formats::slb::VoiceLang;
fn disc() -> Option<DirectorySource> {
let p = PathBuf::from(std::env::var("SYLPHEED_DISC").ok()?);
p.join("dat").is_dir().then(|| DirectorySource::new(p))
}
/// A segment-spanning read returns the same bytes as slicing the whole archive.
///
/// The control that matters: `sound.pak`'s data is five segments, so a TOC
/// offset late in the archive addresses a position no single file has. If the
/// walk were off by a segment this would return plausible-looking wrong bytes
/// rather than fail, which is exactly why it is asserted against the archive's
/// own read rather than against a length.
#[test]
fn segment_range_matches_the_archive_read() {
let Some(src) = disc() else {
eprintln!("SKIP: set SYLPHEED_DISC");
return;
};
let name = "BGM_020.slb";
let hash = sylpheed_formats::hash::name_hash(name);
let via_range = media::read_sound_bank(&src, hash).expect("segment range read");
let toc = src.read_file("dat/sound.pak").unwrap();
let entries = sylpheed_formats::PakArchive::parse_toc(&toc).unwrap();
let e = entries
.iter()
.find(|e| e.name_hash == hash)
.expect("BGM_020 in the TOC");
assert_eq!(via_range.len(), e.comp_size as usize);
// And it decodes, which a misaligned read would not do.
let riffs = media::sound_bank_riffs(&src, name).expect("riffs");
assert!(!riffs.is_empty(), "no sub-waves recovered");
}
/// A movie's voice resolves to a byte region, and the region is sane.
///
/// `RT01A` is one of the cutscenes whose voice spans more than one `.slb`
/// chunk — the case that motivated regions over per-bank reads in the first
/// place. The assertions are deliberately about *shape* (ordered, non-empty,
/// smaller than one bank) rather than exact offsets, because the offsets are
/// disc facts we have no independent oracle for here; a regression that
/// reversed or emptied the region would still be caught.
#[test]
fn movie_voice_resolves_to_a_region_that_decodes() {
let Some(src) = disc() else {
eprintln!("SKIP: set SYLPHEED_DISC");
return;
};
let (start, end) = media::resolve_movie_voice_region(&src, "RT01A", VoiceLang::English)
.expect("RT01A has a bound voice track");
assert!(start < end, "region is inverted: {start}..{end}");
assert!(end - start > 4096, "region is implausibly small");
assert!(end - start < 1_500_000, "region spans more than one bank");
let riffs = media::voice_region_riffs(&src, start, end).expect("region riffs");
assert!(!riffs.is_empty(), "region decoded to no audio");
}
/// An unbound movie stays unvoiced rather than borrowing a neighbour's clip.
///
/// This is a *negative* the corpus paid for: extending resolution to unbound
/// resupply movies by shared demo line played the WRONG recording. The guard
/// keeps that door shut.
#[test]
fn manifest_binding_is_the_only_route() {
let Some(src) = disc() else {
eprintln!("SKIP: set SYLPHEED_DISC");
return;
};
// A movie the manifest does not bind must resolve to nothing, not to a guess.
assert_eq!(
media::resolve_movie_voice_clip(&src, "no_such_movie_xyz", VoiceLang::English),
None
);
}

View File

@@ -167,3 +167,62 @@ fn manifest_slot_and_movie_counts() {
// Every id names a real record, so nothing dangles.
assert!(entries.iter().all(|e| !e.slot.is_empty() && !e.movie.is_empty()));
}
/// Everything the Cutscenes browser shows, asserted against the disc.
///
/// The window's value is that it answers "which cutscenes exist, which mission
/// is each one in, and can I read it without playing it" — so the test checks
/// exactly those three, including the **negative**: five manifest-bound movies
/// have no `.wmv`, and nine more have no English transcript. A browser that
/// quietly omitted them would look complete and be wrong, so the counts are
/// pinned here rather than left to the eye.
#[test]
fn cutscene_catalog_binds_movies_and_transcripts() {
let Some(root) = disc_root() else {
eprintln!("SKIP: set SYLPHEED_DISC");
return;
};
use sylpheed_formats::movie_subtitle as ms;
let (manifest, _) = load_manifest_and_sounds(&root);
let rows = movie_manifest::parse(&manifest);
assert_eq!(rows.len(), 104, "manifest slots");
let movies: std::collections::BTreeSet<&str> =
rows.iter().map(|r| r.movie.as_str()).collect();
assert_eq!(movies.len(), 101, "distinct movies");
assert_eq!(rows.iter().filter(|r| r.subtitle.is_some()).count(), 99);
assert_eq!(rows.iter().filter(|r| r.voice_token.is_some()).count(), 99);
assert_eq!(rows.iter().filter(|r| r.telop.is_some()).count(), 22);
// Bound-but-absent: the four boot logos and one encoder test clip are named
// by the manifest and are not on the disc. The browser marks these in red
// rather than offering a Play button that would fail.
let on_disc: std::collections::BTreeSet<String> = std::fs::read_dir(root.join("dat/movie"))
.unwrap()
.filter_map(|e| {
let p = e.ok()?.path();
(p.extension()?.to_str()? == "wmv")
.then(|| p.file_stem()?.to_str().map(|s| s.to_ascii_lowercase()))?
})
.collect();
let mut absent: Vec<&str> = movies
.iter()
.copied()
.filter(|m| !on_disc.contains(&m.to_ascii_lowercase()))
.collect();
absent.sort_unstable();
assert_eq!(
absent,
["SYLPH_HD720p_8M-CBR_2ch", "logo1", "logo2", "logo3", "logo4"],
"manifest-bound movies with no .wmv on the disc"
);
// Transcripts, which is what makes a cutscene readable without playback.
let lang_pak = PakArchive::open(root.join("dat/movie/eng.pak")).unwrap();
let text_pak = PakArchive::open(root.join("dat/GP_MAIN_GAME_E.pak")).unwrap();
let resolved = movies
.iter()
.filter(|m| !ms::load(m, &lang_pak, &text_pak).is_empty())
.count();
assert_eq!(resolved, 92, "movies with an English transcript");
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,12 @@ use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts};
use crate::iso_loader::{
AudioPreview, FileInfo, FileSelected, GameCategory, GameData, ImageRgba, IsoState, ModelPreview,
MovieSubtitles, MovieVoice, PakContent, PakView, RequestAudio, RequestGameData, RequestOpenDir,
RequestOpenIso, RequestSaveOpen, RequestScreenCatalog, RequestScreenCompose,
RequestShipCatalog, RequestShipRender, RequestSubtitles, RequestVoiceLibrary, SaveBrowser,
ScreenBrowser, ShipBrowser, SkyboxPreview, TextPreview, TexturePreview,
VideoPreview, VoiceLibrary, IsoLoaderSystemSet,
AudioLibrary, AudioPreview, CutsceneBrowser, FileInfo, FileSelected, GameCategory, GameData,
ImageRgba, IsoLoaderSystemSet, IsoState, ModelPreview, MovieSubtitles, MovieVoice, PakContent,
PakView, RequestAudio, RequestAudioLibrary, RequestCutscenes, RequestGameData, RequestOpenDir,
RequestOpenIso, RequestSaveOpen, RequestScreenCatalog, RequestScreenCompose, RequestShipCatalog,
RequestShipRender, RequestSubtitles, SaveBrowser, ScreenBrowser, ShipBrowser, SkyboxPreview,
TextPreview, TexturePreview, VideoPreview,
};
use crate::ViewerState;
use sylpheed_formats::SubLang;
@@ -37,6 +37,7 @@ impl Plugin for ViewerUiPlugin {
app.add_systems(Update, draw_ships_ui.after(IsoLoaderSystemSet));
app.add_systems(Update, draw_screens_ui.after(IsoLoaderSystemSet));
app.add_systems(Update, draw_save_ui.after(IsoLoaderSystemSet));
app.add_systems(Update, draw_cutscenes_ui.after(IsoLoaderSystemSet));
}
}
}
@@ -148,11 +149,12 @@ struct UiEvents<'w> {
file_selected: EventWriter<'w, FileSelected>,
subtitles: EventWriter<'w, RequestSubtitles>,
audio: EventWriter<'w, RequestAudio>,
voice_lib: EventWriter<'w, RequestVoiceLibrary>,
audio_lib: EventWriter<'w, RequestAudioLibrary>,
game_data: EventWriter<'w, RequestGameData>,
ships: EventWriter<'w, RequestShipCatalog>,
screens: EventWriter<'w, RequestScreenCatalog>,
save: EventWriter<'w, RequestSaveOpen>,
cutscenes: EventWriter<'w, RequestCutscenes>,
}
fn draw_viewer_ui(
@@ -170,7 +172,7 @@ fn draw_viewer_ui(
mut subtitles: ResMut<MovieSubtitles>,
mut movie_voice: ResMut<MovieVoice>,
mut audio: ResMut<AudioPreview>,
mut voice_lib: ResMut<VoiceLibrary>,
mut audio_lib: ResMut<AudioLibrary>,
mut events: UiEvents,
) {
let ctx = contexts.ctx_mut();
@@ -202,11 +204,11 @@ fn draw_viewer_ui(
});
ui.menu_button("View", |ui| {
if ui.button("🎙 Voice Lines").clicked() {
voice_lib.open = true;
if !voice_lib.loaded && !voice_lib.loading {
voice_lib.loading = true;
events.voice_lib.send_default();
if ui.button("🔊 Audio Library").clicked() {
audio_lib.open = true;
if !audio_lib.loaded && !audio_lib.loading {
audio_lib.loading = true;
events.audio_lib.send_default();
}
ui.close_menu();
}
@@ -228,6 +230,12 @@ fn draw_viewer_ui(
events.screens.send_default();
ui.close_menu();
}
if ui.button("🎬 Cutscenes…").clicked() {
// The manifest binds every cutscene slot to its movie,
// subtitle track, voice token and telop overlay.
events.cutscenes.send_default();
ui.close_menu();
}
if ui.button("💾 Save File…").clicked() {
// A save is not on the disc — it lives in the emulator's
// content tree, so this opens a file dialog.
@@ -248,98 +256,134 @@ fn draw_viewer_ui(
});
// ── Standalone voice-line browser (floating window) ───────────────────
if voice_lib.open {
if audio_lib.open {
let mut open = true;
egui::Window::new("🎙 Voice Lines")
.default_width(360.0)
.default_height(480.0)
egui::Window::new("🔊 Audio Library")
.default_width(400.0)
.default_height(520.0)
.open(&mut open)
.show(ctx, |ui| {
if voice_lib.loading {
if audio_lib.loading {
ui.horizontal(|ui| {
ui.spinner();
ui.label("Reading sounds.tbl…");
});
ctx.request_repaint();
} else if !voice_lib.loaded {
} else if !audio_lib.loaded {
ui.label("Open a game source first.");
} else {
use sylpheed_formats::slb::VoiceLang;
ui.horizontal(|ui| {
ui.label("Voice:");
// The table name IS the selector: there is no language
// field inside sounds.tbl, so switching re-reads the
// other file. Music/jingles/SFX are shared and stay.
for lang in VoiceLang::ALL {
if ui
.selectable_label(audio_lib.lang == lang, lang.label())
.clicked()
&& audio_lib.lang != lang
{
audio_lib.lang = lang;
audio_lib.loaded = false;
audio_lib.entries.clear();
audio_lib.reload = true;
}
}
ui.separator();
ui.label("Filter:");
ui.text_edit_singleline(&mut voice_lib.filter);
ui.text_edit_singleline(&mut audio_lib.filter);
});
let f = voice_lib.filter.to_lowercase();
// Group the thousands of entries as directory → speaker so the
// list is navigable (e.g. browse `Voice` by character to find a
// cutscene's radio line). `name` is `<lang>\<dir>\<file>.slb`.
let f = audio_lib.filter.to_lowercase();
// Group category → speaker. The category comes from the path
// shape, so the root banks (music/jingles/SFX) get real
// headings instead of the "?" a directory split gave them.
use std::collections::BTreeMap;
let mut groups: BTreeMap<&str, BTreeMap<&str, Vec<&sylpheed_formats::slb::VoiceClip>>> =
BTreeMap::new();
for c in &voice_lib.clips {
if !f.is_empty() && !c.name.to_lowercase().contains(&f) {
type Group<'a> = BTreeMap<&'a str, Vec<&'a sylpheed_formats::slb::AudioEntry>>;
let mut groups: BTreeMap<
sylpheed_formats::slb::AudioCategory,
Group<'_>,
> = BTreeMap::new();
for e in &audio_lib.entries {
if !f.is_empty() && !e.clip.name.to_lowercase().contains(&f) {
continue;
}
let dir = c.name.rsplit('\\').nth(1).unwrap_or("?");
groups
.entry(dir)
.entry(e.category)
.or_default()
.entry(c.speaker.as_str())
.entry(e.clip.speaker.as_str())
.or_default()
.push(c);
.push(e);
}
let shown: usize = groups.values().flat_map(|s| s.values()).map(Vec::len).sum();
ui.label(
egui::RichText::new(format!("{shown} / {} clips", voice_lib.clips.len()))
.weak()
.small(),
egui::RichText::new(format!(
"{shown} / {} banks",
audio_lib.entries.len()
))
.weak()
.small(),
);
ui.separator();
let filtering = !f.is_empty();
egui::ScrollArea::vertical().show(ui, |ui| {
for (dir, speakers) in &groups {
let dtotal: usize = speakers.values().map(Vec::len).sum();
egui::CollapsingHeader::new(format!("📁 {dir} ({dtotal})"))
.id_salt(("vdir", *dir))
.default_open(filtering)
.show(ui, |ui| {
for (speaker, clips) in speakers {
for (cat, speakers) in &groups {
let ctotal: usize = speakers.values().map(Vec::len).sum();
egui::CollapsingHeader::new(format!(
"{} ({ctotal})",
cat.label()
))
.id_salt(("acat", *cat))
.default_open(filtering || ctotal <= 40)
.show(ui, |ui| {
for (speaker, entries) in speakers {
// A single-bank group (Static.slb) would be a
// pointless nested header.
let flat = speakers.len() == 1 || entries.len() == 1;
let mut row = |ui: &mut egui::Ui| {
for e in entries {
ui.horizontal(|ui| {
if ui
.button("")
.on_hover_text(&e.clip.name)
.clicked()
{
audio.generation =
audio.generation.wrapping_add(1);
audio.loading = true;
audio.active = true;
audio.error = None;
audio.name = e.clip.display.clone();
events.audio.send(RequestAudio {
clip: e.clip.name.clone(),
display: e.clip.display.clone(),
movie: None,
mono: e.category.is_voice(),
generation: audio.generation,
});
}
ui.label(&e.clip.display);
});
}
};
if flat {
row(ui);
} else {
egui::CollapsingHeader::new(format!(
"{speaker} ({})",
clips.len()
entries.len()
))
.id_salt(("vspk", *dir, *speaker))
.default_open(filtering || clips.len() <= 6)
.show(ui, |ui| {
for c in clips {
ui.horizontal(|ui| {
if ui
.button("")
.on_hover_text(&c.name)
.clicked()
{
audio.generation =
audio.generation.wrapping_add(1);
audio.loading = true;
audio.active = true;
audio.name = c.display.clone();
events.audio.send(RequestAudio {
clip: c.name.clone(),
display: c.display.clone(),
movie: None,
generation: audio.generation,
});
}
ui.label(&c.display);
});
}
});
.id_salt(("aspk", *cat, *speaker))
.default_open(filtering || entries.len() <= 6)
.show(ui, &mut row);
}
});
}
});
}
});
}
});
voice_lib.open = open;
audio_lib.open = open;
}
// ── Left panel: file browser ──────────────────────────────────────────
@@ -481,11 +525,13 @@ fn draw_viewer_ui(
audio.generation = audio.generation.wrapping_add(1);
audio.loading = true;
audio.active = true; // show the panel immediately (spinner)
audio.error = None;
audio.name = format!("VOICE_{movie}");
events.audio.send(RequestAudio {
clip: String::new(),
display: format!("VOICE_{movie}"),
movie: Some((movie.clone(), movie_voice.lang)),
mono: true, // a cutscene voice track
generation: audio.generation,
});
}
@@ -1348,8 +1394,10 @@ fn draw_audio_player(ui: &mut egui::Ui, audio: &mut AudioPreview) {
if audio.loading {
ui.spinner();
ui.label("decoding…");
} else if let Some(err) = &audio.error {
ui.colored_label(egui::Color32::from_rgb(224, 86, 122), format!("{err}"));
} else {
ui.label("voice track");
ui.label("sound bank");
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("✖ Close").clicked() {
@@ -1876,6 +1924,21 @@ fn draw_screens_ui(
if !screens.filter.is_empty() && ui.small_button("").clicked() {
screens.filter.clear();
}
ui.separator();
// Widens the enumeration to everything `compose` can draw — the
// same rule the pak browser's inline preview uses. Costs a
// re-scan, so it is a deliberate click rather than the default.
if ui
.checkbox(&mut screens.include_fragments, "Fragments")
.on_hover_text(
"Also list RATC bundles with no .rat layout child: the developer-logo \
splash, and ~1 786 two-element fragments (a button beside its glow). \
These are what the PAK browser preview draws but this list omits.",
)
.changed()
{
screens.rescan = true;
}
});
ui.separator();
@@ -1895,6 +1958,12 @@ fn draw_screens_ui(
egui::CollapsingHeader::new(&pak.label)
.default_open(pak.builds.len() == 1)
.show(ui, |ui| {
if pak.truncated {
ui.colored_label(
egui::Color32::from_rgb(224, 168, 86),
"⚠ list truncated (decode budget)",
);
}
for (bi, (entry, size)) in pak.builds.iter().enumerate() {
let selected = screens.selected == Some(pi)
&& screens.build == bi;
@@ -1922,6 +1991,32 @@ fn draw_screens_ui(
{
recompose = true;
}
if ui
.checkbox(&mut screens.black_backdrop, "Black backdrop")
.on_hover_text(
"Composite over black instead of the default dim slate. The slate \
stands in for the PRMD dim quad plus the live 3D scene behind an \
in-mission screen; black is what the game composites over on a \
screen carrying its own background — and what a framebuffer \
capture must be compared against.",
)
.changed()
{
recompose = true;
}
if ui
.checkbox(&mut screens.show_primitives, "Primitives")
.on_hover_text(
"Draw the untextured .prm fade/dim/flash quads. Decoded, but their \
paint order is unsolved: a primitive has no T8aD header and so no \
layer key, and the derived order forces keyless elements last. \
That is measurably wrong — expect an opaque quad to wipe some \
screens (32 of GP_DIALOG's builds).",
)
.changed()
{
recompose = true;
}
if screens.composing {
ui.spinner();
ctx.request_repaint();
@@ -2081,8 +2176,17 @@ fn draw_screens_ui(
compose.send(RequestScreenCompose {
pak: pak.path.clone(),
build: screens.build,
// The catalog stored (pak entry index, size) per build; the
// entry index is what lets the worker read one entry.
entry: pak
.builds
.get(screens.build)
.map(|(e, _)| *e)
.unwrap_or(0),
focus: screens.show_focus,
animated: screens.show_animated,
black_backdrop: screens.black_backdrop,
primitives: screens.show_primitives,
hidden: screens.hidden.clone(),
});
}
@@ -2295,3 +2399,236 @@ fn draw_save_ui(
}
saves.open &= open;
}
// ── Cutscene browser (View ▸ Cutscenes) ──────────────────────────────────────
/// The cutscene catalog: every slot the manifest binds, with its movie,
/// subtitle track, voice token and telop overlay — and a transcript pane that
/// resolves the captions to text **without playing the video**.
///
/// Before this the manifest was invisible plumbing: it resolved a voice bank and
/// was never rendered, so a cutscene could only be found by hunting `.wmv` in
/// the ISO tree, where nothing says which mission a file belongs to.
#[cfg(not(target_arch = "wasm32"))]
fn draw_cutscenes_ui(
mut contexts: EguiContexts,
mut cut: ResMut<CutsceneBrowser>,
mut requests: EventReader<RequestCutscenes>,
mut file_selected: EventWriter<FileSelected>,
mut browser: ResMut<FileBrowserState>,
) {
if requests.read().next().is_some() {
cut.open = true;
}
if !cut.open {
return;
}
let ctx = contexts.ctx_mut().clone();
let mut open = true;
let mut pick: Option<usize> = None;
let mut play: Option<String> = None;
egui::Window::new("🎬 Cutscenes")
.default_width(880.0)
.default_height(560.0)
.open(&mut open)
.show(&ctx, |ui| {
if cut.loading {
ui.horizontal(|ui| {
ui.spinner();
ui.label("Reading the cutscene manifest…");
});
ctx.request_repaint();
return;
}
if !cut.loaded {
ui.label("Open a game source first (File ▸ Open…).");
return;
}
ui.horizontal(|ui| {
ui.label("🔍");
ui.text_edit_singleline(&mut cut.filter);
if !cut.filter.is_empty() && ui.small_button("").clicked() {
cut.filter.clear();
}
ui.separator();
ui.label("Subtitles:");
let before = cut.lang;
egui::ComboBox::from_id_salt("cutscene_lang")
.selected_text(cut.lang.label())
.show_ui(ui, |ui| {
for l in sylpheed_formats::movie_subtitle::SubLang::ALL {
ui.selectable_value(&mut cut.lang, l, l.label());
}
});
if cut.lang != before && cut.selected.is_some() {
cut.want_cues = true;
}
});
let missing = cut.rows.iter().filter(|r| !r.present).count();
ui.label(
egui::RichText::new(format!(
"{} slots · {} distinct movies{}",
cut.rows.len(),
cut.rows
.iter()
.map(|r| r.movie.as_str())
.collect::<std::collections::BTreeSet<_>>()
.len(),
if missing > 0 {
format!(" · ⚠ {missing} bound to a movie not on the disc")
} else {
String::new()
}
))
.weak()
.small(),
);
ui.separator();
let filter = cut.filter.to_lowercase();
egui::SidePanel::left("cutscene_list")
.resizable(true)
.default_width(330.0)
.show_inside(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
egui::Grid::new("cutscene_grid")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
for (i, r) in cut.rows.iter().enumerate() {
let hay = format!("{} {} {}", r.slot, r.movie, r.kind)
.to_lowercase();
if !filter.is_empty() && !hay.contains(&filter) {
continue;
}
let label = match (r.mission, r.phase) {
(Some(m), Some(p)) => format!("S{m:02} ph{p} {}", r.movie),
(Some(m), None) => format!("S{m:02} {}", r.movie),
_ => format!(" {}", r.movie),
};
let mut text = egui::RichText::new(label).monospace();
if !r.present {
text = text.color(egui::Color32::from_rgb(224, 86, 122));
}
if ui
.selectable_label(cut.selected == Some(i), text)
.on_hover_text(&r.slot)
.clicked()
{
pick = Some(i);
}
ui.label(
egui::RichText::new(r.kind).weak().small(),
);
ui.end_row();
}
});
});
});
egui::CentralPanel::default().show_inside(ui, |ui| {
let Some(row) = cut.selected.and_then(|i| cut.rows.get(i)) else {
ui.label("Pick a cutscene on the left.");
return;
};
ui.horizontal(|ui| {
ui.heading(&row.slot);
if row.present {
if ui.button("▶ Play").clicked() {
play = Some(format!("dat/movie/{}.wmv", row.movie));
}
} else {
ui.colored_label(
egui::Color32::from_rgb(224, 86, 122),
"⚠ not on the disc",
);
}
});
egui::Grid::new("cutscene_detail")
.num_columns(2)
.striped(true)
.show(ui, |ui| {
let mut kv = |k: &str, v: String| {
ui.label(egui::RichText::new(k).weak().small());
ui.label(egui::RichText::new(v).monospace());
ui.end_row();
};
kv("kind", row.kind.to_string());
kv("movie", format!("dat/movie/{}.wmv", row.movie));
if let (Some(m), Some(p)) = (row.mission, row.phase) {
kv("mission", format!("S{m:02}, phase {p}"));
} else if let Some(m) = row.mission {
kv("mission", format!("S{m:02}"));
}
kv(
"subtitle",
row.subtitle.clone().unwrap_or_else(|| "".into()),
);
kv(
"voice track",
row.voice_token.clone().unwrap_or_else(|| "".into()),
);
// The .prt overlay is bound here but we have no parser,
// so name the reference and say the content is not read.
kv(
"telop (.prt)",
match &row.telop {
Some(t) => format!("{t} (not decoded)"),
None => "".into(),
},
);
});
ui.separator();
ui.label(egui::RichText::new("Transcript").strong());
if cut.cues_loading {
ui.horizontal(|ui| {
ui.spinner();
ui.label("resolving captions…");
});
ctx.request_repaint();
} else if cut.cues.is_empty() {
ui.label(
egui::RichText::new(
"no captions resolved for this movie in this language",
)
.weak(),
);
} else {
egui::ScrollArea::vertical()
.id_salt("cue_scroll")
.show(ui, |ui| {
egui::Grid::new("cue_grid").num_columns(2).striped(true).show(
ui,
|ui| {
for c in &cut.cues {
ui.label(
egui::RichText::new(fmt_time(c.start))
.monospace()
.weak()
.small(),
);
ui.label(&c.text);
ui.end_row();
}
},
);
});
}
});
});
if let Some(i) = pick {
cut.selected = Some(i);
cut.want_cues = true;
}
if let Some(path) = play {
// Route through the normal file-open path, so the existing video player
// handles it exactly as it would from the tree.
browser.loading = true;
browser.selected = browser.files.iter().position(|f| f.eq_ignore_ascii_case(&path));
file_selected.send(FileSelected(path));
}
cut.open = open;
}

67
docker/agent/bin/push-work Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Push the current topic branch to origin — the ONLY sanctioned way out of the
# container.
#
# Why a wrapper instead of plain `git push`:
#
# * **`main` and shared branches are refused.** The agent commits to
# `auto/<topic>`; a human merges. A token that can push anywhere is one
# confused iteration away from rewriting the consolidated line.
# * **Force-push is refused**, always. Nothing here needs it, and history
# rewriting is the one mistake that cannot be undone by merging.
# * It pushes the CURRENT branch only, by name, so a stray `--all` cannot
# publish another agent's worktree branch mid-experiment.
#
# Credentials come from a file mounted read-only at ~/.git-credentials (see
# `sylph-agent`). They are never printed, never logged, and never passed on a
# command line.
#
# push-work push the current branch
# push-work --dry-run say what it would do
set -euo pipefail
DRY=0
[ "${1:-}" = "--dry-run" ] && DRY=1
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || {
echo "push-work: not inside a git repository" >&2; exit 1; }
cd "$repo_root"
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" = "HEAD" ]; then
echo "push-work: detached HEAD — check out a branch first" >&2; exit 1
fi
case "$branch" in
auto/*) ;;
*)
echo "push-work: refusing to push '$branch'." >&2
echo " Only auto/* topic branches may leave the container; a human merges" >&2
echo " them into main. Move your work: git switch -c auto/<topic>" >&2
exit 1 ;;
esac
if [ ! -s "$HOME/.git-credentials" ]; then
echo "push-work: no credentials mounted at ~/.git-credentials." >&2
echo " The host must start the container with SYLPH_GIT_CREDENTIALS pointing" >&2
echo " at a file containing one line:" >&2
echo " https://<user>:<token>@git.mc02.dev" >&2
exit 1
fi
# `store` reads the file we mounted; nothing is written back (it is read-only).
git config --local credential.helper "store --file=$HOME/.git-credentials"
ahead=$(git rev-list --count "origin/$branch..$branch" 2>/dev/null || git rev-list --count HEAD)
echo "push-work: $branch — $ahead commit(s) to publish"
if [ "$DRY" = 1 ]; then
echo "push-work: --dry-run, stopping here"
exit 0
fi
# --force-with-lease is deliberately NOT offered. If this is rejected as
# non-fast-forward, someone else moved the branch: fetch and merge, do not
# overwrite.
git push --set-upstream origin "$branch"
echo "push-work: pushed $branch"

View File

@@ -1,48 +1,122 @@
Work the Project Sylpheed reverse-engineering backlog, one item at a time.
Answer the open questions the Godot menu port is blocked on, one at a time.
Read `Syplheed-Reborn/docker/agent/AGENT.md` first — it has the container's
tooling and, more importantly, the method rules this corpus is built on.
## Your objective
`Syplheed-Reborn/docs/port/MISSION.md` — read it every iteration. It lists the
open questions Q1…Q9 plus a gated probe S1, and the gate each one must pass.
**You do not build the port.** A separate agent does that, from what you produce.
Your deliverable is decoded, verified, written-down answers with the evidence.
If you find yourself designing an export schema or writing GDScript, you have
crossed the line — go back to the question you were answering.
This is still reverse engineering. What changed is what earns attention: an item
is worth doing when the menu port is blocked on it.
## Read these first, every iteration
Short on purpose, and the reason this prompt is short:
1. `docs/port/MISSION.md` — the open questions, their gates, what is out of scope.
2. `docs/port/HANDOFF.md` — what the port agent has been told so far. **Update it
when you answer something.** An answer not reachable from that page has not
been delivered.
3. `docs/re/REFUTED.md` — claims already tested and dead. Grep it for your nouns
before designing anything.
4. `docs/re/METHOD.md` — the traps this corpus has already paid for.
5. `docs/re/INDEX.md` — what is already decoded. Re-deriving a ✅ row is not a
finding; asking whether its values *resolve* is.
6. `docker/agent/AGENT.md` — the container's tooling.
`docs/re/disc-atlas.html` maps how the assets reference each other — useful when
you need to find what feeds what.
**These files are the memory.** A finding that lives only in your context is lost
when the container dies.
## Each iteration
1. **Pick one item.** Take the next open entry from
`Syplheed-Reborn/docs/re/BACKLOG.md`, preferring the one whose "first step"
is cheapest and most decisive. If you are mid-item from a previous
iteration, continue it rather than starting another.
2. **Do the smallest experiment that could settle it**, and try to *refute*
your hypothesis before you believe it.
3. **Write the result down** in `docs/re/` under the ✅/🟡/❔ convention, with
the evidence. A withdrawn or refuted result is a real result — record it,
with the reasoning, rather than deleting it.
4. **Commit** to a topic branch (below), one logical change per commit.
5. **Say plainly what you did not settle**, and stop the iteration.
1. **Pick one question** from MISSION.md, preferring the one that blocks the port
earliest and whose first step is cheapest. If you are mid-question, continue it
rather than starting another.
2. **Do the smallest experiment that could settle it**, and try to *refute* your
hypothesis before believing it. Run the known-positive through any new filter
first; a filter that fails its own control is dead, not tuneable.
3. **Classify the answer honestly.** Every answer is exactly one of:
* **decoded** — the field, plus a disc-wide check;
* **measured** — not on the disc in any form you found, but here is what the
running game does, and here is the capture;
* **undecodable, with reach** — you looked here, here and here, and this is
why it is not there.
Never a fourth thing. *Measured* and *undecodable* mean the port agent will
author that value by hand, and it must know it is authoring rather than
transcribing. Labelling a guess as a decode puts it into the port wearing the
badge of a measurement.
4. **Write it down** in `docs/re/` under the ✅/🟡/❔ convention, with the evidence
and the *reach* of any negative, then update the row in `docs/port/HANDOFF.md`.
* Refuted something → a line in `REFUTED.md`.
* Bitten by a general trap → a line in `METHOD.md`.
* Closed a format → update its `INDEX.md` row.
5. **Commit** to `auto/<topic>`, one logical change per commit.
6. **Publish**: `push-work`. Every iteration that produced a commit.
7. **Say plainly what you did not settle**, and stop.
## Hard rules
* **Never commit to `main`.** Work on `auto/<topic>` in whichever repo you are
touching, branched from the current `main`. Create it if it does not exist.
* **Never push.** No push credentials are mounted, and that is deliberate — a
human reviews before anything leaves the box.
* **One emulator at a time.** `run-canary` enforces this with a lockfile; do not
work around it.
* **Do not edit `main`'s history**, do not rebase shared branches, and do not
delete branches.
* **Measure the oracle; never infer it.** An iteration that reasons about the
game without running it is a red flag unless it is a pure static-format task.
* **Verify with an artifact**, not with "it compiles": `build-reborn test` (it
wires up `SYLPHEED_DISC` — without it the disc tests silently self-skip and a
green run means almost nothing), `sylpheed-cli mesh render`, `screen render`,
`save info`, a screenshot.
* **Do not build the port.** No Godot project, no GDScript, no asset pipeline, no
export schema, no transcoding. Those belong to the port agent.
* **Do not touch `crates/sylpheed-viewer`.** The Explorer is the human's tool for
exploring and verifying the RE work; it keeps its static-data-only rule and the
port does not depend on it.
* **Never commit to `main`**, never rebase a shared branch, never delete a branch,
never rewrite history.
* **Do not touch another agent's worktree.** `git worktree list` first; branches
marked `+` are checked out elsewhere.
* **One emulator at a time** — `run-canary` enforces it with a lockfile.
* **Measure the oracle; never infer it.** Most of the open questions are about
*behaviour* — timing, transitions, what a button does, what a d-pad press does
at the end of a list. Those cannot be answered from the file. An iteration that
reasons about the game without running it is a red flag unless the question is
a pure static-format one.
* **Do not improvise around a blocker.** If a question needs a decision only the
user can make, or the container cannot do it, write what you found, note it in
MISSION.md, and move to the next question you can actually finish.
## When you are blocked
## The S1 probe
If an item needs something the container cannot do — hardware Vulkan for a
rendering question, a push, a decision only the user can make — **do not
improvise around it**. Write what you found, note the blocker in `BACKLOG.md`,
and move to the next item.
One iteration, then **stop and write the go/no-go**. Do not start Ready Room work
on your own authority — MISSION.md §S1 says why.
## Verifying
* `build-reborn test` wires up `SYLPHEED_DISC`; without it the disc tests
self-skip and a green run means almost nothing.
* Verify with an **artifact**, not with "it compiles": `sylpheed-cli screen
info` / `screen render` / `mesh render` / `save info`, a capture, a screenshot.
* Commit the reference data beside the finding, so the port can be built without
a disc in the loop during development.
* A regenerated artifact that comes out byte-identical is strong evidence a change
was additive. When it does change, check that every diff line pairs exactly.
## Publishing
`push-work` pushes the current branch to origin. It refuses anything that is not
`auto/*` and never force-pushes, so the consolidated line stays a human's
decision. Run it **every iteration that produced a commit** — not at the end of
some longer arc, which is exactly when a container dies.
If it reports no credentials, say so in your reply and continue working. Do not
improvise another route out: no remote rewrite, no credential helper of your own,
no alternate transport. A push that is blocked is a blocked push.
## Pacing
Self-pace. A useful iteration is one experiment plus its write-up, not a
marathon; stopping with a clean commit and an honest "here is what is still
open" is the goal every time.
One experiment plus its write-up is a good iteration; a marathon is not. Stop with
a clean commit, a push, and an honest list of what is still open.
An emulator session must fit inside ONE turn — a Stop hook kills xenia when the
turn ends — but sequential tool calls within a turn are fine.
The loop runs on a fixed interval set by the harness, so you do **not** need to
arm the next wakeup yourself. Spend that attention on the write-up instead.

View File

@@ -21,6 +21,9 @@
# SYLPH_VULKAN=sw force software Vulkan (lavapipe) even if /dev/dri exists
# SYLPH_REMOTE=0 do NOT enable Remote Control (default: enabled for `loose`)
# SYLPH_REMOTE_NAME Remote Control session name (default: sylpheed-agent)
# SYLPH_GIT_CREDENTIALS file with `https://<user>:<token>@host` for push-work
# (default: $HOME/.sylph-git-credentials)
# SYLPH_LOOP_INTERVAL fixed loop cadence, e.g. 30m (default: 45m)
# SYLPH_CPUS / SYLPH_MEM_GB override the computed half
set -euo pipefail
@@ -125,6 +128,21 @@ docker_args() {
echo " packaged SPIRV-Tools is too old. Running is unaffected." >&2
fi
# ── git push ──
# Read-only, and only ever used by `push-work`, which refuses anything but an
# auto/* branch and never force-pushes. Without this the agent's work only
# exists inside the container and dies with it.
GITCRED="${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}"
if [ -f "$GITCRED" ]; then
_out+=(-v "$GITCRED:/sylph-home/re/.git-credentials:ro")
else
echo "==> NOTE: no git credentials at $GITCRED — the agent cannot push," >&2
echo " so its work will be lost if the container is destroyed. Create it" >&2
echo " with a single line and chmod 600:" >&2
echo " https://<user>:<token>@git.mc02.dev" >&2
echo " or point SYLPH_GIT_CREDENTIALS elsewhere." >&2
fi
[ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
[ -n "${SYLPH_VULKAN:-}" ] && _out+=(-e "SYLPH_VULKAN=$SYLPH_VULKAN")
[ -n "${SYLPH_REMOTE:-}" ] && _out+=(-e "SYLPH_REMOTE=$SYLPH_REMOTE")
@@ -191,7 +209,12 @@ case "${1:-}" in
TASK="Work the RE backlog in Syplheed-Reborn/docs/re/BACKLOG.md."
fi
fi
INTERVAL="${SYLPH_LOOP_INTERVAL:-}" # empty = let the model self-pace
# A FIXED interval by default, not self-pacing. Self-pacing requires the
# agent to call ScheduleWakeup itself at the end of every turn, and the one
# thing an agent deep in an experiment reliably forgets is the bookkeeping
# after it. With an interval the harness owns the cadence and a forgotten
# wakeup cannot end the run. Set SYLPH_LOOP_INTERVAL= (empty) to self-pace.
INTERVAL="${SYLPH_LOOP_INTERVAL-45m}"
declare -a ARGS; docker_args ARGS
ARGS+=(-e SYLPH_AUTONOMOUS=1 -w "$PROJECT")
echo "==> loose | cpus=$CPUS mem=${MEM_GB}g shm=${SHM_GB}g"
@@ -206,8 +229,19 @@ case "${1:-}" in
echo " ./sylph-agent attach chat with it locally (Ctrl-P Ctrl-Q to leave it running)"
echo " ./sylph-agent stop stop it"
echo
echo " It commits to auto/* branches and cannot push — no git credentials"
echo " are mounted, so review its work with: git -C '$PROJECT/Syplheed-Reborn' log --oneline auto/..."
# Report what is actually true. This line used to claim unconditionally that
# the agent could not push, which was written before credentials were
# supported and then went stale — telling the operator their work was at risk
# when it was not, which is the exact failure the credential mount fixes.
if [ -f "${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}" ]; then
echo " It commits to auto/* branches and publishes them with push-work,"
echo " which refuses any other branch and never force-pushes. Review with:"
echo " git -C '$PROJECT/Syplheed-Reborn' fetch origin && git log --oneline origin/auto/..."
else
echo " It commits to auto/* branches and CANNOT PUSH — no git credentials"
echo " are mounted, so its work dies with the container. Review it with:"
echo " git -C '$PROJECT/Syplheed-Reborn' log --oneline auto/..."
fi
;;
logs) shift; exec docker logs "$@" "$NAME" ;;

109
docs/port/HANDOFF.md Normal file
View File

@@ -0,0 +1,109 @@
# Handoff — what the menu port needs, and where it stands
The single page the **port agent** reads. Everything here is produced by the
container agent's reverse engineering; nothing here is a design decision about
the port itself.
Keep it current. It is a summary with links into `docs/re/`, not a second copy of
the findings — but an answer that is not reachable from this page has not been
delivered.
## How to read an answer
Every row below is one of exactly three things, and the distinction is the point:
| | meaning | what the port should do |
|---|---|---|
| **decoded** | a field on the disc, with a disc-wide check | read it from the data |
| **measured** | not on the disc in any form we found, but the running game does *this* | hardcode it, and cite this page |
| **undecodable** | we looked in these places, it is not there, here is the reach of the negative | author it by hand, knowingly |
There is no fourth kind. If a row says *measured* or *undecodable*, the port is
**authoring** that value, not transcribing it — and it should be kept somewhere a
human can see it is a human decision, so that when it is later decoded the
authored version can be deleted.
## Status
| | Question | State | Answer / link |
|---|---|---|---|
| Q1 | keyframe time unit + ramp shape | ✅ answered | ramp is **linear**; the clock advances **2 units per rendered frame**; working conversion **1 unit = 1/60 s** — [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md) |
| Q2 | which build is which screen state | 🟡 partial | build 4 title, 5 main menu, 6/8/9 submenus, `palogo` splash — unconfirmed against captures |
| Q3 | paint order for the six screens | ❔ open | runtime-solved only; declaration table is refuted |
| Q4 | button → GamePart | ❔ open | labels are baked into sprites |
| Q5 | navigation semantics | ❔ open | |
| Q6 | boot sequence + what drives it | 🟡 partial | order observed; the driver is not decoded |
| Q7 | transitions | ❔ open | |
| Q8 | menu audio bindings | ❔ open | cue table complete, event binding is not |
| Q9 | video binding + playback rules | 🟡 partial | `ADV.wmv` is the boot intro; new-game intro unidentified |
| Q10 | music-bank sub-wave roles (intro+loop?) | ❔ open | we concatenate blindly today |
| S1 | Ready Room go/no-go | ❔ open | probe not run |
## Already settled — the port can rely on these today
* **`GP_TITLE.pak` is the whole title-side tree.** Build 4 is the title with the
animating wordmarks, build 5 the five-button main menu, builds 6/8/9 are
submenus, and the developer splash is the `palogo` bundle in the same archive.
✅ decoded (enumeration), 🟡 the state labels are not yet capture-confirmed.
* **Buttons are identifiable as data.** Element kind `0x3002` = button, `0x0` =
decoration, `0x10` = primitive. ✅ decoded.
* **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is
✅ correct for a vertical menu and is **not** a decoded neighbour graph — the
disc's real navigation structure is unknown, and `opt ` is *not* a focus link
(measured and refuted, see
[`ui-focus-and-effect-elements.md`](../re/structures/ui-focus-and-effect-elements.md)).
* **Highlighted states pair by name** — `ptbtn01.rat``ptbtn01f.rat`. 🟡 a
naming convention that holds for all 54 real pairs, not a decoded field.
* **The resting pose is the hold**, not the first, last or longest-dwell keyframe;
a keyframe is the **start of a ramp**.
[`ui-resting-pose.md`](../re/structures/ui-resting-pose.md). ✅
* **That ramp is linear, and it runs at 2 keyframe time units per rendered
frame.** Measured frame-by-frame off the running game's own draw stream: a
declared 15-unit fade lands on `round(255·k/15)` for all seven of its samples,
with `k` stepping 2, 4, 6, 8, 10, 12, 14 on seven consecutive submitted frames.
**measured**, not decoded — the disc says `t=30`, it does not say what a `t` is.
The seconds conversion (`1 unit = 1/60 s`, so a 30 fps screen) rests on a
measured 27.6 present-frames/second and is the one part still worth re-testing;
[`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md) names the test.
If it turns out the game presents at 60 Hz, every duration halves — nothing
else on this page changes.
* **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the
executable's own registration strings. ✅ This is the screen vocabulary; which
button reaches which entry is Q4 and is *not* part of it.
* **The logo splash is a screen, not a video.** `logo1``logo4` are
manifest-bound with no `.wmv` on the disc. ✅
* **Sprites carry their own labels.** No font rendering or localisation is needed
for this milestone. ✅
## Facts the port will trip over
* **`ADV.wmv` is WMV3 video + WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4
plays only Ogg Theora natively. How to handle that is the port's decision, not
ours — but it is not optional.
* **The disc holds 3.3 GB of video.** Only the boot intro and the one new-game
intro are in scope.
* **`Static.slb` over-declares its size** by 616 768 bytes — it is the
highest-offset entry in `sound.pak` and its size field is an allocation size. A
reader must allow a short read there and only there.
* **Voice downmixes to mono, music does not.** The left-channel downmix is correct
for spoken lines and discards half a music mix.
* **A music bank has several sub-waves and we glue them together.** `BGM_001`
is 10 KB + 4.47 MB + 4.67 MB, concatenated into one 347 s track. Nobody has
established whether those are intro + loop, two variations, or two halves —
see Q10. Do not build menu looping on the concatenated track until it is
answered.
* **`JNGL_001.slb` does not decode.** One bank in 9 519; its payload is not a whole
number of XMA1 packets from any known data offset.
## Reference data
Committed alongside the findings, so the port can be built without a disc in the
loop during development:
* `sylpheed-cli screen info --build <n> GP_TITLE.pak` — the element table, per
build, with pivots, kinds, focus links, keyframes and resting poses.
* `sylpheed-cli screen render` — the reference composite. When the port draws a
screen, this is what it should be diffed against; where they disagree, one of
them is wrong and the disagreement is worth reporting back.
* `docs/re/captures/` — framebuffer captures of the real screens, for anything
that has to be checked against the game rather than against our renderer.

132
docs/port/MISSION.md Normal file
View File

@@ -0,0 +1,132 @@
# Primary objective — answer everything the menu port needs
**Status:** active, set 2026-08-28. This replaces "work the RE backlog" as the
agent's primary objective. It does not change what the agent *does* — it is still
reverse engineering — it changes what earns attention: an item is worth doing
when the menu port is blocked on it.
## Who builds what
**You do not build the port.** A separate agent will build it, from what you
produce. Your deliverable is decoded, verified, written-down answers plus the
reference data that proves them.
| | container agent (you) | port agent |
|---|---|---|
| decodes the disc | ✅ | ❌ — consumes your answers |
| measures the running game | ✅ | ❌ — no emulator |
| writes `docs/re/` and `docs/port/HANDOFF.md` | ✅ | reads them |
| Godot project, asset pipeline, transcoding | ❌ | ✅ |
If you find yourself designing an export schema or writing GDScript, you have
crossed the line. Stop and go back to the question you were answering.
`crates/sylpheed-viewer` is also **not yours to change** for this objective. The
Explorer is the human's tool for exploring and verifying the RE work, it keeps its
static-data-only rule, and the port does not depend on it.
## The target
Someone else has to build this, from your answers alone:
```
developer logo splash → intro video → title / PRESS Ⓐ → main menu → submenus
```
No gameplay, no 3D, no HUD, no missions. If an answer is not needed to put those
five screens on a display and let a person move through them with a d-pad and Ⓐ,
it is not in this objective.
## What is already answered
Do not re-derive these. They are in `docs/re/` and the
[disc atlas](../re/disc-atlas.html):
* **The screen archive.** `GP_TITLE.pak` holds the whole title-side tree — build 4
the title with animating wordmarks, build 5 the five-button main menu, builds
6/8/9 submenus, and the developer splash as the `palogo` bundle in the same pak.
* **Buttons are identifiable as data.** Element kind `0x3002` is a button, `0x0`
decoration, `0x10` a primitive; buttons sort top-to-bottom by resting Y; each
pairs with an `f`-suffixed highlighted variant.
* **The screen vocabulary.** The GamePart id table, 29 entries at `.rdata
0x820A1630`, confirmed by the executable's own factory-registration strings.
* **The resting pose rule** — the hold, not the longest dwell — and that a
keyframe is the *start of a ramp*.
* **Screen composition**, pixel-accurate for the tutorial pause menu and the title
main menu, via `sylpheed-cli screen render`.
* **The logo splash is not a video.** `logo1``logo4` are manifest-bound with no
`.wmv` on the disc; the splash is the RATC screen, which already renders.
## The open questions — these are the objective
Ordered by what blocks the port earliest. Each is done when its **gate** exists:
a written `docs/re/` result with the evidence, and reference data committed
alongside it.
| | Question | Gate |
|---|---|---|
| **Q1** | **What is a keyframe time?** Values run 16…269. 60 Hz frames would make the title intro ~4.5 s — plausible and untested. Also: is the ramp linear, or eased? | A measured answer against the running game, not an inference. Everything animated downstream depends on this number |
| **Q2** | **Which build is which screen state?** Confirm build↔state for splash, title/PRESS Ⓐ, main menu and each submenu | A table, each row confirmed against a capture of the real screen |
| **Q3** | **Paint order for these six screens.** Solved at runtime, unsolved from the file — the declaration table is provably not it | Either a rule derived from the bundle, or six measured orders and a clear statement that no file-side rule was found |
| **Q4** | **What does each button do?** Labels are baked into the sprites; no decoded field says which GamePart a button opens | Button → GamePart id, from code or from driving the game. Say which |
| **Q5** | **Navigation semantics.** Initial focus, wrap-around at the ends, whether left/right does anything, what B does on each screen | Observed behaviour, per screen |
| **Q6** | **The boot sequence, and what drives it.** Order is observable; the *data or code* that sequences it is not decoded. Include the attract loop and what returns to the title | The sequence, plus whatever the game reads to decide it |
| **Q7** | **Transitions.** What happens visually between screens — the `pteff00.prm` quads, a fade, a cut — and its timing | Described and timed against a capture |
| **Q8** | **Menu audio.** Which BGM per screen; which cue on move / confirm / back / error. The cue table is complete; the event binding is not | Cue names bound to events, with how you established each |
| **Q9** | **Video binding.** Which movie is the boot intro vs the new-game intro; whether playback is skippable and what ends it | Named movies plus the playback rules |
| **Q10** | **What are a music bank's sub-waves?** `BGM_001.slb` is three sub-waves — 10 KB, 4.47 MB, 4.67 MB — and we currently **concatenate them blindly** into one 347 s track. Two near-equal halves could be intro + loop, or two variations, or two halves of one piece. A menu that loops its music needs to know which | The role of each sub-wave, established for at least the menu BGM. "Concatenate" is a decision, not a default — right now it is a default nobody chose |
| **S1** | **Ready Room probe.** *Gated* — one iteration, then stop | A written go/no-go (see below) |
## Known unknowns — say so, do not fill them in
Some of these may turn out to be undecodable. That is a valid, useful answer, and
it is better than a guess, because the port agent will otherwise have to author
the mapping by hand and needs to know it is authoring rather than transcribing.
For each question, the answer is one of:
* **decoded** — here is the field, here is the disc-wide check;
* **measured** — not on the disc in any form we found, but here is what the
running game does, and here is the capture;
* **undecodable, with reach** — we looked here, here and here, and this is why it
is not there.
Never a fourth thing. In particular: if Q4 ends as "read the labels off the sprite
images by eye", say exactly that — it is then an authored mapping on the port
side, not a disc fact, and mislabelling it would put a guess into the port wearing
the badge of a measurement.
## The Ready Room probe (S1) — one iteration, then stop
`GP_READY_ROOM.pak` is the largest UI archive on the disc, 1 106 entries, and only
**6 of its names resolve**. It is also ISL-scripted. That is either a week or a
quarter, and one cheap test tells you which.
Our screen catalog enumerates bundles by **content**, not by name — `is_build` /
`is_composable` read the bytes — so unrecoverable *paths* do not necessarily mean
unrenderable *screens*.
```bash
sylpheed-cli screen list "$SYLPHEED_DISC/dat/GP_READY_ROOM.pak"
sylpheed-cli screen render --build <n> "$SYLPHEED_DISC/dat/GP_READY_ROOM.pak" /tmp/rr.png
```
Report how many builds it finds, whether any composite looks like a Ready Room,
and **whether the room is 2D at all** or 3D with a UI overlay — if it is 3D the
answer is no-go by definition, not "try harder".
**Then stop and write the go/no-go.** Do not start Ready Room work on your own
authority.
## Handing it over
[`HANDOFF.md`](HANDOFF.md) is the single page the port agent reads. Keep it
current as you answer questions: it is a summary with links into `docs/re/`, not a
second copy of the findings. An answer that is not reachable from HANDOFF.md has
not been delivered.
## Out of scope
3D, gameplay, HUD, missions, save/load, localisation beyond English, the Godot
project itself, any asset pipeline, and any archive outside `GP_TITLE`,
`tables.pak`, `sound.pak` and `dat/movie/` — except for the S1 probe.

View File

@@ -2,6 +2,11 @@
Confidence: ✅ `CONFIRMED` · 🟡 `PROBABLE` · ❔ `HYPOTHESIS`. See [README](README.md).
Also durable, and worth reading before proposing anything:
[`REFUTED.md`](REFUTED.md) — what has already been tested and died ·
[`METHOD.md`](METHOD.md) — the traps this corpus has already paid for ·
[`BACKLOG.md`](BACKLOG.md) — what is still open.
Formats we've already reversed are, for now, **documented by their parser + disc round-trip
tests** (the executable spec) rather than a prose file — the "Spec" column points there.
Promote to a prose `structures/…md` file when a format needs behavioural notes beyond layout.
@@ -13,6 +18,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
| IPFB `.pak` archive | ✅ | `sylpheed-formats/src/pak.rs` + `tests/pak_idxd_disc.rs` | header + 12-byte TOC, Z1/zlib payloads |
| name-hash (TOC keys) | ✅ | `sylpheed-formats/src/hash.rs` | Barrett-reduction hash; recovers original paths |
| IDXD object/table | ✅ | `sylpheed-formats/src/idxd.rs` + `tests/idxd_records_disc.rs` ([container](structures/idxd-container.md)) | **The binary record/index region in front of the string pool is DECODED** (2026-08-25), closing the parser's long-standing "not yet decoded" note. Uniform 16-byte records `{name_hash, name_off, field_begin, field_end}` sorted by hash and binary-searched, then a field count, 12-byte fields `{key, name_off, value_off}` sorted by key, a pool size, and the string pool; the trailing `pool_size == file_len - pool_base` identity makes the layout self-checking. Verified over the **whole disc** with **zero** failures: 7 750/7 750 objects, 190 782/190 782 records reproducing their stored `tag_hash`, 1 271 462/1 271 462 named fields reproducing their key — and `IXUD` is the same container with `ixud_hash`, UTF-16BE and all offsets in **chars** (1 104/1 104 objects, 628 165/628 165 fields). **Field names are stored on disc** — a field's middle word points at its own name — so nothing needs preimage search except the **504** field entries disc-wide that are hash-keyed with no name — which are only **42 distinct keys**, each in 12 places (the page for these was never written — the finding is in this row), and are provably unrecoverable from the hash alone; the other 1 485 073 nameless fields are *positional*, keyed by a literal integer (line slots, movie ids). ⚠️ **Two long-held beliefs WITHDRAWN**: the word at `0x08` is **not a schema hash**, it is record 0's `name_hash` (7 750/7 750) — the header has no type field at all, so an object's kind is known only from the caller that loads it; and the field's middle word is **not** an always-`0xFFFFFFFF` flags word. The first was caught by a test asserting that every movie id names a real record: `1005 -> STAGE10_PHASE01` failed because `tag_hash("STAGE10_PHASE01")` **is** `0x067025B9`, that table's supposed schema id. 🟡 the legacy value-before-key string-pool reader is now known to be an *approximation* of the real table, and every number derived from it is re-checkable but not yet re-checked |
| IDXD nameless field keys | ✅/❌ | [idxd-unnamed-keys](structures/idxd-unnamed-keys.md) + [`tools/re-capture/idxd_unnamed_keys.py`](../../tools/re-capture/idxd_unnamed_keys.py) | Census of every field entry whose `name_off` is `0xFFFFFFFF`, disc-wide: **7 750 objects, 2 757 039 field entries, 0 parse failures**, `tag_hash` reproducing **1 271 462/1 271 462** named keys. **7 094 distinct keys are never named — and 7 052 of them are not hashes at all**, but author-assigned element ids (equal to the field's own index in 1 404 924 of 1 485 577 cases; `tag_hash("BGM_001")` is `0xC662435B` while the key valued `BGM_001.slb` is `0x000003E9`). ⚠️ **The "504 hash-keyed nameless fields" figure is 504 ENTRIES, not 504 names** — 42 distinct keys × 6 language copies × 2 records. All 42 are **ISL script-symbol hashes** in `<lang>\script\ID.tbl` (GP_READY_ROOM.pak), the link map built by `PrepareScript`'s "isl script prescanning"; 41 of 42 appear as little-endian call targets inside the `.isb` bytecode, forming a coherent launcher/helper call graph. The hash's own algebra pins the **trailing digits of 30 of the 42 names** (deltas of exactly `+0x01000001` across `stage01..09`, `stage10..16`, `challenge01..06`; `+0x01010000` across `tutorial0101..0601`). ❌ **No name was cracked, and the negative is quantified**: seven attacks up to a 3.5×10⁸ composition space found nothing above the noise floor; exhaustive preimage search recovers `"Stage01"` from its own hash but returns nothing for the real targets at ≤6 characters, and at 7 characters one target already has **1 176** preimages — a 24-bit modulus cannot name an 8+ character identifier uniquely |
| XPR2 texture + cubemap | 🟡/✅ | `sylpheed-formats/src/texture.rs` + [colour check](xpr2-colour-check.md) | de-tile + A8R8G8B8 and DXT1. **Channel order ✅ confirmed against the running game**: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, *zero* cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering ❔ |
| T8aD 2D texture | ✅ | `sylpheed-formats/src/t8ad.rs` | **100 % of the disc decodes** (19 216/19 216, measured). The "~15 % deferred variants" were a wrong model, not a variant: a surface is a list of **arbitrary sub-rectangles**, each with a 16-byte header of `dst X, dst Y, width, height`, not a 256×256 grid — `0x1c` is the **rectangle count**. Uncovered area stays transparent. **Colours ✅ CONFIRMED** ([k8888](structures/texture-color-k8888.md)) |
| RATC bundle | ✅ | `sylpheed-formats/src/ratc.rs` | child listing confirmed. **"One level deep" is not a limitation — there is nothing deeper**: 2 859 bundles hold 18 002 children at depth 1 and **0 at depth 2**, with no parse failures. Nested RATC blobs are **leaf records that reference siblings by name** (`opt `, the sprite name): 3 311 leaves, all embedding sibling names, **10 144 of 10 148 references resolve**. The 4 that do not are one dangling asset — `pmbase.rat``pmbase.t32` in `GP_STAGE_CLEAR.pak`'s four language builds, and `pmbase.t32` is **on the disc nowhere** |
@@ -94,7 +100,7 @@ files, which is how the same ground got covered twice.
| [`idxd-legacy-reader-audit.md`](idxd-legacy-reader-audit.md) | The legacy IDXD string-pool reader vs the real field table — what the old numbers got wrong | 🟡 shape CONFIRMED by hand (`FCSRange`, `ShieldRatio`, hangar `Model`); disc-wide rates are single-source |
| [`structures/idxd-container.md`](structures/idxd-container.md) | The IDXD/IXUD container — record/field table, and the two beliefs it withdraws | ✅ CONFIRMED disc-wide, 7 750/7 750 objects and 1 271 462/1 271 462 named fields, zero failures |
| [`structures/hud-glyph-quad.md`](structures/hud-glyph-quad.md) | The HUD's glyph quad — vtable `0x820B2A64` | ✅ CONFIRMED for the object layout and the atlas size, read live off |
| [`structures/slb-data-offset.md`](structures/slb-data-offset.md) | `.slb` leading-stream offset is `first_riff % 2048`, not the constant 1392 | ✅ CONFIRMED by decoding — 85 of 140 sampled banks yield more audio (median 70×), 54 identical controls. ⚠️ The *cause* is a segment-packing phase, not a header: `X = (cumulative .pNN start) mod 2048`. Wave boundaries are exact — `seek` magic at `data_at + declared_size`, **7 620/7 620** — and `Channels` must be read from `RIFF+49` (2.12 % are stereo) |
| [`structures/slb-data-offset.md`](structures/slb-data-offset.md) | `.slb` data offset = `(cumulative start of the `.pNN` segment) mod 2048`; the leading bytes are the previous bank's audio, not a header | ✅ CONFIRMED — exact for 8 783/8 783 banks, 0 mismatches; the four values (1392/1468/1600/1728) are the running sums of the five segment sizes mod 2048, which supersedes the earlier `first_riff % 2048` scan heuristic. Decoding at the right offset yields more audio in 85 of 140 sampled banks (median 70×), 54 identical controls. Wave boundaries are exact — `seek` magic at `data_at + declared_size`, **7 620/7 620** — and `Channels` must be read from `RIFF+49` (2.12 % are stereo) |
| [`structures/sound-pak-contents.md`](structures/sound-pak-contents.md) | Census of `sound.pak`, and the limit of the leading-region rule | ✅ CONFIRMED, 5 135/5 135 names hash into the TOC, **9 519/9 519** entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; ⚠️ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn |
| [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files |
| [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways |
@@ -116,17 +122,19 @@ files, which is how the same ground got covered twice.
| [`structures/unit-datasheet-static.md`](structures/unit-datasheet-static.md) | The static unit datasheet and AI flight model — `Generic`, `Maneuver`, `Effect` | ✅ CONFIRMED — 394 Generic + 114 Maneuver + 114 Effect records read; AA_/AV_ are one interleaved block (AV at X, AA at X+8), selection 🔴 BLOCKED for static RE (4 routes, all controlled) |
| [`structures/weapon-datasheet-static.md`](structures/weapon-datasheet-static.md) | The static weapon datasheet — `Weapon`, `Shell`, `AssortMissileParam` | ✅ CONFIRMED — 131 Weapon + 131 Shell + 9 AssortMissileParam records read |
| [`structures/isl-timers.md`](structures/isl-timers.md) | A ScriptPhase owns 32 stopwatches, and they count seconds | ✅ CONFIRMED — the advance is read from `sub_822710D0`, the unit from |
| [`structures/isl-builtins.md`](structures/isl-builtins.md) | The 147 ISL built-ins — dispatch table, calling convention, and which one does what | ✅ for the table/ABI and ~135 handlers; ✅ 26 `set_unit_hp_pct`, 29 `set_unit_damage_taken_pct`, 101 `all_units_invulnerable`; 🟡 28 |
| [`structures/isl-message-dialogue-link.md`](structures/isl-message-dialogue-link.md) | Mission scripts as dialogue — built-in 64 -> message id -> caption text | ✅ CONFIRMED total, 2 683/2 683 call sites across all 28 stages resolve, no residue |
| [`structures/mission-objective-counter.md`](structures/mission-objective-counter.md) | `REMAINING OB` — the mission's own objective counter, in RAM | ✅ CONFIRMED for one Stage 02 run: a big-endian u32 whose value |
| [`structures/movie-subtitles.md`](structures/movie-subtitles.md) | Movie subtitles & the movie ↔ mission ↔ text chain | — |
| [`structures/asteroid-fields.md`](structures/asteroid-fields.md) | `AsteroidGroup_00N` — the asteroid-field tables a `Phase_N.AsteroidDefinition` names | ✅ 10 objects/pack, 384 group records, 3 fields each (`AsteroidModelName`, `AsteroidFrameName`, `EnumAsteroid` = **the frame record's `FrameCount`, 57/57**, and the frame holds **`index, quaternion(x,y,z,w), position(x,y,z)`** per asteroid — 37 518 items, unit norm 37 518/37 518); `Enum<Thing>` counts while `Enumerate<Thing>` names a table (54/60); the join resolves as `name_hash("stage\" + name)` 9/9; 🔑 a tenth `Frame_Alpha_S01_*` object nothing references — it miscounts AND its 7 frames have no placement records; 🔑 **S28.Phase_1 borrows `S14_p2_asteroid.tbl`** — the per-phase join shows 9 tables ↔ 9 volumes with one table shared |
| [`structures/mcol-collision.md`](structures/mcol-collision.md) | `MCOL` — same container and map parameters as `REGN` | 🟡 OPENED — `POF0` at `data_size+16` 11/11, bbox pad words 11/11, `extent == maxmin` 11/11, and bbox + cell-size distributions identical to `REGN` (2/6/3 and 2/9); everything past `0x40` ❔ |
| [`structures/regn-map-grid.md`](structures/regn-map-grid.md) | `REGN` — a per-map spatial grid (and `MCOL` beside it) | ✅ CONFIRMED for the header, which self-checks on all 11 objects on |
| [`structures/regn-map-grid.md`](structures/regn-map-grid.md) | `REGN` — a stage's tetrahedral navigation mesh (and `MCOL` beside it) | ✅ CONFIRMED — tet mesh + face adjacency + portal costs + uniform grid; decoded from the file's own `POF0` fixup table |
| [`structures/savegame-format.md`](structures/savegame-format.md) | Save file (`savedata`) — container ✅ exact, 3 fields named ✅, rest ❔ (2026-08-11) | ✅ CONFIRMED for the container and the chunk layout — parsed off the |
| [`structures/sound-slb.md`](structures/sound-slb.md) | Sound bank audio — `sound.pak` / `.slb` / XMA1 | — |
| [`structures/stage-definition-table.md`](structures/stage-definition-table.md) | Stage definition table and the squadron (`UnitGroup`) roster | ✅ for the record vocabulary and the stage→table wiring; |
| [`structures/stage-mission-tables.md`](structures/stage-mission-tables.md) | The stage table set — phases, routes, sub-objectives and AI parameters | ✅ the table set and how the stage record reaches it, validated across; **`AIParams` disc-wide: 23 objects, one shared 34-profile roster (782 records), loader `sub_8233C368`; `Type`→field-count holds except the two `_Test` templates** |
| [`structures/texture-color-k8888.md`](structures/texture-color-k8888.md) | Texture colour interpretation — `k_8_8_8_8` (32bpp UI/HUD textures) | — |
| [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) | What a keyframe time is worth, and what shape the ramp has | ✅ CONFIRMED from the running game's own draw stream — the ramp is **linear** (a declared 15-unit fade lands on `round(255·k/15)` for all seven samples) and the animation clock advances **2 time units per submitted frame**. 🟡 the seconds conversion (`1 unit = 1/60 s`) rests on a measured 27.6 present-frames/second |
| [`structures/ui-composable-bundles.md`](structures/ui-composable-bundles.md) | A screen build is not the only thing `compose` can draw | ✅ CONFIRMED by measurement over the disc, with the artifact to |
| [`structures/ui-focus-and-effect-elements.md`](structures/ui-focus-and-effect-elements.md) | `_eff` glow layers are not focused-state records | ✅ CONFIRMED by measurement over all 965 screen builds on the disc, |
| [`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md) | The paint order comes from a layer key in the T8aD sprite header | ✅ CONFIRMED on both screens whose paint order has been measured — |

119
docs/re/METHOD.md Normal file
View File

@@ -0,0 +1,119 @@
# Method traps already paid for
Each line cost an iteration at least once. They are general — they are not about
Sylpheed, they are about how this kind of measurement goes wrong.
Like [`REFUTED.md`](REFUTED.md), this list had been living in the autonomous
agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
✅/🟡/❔ confidence convention itself.
## Controls
* **Every result needs a control. A control that fails kills the instrument.**
* **Run the known-positive through a new filter FIRST.** Three filters have been
killed by their own control. When one fails, **read the known-good's
disassembly** before assuming a shape.
* **A measured negative is a result** — but a negative is only as strong as the
route you ran, so **state its reach**.
* **A null result needs its cause shown to have happened.**
* **A result with NO unknowns is suspicious.**
* **Census the whole set; always run the other population as the control.**
**Zero partials is stronger than a majority.**
* **A 2×2 partition is the sharpest general tool** — both off-diagonals empty is
a law.
* **Re-derive a doc's own numbers as the control.**
## Inference
* **Never conclude from ONE sample.**
* **A law proved on one population is a hypothesis on the next.**
* **Finding one exception does not imply a family.**
* **Consistency is not proof. A suggestive coincidence is a coincidence until
measured.** **An analogy is not a measurement.**
* **Same layout ≠ same instance.** **Same record-name set ≠ same object.**
* **A marker is only proven by what it leaves out.**
* **A high-confidence SCORE is not a high-confidence MECHANISM.**
* **Knowing HOW MANY is not knowing WHICH.**
* **Round numbers matching is weak evidence — unless you read the constant.**
* **My own last-turn result is a hypothesis too.**
* **A global partition can understate a per-owner one.**
* **A residual is measured against a population — name it.**
## Searching and tooling
* **A search that returns thousands has no power; state the reach.**
* **A substring match is not a hit.** **A regex miss looks like a null result —
print one raw sample before believing a zero.**
* **A derived table can be a cross product — measure its shape first.**
* **After refuting an instrument, sweep everything that depended on it.**
* **Before measuring how wrong a tool is, read what the tool actually does.**
* **The instrument must pass its own control.**
* **Classify a bulk before mining it. The residual is the prize.**
* **Rank by similarity — the cliff is the finding.** But **read the values
before trusting the rank.**
* **Grep the nouns before designing the experiment — and believe it.**
* **Grep gives you a file list — read *every* file on it.**
* **The answer is often already in the doc that owns the subject — read it end
to end.** A 🟡 often names its own route.
* **Before re-trying a blocked idea, check whether the blocker's own doc already
tried it.**
* **Ship a regenerator with every artefact.** An artefact that moves by a pure
reorder is a tool bug.
* **Never print per-entry lines from a disc-wide sweep — aggregate.**
## Reading the data
* **Read what a loader NAMES, not where it stores.**
* **A field the disc never values still gets named by the loader.**
* **An indexed read beats a deduped-pool adjacency read.**
* **Check the whole string set, not the one matching word.**
* **A dict keyed by record name across a multi-entry pak is a lie.**
* **A set-difference over names hides reuse — join per USER.**
* **A self-index names records, not files.**
* **Case-insensitive hashing means two spellings can be one entry.**
* **An "unresolved" name may be the wrong kind, namespace or prefix — or part of
a cut asset.**
* **A garbled value may be a real string in another encoding.**
* **Two of my own counts disagreeing is a grammar clue.**
* **A game's own typo is a join key.**
* **A bias constant in the code is a join key.**
* **A prefix trap: enumerate maximal `[A-Za-z0-9_]` runs, not `startswith`.**
* **Re-deriving a format is not a finding — asking whether its values *resolve*
is.**
## Mechanics that have bitten
* **Never hand-convert a decimal VA — print `hex()`.**
* **`grep -c` counts LINES** — use `grep -o | wc -l`.
* **`Counter.most_common()` tie-breaks by insertion order — use `sorted()`.**
* **Raw grep cannot see inside compressed pak entries.**
* **Commit messages go in a file** (`git commit -F`); a literal `|` in a table
cell needs escaping; `git log --all -- <path>` can hang.
## Runtime / emulator
* **Look at the PNG** — and check its dimensions.
* **"Animating" is not "still in a mission".**
* **Dedup entity enumerations by position value.**
* **Do not diagnose timing or liveness under gdb.** `ps %cpu` is cumulative.
* **Classify screens by whole-image statistics, not named pixels** — a named
pixel is only valid while the image sits at a known place, and nothing errors
when it moves.
* **Do not poll faster than the guest updates** — it manufactures a clean curve
out of noise.
* **A probe that never performs the action will "prove" the action does not
exist.**
* **The container's Canary binary can be older than the Canary source tree, and
the failure mode is a hang, not an error.** After a merge into `sylpheed-re`
the prebuilt `xenia_canary` had no `log_ui_draws`, no `mem_watch`, no
`create_profile_if_none` — and an unknown cvar makes xenia open an SDL message
box before logging is up, which headless is an unexplained freeze. Check before
trusting a harness flag: `nm -C <binary> | grep cvars::<flag>`, and
`build-canary Release` if it is missing.
* **A capture armed *at* a screen only ever sees the steady state.** Anything
about how a screen is built or animated has to be armed *before* it exists.
Re-arming every few seconds and keeping every log tiles the approach: each F10
opens a new numbered file and closes the previous one complete.
* **Measure animation in submitted frames, not in seconds.** `VdSwap` counts are
the guest's own frames, so an emulator at 80 % of real time does not move them;
a stopwatch reading does, silently and by an unknown factor.

185
docs/re/REFUTED.md Normal file
View File

@@ -0,0 +1,185 @@
# Refuted — claims that were tested and died
**Read this before proposing a hypothesis.** Every line below was believed at
some point, measured, and found false. Reviving one costs a whole iteration and
produces nothing.
This file exists because the list had been living in the autonomous agent's
*loop prompt* — the only copy, lost the moment the container was. A refuted
result is a real result; it belongs in the corpus like any other.
**Format:** each entry is the claim as it was believed. Where the true answer is
known it follows after `→`. Grouped by subject so a grep for your noun finds the
neighbourhood, not just the line.
---
## Offsets, structs and the progress singleton
* `position = instance 0x12c` → refuted.
* `+0x29d0` → refuted.
* "an offset intersection can find a struct's consumer" → **only for LARGE or
unusual offsets.** Small ones have no power (`+184`: 301/351/115 hits).
* "a `+1956` store means a progress write" → writes go through the COPY, not
direct stores. 9 direct stores, none of them a progress write.
* "the singleton-global filter can find progress writers" → it fails its own
control.
* "the progress copy destination is an `r1`-relative stack local" → it is a
**frame register**. The `r1` assumption returned 0 for all 21 candidates
*including the known-good* — the filter was killed by its own control.
* "word B's writer also stores the Time/Points record" → it does not.
* "the debriefing records the metric with the clear bit" → 44 calls, exactly two
strings (`DEBRIEFING`, `BASE_INFO`), no `Time`, no `Points`.
* "`0x820AF030` holds live state" → all 384 words constant; it is a
spawned-entity record, not live state.
## Screens, classes and RTTI
* "the RTTI route can name the anonymous classes" → 1 150 vtables: 1 150
`ANON_`, 0 `rtti_present`, 0 base classes.
* "the sibling vtable methods name the class" → they cannot.
* "`xrefs` can name the callers of a vtable method" → no.
* "the `ind_call` refutation voids existing corpus claims" → damage bounded,
4/4 caller claims verify. But **`xrefs.ind_call` is a CROSS PRODUCT** — always
filter `kind='call'`.
* "`BASE_INFO` marks the 5-slot screen family" → it discriminates
screen-config from table-read, 9/9 vs 10/10.
* "a high key count means a rich screen" → `sub_82297550` / `sub_822A2F00`'s 27
"keys" are coordinate pairs, i.e. a layout table.
* "`EX_` = the CHALLENGE-mission debriefing" → `EX_` is **EXTRA**, mission-kind
3.
* "the `EX_` selection has not been shown" → it has: `[[obj+4]+184] == 3`.
## Stages, missions and the challenge set
* "the disc's stages are numbered 1..28" → S01S16 story, S17 **cut**, S18S23
tutorials, S24S29 challenge.
* "S24S29 are story missions" → they are the challenge missions.
* "the challenge missions have their own maps" → they reuse
`GP_MAIN_GAME_E.pak`'s stage records.
* "the challenge `REQUIREMENT` values are 16,25,26,27,29" → the chain is
16→24→25→26→27→28.
* "the `Extra0n` family shares one leaderboard metric" → `RECORD_TYPE` is
per-stage: 3 Time / 3 Points.
* "stage = filled SHAB count + 1" → refuted.
* "the first TRIGGER is always the point of no return" → refuted.
* "`EnumUnit_S14.tbl` might be missing" / "S14's 13 are a manifest omission" /
"asteroids are exempt from the manifest" → S14's 13 are **dangling
deployments**. NEEDS-HUMAN: fly S14.
* "`StageMessageSet_S02.tbl` is not in the pak" → it is.
* "`S28_p1` has an asteroid volume with no definition" → refuted.
* "`test_s8p1_asteroid.tbl` is test-only" → refuted.
* "the settings family has 28 or 29 objects" → 24.
## ISL / mission scripting
* "the bytecode is in the `.embsec_` sections" → refuted.
* "only 31 built-ins take a unit" → refuted.
* "`sub_8230C398` is the message pump" → refuted.
* "`bus+8216` is the subscriber registry" → refuted.
* "the ScriptPhase vtable is ≥200 slots" → 113.
## IDXD, paks and naming
* "IDXD record keys are `name_hash`" → record keys are **`tag_hash`**
(case-SENSITIVE); `name_hash` is case-INSENSITIVE and used for pak keys.
* "pak TOC order is stage order" / "TOC order is semantic order" → it is not.
* "the executable holds the asset names" → the image names **no data value at
all**; that route is powerless.
* "the image might name a data VALUE" → powerless.
* "the XPR2 manifest names hash to the DefTables tables" → refuted.
* "the `DefTables` model names are unreachable" → reachable via the `Enumerate`
declaration tables (1 413/1 425, 99.2 %).
* "the `GP_MAIN_GAME_*` unnamed block is undiscovered data" → refuted.
* "each `GP_MAIN_GAME_*` `Enumerate` object declares something" → refuted.
* "`GP_HANGAR_ARSENAL` is missing data tables" → refuted.
* "the `Enumeration` self-index can name objects" → a self-index names
**records, not files**.
* "a per-pak prefix might close the 2D blocker" → no.
* "the `+` paths might name the 2D or `GP_READY_ROOM` keys" → the `+`-dictionary
route is exhausted, 0 of 1 817.
* "the `game:\` paths are unresolved" → refuted.
* "a set-difference over file names can see reuse" → it cannot; **join per
USER**. Per-pak copies are ×6.
## Units, weapons, effects and assets
* "`Generic` (394) is the unit datasheet" → refuted.
* "a loadout's `Arm1` names an item" → it names a **hardpoint slot**
(`Turret_NNN`), 59/59.
* "`EnumUnit` and the unit datasheet share a vocabulary" → they do not.
* "the roster is the `Generic.Model` set" → roster 40, `Generic.Model` 46,
`GameResourceID` 480 — three vocabularies.
* "every unit ID is `UN_<l>###_<FACTION>_<name>`" → the grammar is
`UN_<letter>###_[<subkind>_]<FACTION>_<name>`.
* "`_EXn` is the `Extra0n` index" → three different `EX` vocabularies exist.
* "the only two `_EX5` names on the disc are the AA gun and the DeltaSaber" →
refuted.
* "running the tutorial will instantiate the `_Ttrl` weapons" → refuted.
* "the disc has exactly three `EnumWeapon` tables" → four.
* "the `wep_NN` package gaps are unshipped weapons" / "`wep_85` is the tip of a
family" → `wep_85` is the **only** declared-but-unshipped asset (59/0/1/26).
* "nothing is deployed without being declared" → refuted.
* "effects are one namespace" → refuted.
* "the 58 undeclared effect names are missing assets" → refuted.
* "all five orphan effects are unshipped" → refuted.
* "`eff_f0002` ships in `Base.xpr`" → refuted.
* "`Base.xpr` holds more bound effects than `ptc_pack`" → refuted.
* "the 34 unlocated are a scatter" → refuted.
* "the 9 unlocated might be under another prefix" → refuted.
* "`ptc_pack` has 532 names" → 727.
* "the `_e`/`_f` law is effect-FIELD-specific" → it is the **faction law**,
564/564.
* "a disc-wide `.xpr` byte search can show an effect is ABSENT" → it cannot.
* "`rot_n001` is on the disc" → refuted.
* "`rou_f004`'s mesh is in `Stage_S28.xpr`" → it is in `DeltaSaber_A.xpr`.
* "`parent` + `_all` + `_child` is the composite-model convention" → refuted.
`_hangar` **is** real (59 of 166, 59/59 with a bare twin); `_all`/`_child` is
not.
* "`Motion_guard_start` has no damage variants" → refuted.
* "`CoverArea` bits 2 and 3 are mutually exclusive" → refuted.
* "the 27 unresolved `NamePlate` values are missing objects" → refuted.
## LOD, background and misc tables
* "`EnumLODSet_*` is a per-stage family" → `EnumLODSet_test.tbl` serves 17
stages; 17+5+1 = 23.
* "there are 8 orphan LOD tables" → 6.
* "the orphans are stale copies of `_test`" → refuted.
* "S25 is absent from the `DefTables` LOD families" → refuted.
* "`BackGroundID` has no referent anywhere" → it is an **identity**.
* "`BackGroundPackage == BG_<id>.xpr`" → refuted.
* "`<X>ID` + `<X>Package` is a convention" → refuted.
* "`Placement_*` / `RouteTest_*` are unattached" → refuted.
* "the `AsteroidDefinition` join does not reproduce by hash" → it does.
* "the 8-value frame is a new finding" → it was already in the corpus.
## Loaders, config and tuning
* "the config reader is XML" → INI.
* "`sub_822F9498` is the unit-definition loader" → it is `PlayerParams`'s.
* "`sub_822AE628` reads the main-game `Tweak`" → refuted.
* "`sub_8230D1F8` is a rank table" → it is the stage-settings loader.
* "`sub_82286BC8`'s key list is new" → refuted.
* "`sub_825F2CF0` / `sub_825F2F88` read a post-processing table" → refuted.
* "`Booster` is a new schema" / "`Booster` is the player craft's flight
envelope" → refuted; nothing selects `Booster`.
* "the `AnalogRevice`/`Tweak` block is unreachable" → reachable
(`sub_821A6CF0`, base `0x820A1630`).
* "a 0-xref string block has no reader" → refuted.
* "the AI table was NEEDS-HUMAN" → refuted.
* "the `PG*` HUD names are undocumented" → they are documented.
* "a base-solver row identifies a FUNCTION" → it does not.
* "a 64K-boundary base is low confidence" → **inverted**; it is high
confidence.
* "the 0x820B0000 cluster is a false positive" → refuted.
* "a pointer to a function in the image implies a registry" → refuted.
`.pdata` is not a registry.
* "the 13 player-facing chatter tables are the WINGMAN tables" → refuted.
* "the 8 undeclared chatter tables are tutorial chatter" → refuted.
* "other datasheets ship a schema too" → refuted.
## Encoding and text
* "every IDXD string value is ASCII" → 6 non-ASCII values of 99 328.
* "`文字列` is a dev placeholder" → they are Shift-JIS **type words**.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,790 @@
frame,draw,x,y,w,h,rot,col
1,0,0,0,1280,720,,
1,1,0,0,1280,720,,FF000000
1,2,307,331,666,65,,FFFFFFFF
2,5,0,0,1280,720,,
2,6,0,0,1280,720,,FF000000
2,7,307,331,666,65,,FFFFFFFF
3,10,0,0,1280,720,,
3,11,0,0,1280,720,,FF000000
3,12,307,331,666,65,,FFFFFFFF
4,15,0,0,1280,720,,
4,16,0,0,1280,720,,FF000000
4,17,307,331,666,65,,FFFFFFFF
5,20,0,0,1280,720,,
5,21,0,0,1280,720,,FF000000
5,22,307,331,666,65,,FFFFFFFF
6,25,0,0,1280,720,,
6,26,0,0,1280,720,,FF000000
6,27,307,331,666,65,,FFFFFFFF
7,30,0,0,1280,720,,
7,31,0,0,1280,720,,FF000000
7,32,307,331,666,65,,FFFFFFFF
8,35,0,0,1280,720,,
8,36,0,0,1280,720,,FF000000
8,37,307,331,666,65,,FFFFFFFF
9,40,0,0,1280,720,,
9,41,0,0,1280,720,,FF000000
9,42,307,331,666,65,,FFFFFFFF
10,45,0,0,1280,720,,
10,46,0,0,1280,720,,FF000000
10,47,307,331,666,65,,FFFFFFFF
11,50,0,0,1280,720,,
11,51,0,0,1280,720,,FF000000
11,52,307,331,666,65,,FFFFFFFF
12,55,0,0,1280,720,,
12,56,0,0,1280,720,,FF000000
12,57,307,331,666,65,,FFFFFFFF
13,60,0,0,1280,720,,
13,61,0,0,1280,720,,FF000000
13,62,307,331,666,65,,FFFFFFFF
14,65,0,0,1280,720,,
14,66,0,0,1280,720,,FF000000
14,67,307,331,666,65,,FFFFFFFF
15,70,0,0,1280,720,,
15,71,0,0,1280,720,,FF000000
15,72,307,331,666,65,,FFFFFFFF
16,75,0,0,1280,720,,
16,76,0,0,1280,720,,FF000000
16,77,307,331,666,65,,FFFFFFFF
17,80,0,0,1280,720,,
17,81,0,0,1280,720,,FF000000
17,82,307,331,666,65,,FFFFFFFF
18,85,0,0,1280,720,,
18,86,0,0,1280,720,,FF000000
18,87,307,331,666,65,,FFFFFFFF
19,90,0,0,1280,720,,
19,91,0,0,1280,720,,FF000000
19,92,307,331,666,65,,FFFFFFFF
20,95,0,0,1280,720,,
20,96,0,0,1280,720,,FF000000
20,97,307,331,666,65,,FFFFFFFF
21,100,0,0,1280,720,,
21,101,0,0,1280,720,,FF000000
21,102,307,331,666,65,,FFFFFFFF
22,105,0,0,1280,720,,
22,106,0,0,1280,720,,FF000000
22,107,307,331,666,65,,FFFFFFFF
23,110,0,0,1280,720,,
23,111,0,0,1280,720,,FF000000
23,112,307,331,666,65,,FFFFFFFF
24,115,0,0,1280,720,,
24,116,0,0,1280,720,,FF000000
24,117,307,331,666,65,,FFFFFFFF
25,120,0,0,1280,720,,
25,121,0,0,1280,720,,FF000000
25,122,307,331,666,65,,FFFFFFFF
26,125,0,0,1280,720,,
26,126,0,0,1280,720,,FF000000
26,127,307,331,666,65,,FFFFFFFF
27,130,0,0,1280,720,,
27,131,0,0,1280,720,,FF000000
27,132,307,331,666,65,,FFFFFFFF
28,135,0,0,1280,720,,
28,136,0,0,1280,720,,FF000000
28,137,307,331,666,65,,FFFFFFFF
29,140,0,0,1280,720,,
29,141,0,0,1280,720,,FF000000
29,142,307,331,666,65,,FFFFFFFF
30,145,0,0,1280,720,,
30,146,0,0,1280,720,,FF000000
30,147,307,331,666,65,,FFFFFFFF
31,150,0,0,1280,720,,
31,151,0,0,1280,720,,FF000000
31,152,307,331,666,65,,FFFFFFFF
32,155,0,0,1280,720,,
32,156,0,0,1280,720,,FF000000
32,157,307,331,666,65,,FFFFFFFF
33,160,0,0,1280,720,,
33,161,0,0,1280,720,,FF000000
33,162,307,331,666,65,,FFFFFFFF
34,165,0,0,1280,720,,
34,166,0,0,1280,720,,FF000000
34,167,307,331,666,65,,FFFFFFFF
35,170,0,0,1280,720,,
35,171,0,0,1280,720,,FF000000
35,172,307,331,666,65,,FFFFFFFF
36,175,0,0,1280,720,,
36,176,0,0,1280,720,,FF000000
36,177,307,331,666,65,,FFFFFFFF
37,180,0,0,1280,720,,
37,181,0,0,1280,720,,FF000000
37,182,307,331,666,65,,FFFFFFFF
38,185,0,0,1280,720,,
38,186,0,0,1280,720,,FF000000
38,187,307,331,666,65,,FFFFFFFF
39,190,0,0,1280,720,,
39,191,0,0,1280,720,,FF000000
39,192,307,331,666,65,,FFFFFFFF
40,195,0,0,1280,720,,
40,196,0,0,1280,720,,FF000000
40,197,307,331,666,65,,FFFFFFFF
41,200,0,0,1280,720,,
41,201,0,0,1280,720,,FF000000
41,202,307,331,666,65,,FFFFFFFF
42,205,0,0,1280,720,,
42,206,0,0,1280,720,,FF000000
42,207,307,331,666,65,,FFFFFFFF
43,210,0,0,1280,720,,
43,211,0,0,1280,720,,FF000000
43,212,307,331,666,65,,FFFFFFFF
44,215,0,0,1280,720,,
44,216,0,0,1280,720,,FF000000
44,217,307,331,666,65,,FFFFFFFF
45,220,0,0,1280,720,,
45,221,0,0,1280,720,,FF000000
45,222,307,331,666,65,,FFFFFFFF
47,225,0,0,1280,720,,
47,226,0,0,1280,720,,FF000000
47,227,307,331,666,65,,FFFFFFFF
48,230,0,0,1280,720,,
48,231,0,0,1280,720,,FF000000
48,232,307,331,666,65,,FFFFFFFF
49,235,0,0,1280,720,,
49,236,0,0,1280,720,,FF000000
49,237,307,331,666,65,,FFFFFFFF
50,240,0,0,1280,720,,
50,241,0,0,1280,720,,FF000000
50,242,307,331,666,65,,FFFFFFFF
51,245,0,0,1280,720,,
51,246,0,0,1280,720,,FF000000
51,247,307,331,666,65,,FFFFFFFF
52,250,0,0,1280,720,,
52,251,0,0,1280,720,,FF000000
52,252,307,331,666,65,,FFFFFFFF
53,255,0,0,1280,720,,
53,256,0,0,1280,720,,FF000000
53,257,307,331,666,65,,FFFFFFFF
54,260,0,0,1280,720,,
54,261,0,0,1280,720,,FF000000
54,262,307,331,666,65,,FFFFFFFF
55,265,0,0,1280,720,,
55,266,0,0,1280,720,,FF000000
55,267,307,331,666,65,,FFFFFFFF
56,270,0,0,1280,720,,
56,271,0,0,1280,720,,FF000000
56,272,307,331,666,65,,FFFFFFFF
57,275,0,0,1280,720,,
57,276,0,0,1280,720,,FF000000
57,277,307,331,666,65,,FFFFFFFF
58,280,0,0,1280,720,,
58,281,0,0,1280,720,,FF000000
58,282,307,331,666,65,,FFFFFFFF
59,285,0,0,1280,720,,
59,286,0,0,1280,720,,FF000000
59,287,307,331,666,65,,FFFFFFFF
60,290,0,0,1280,720,,
60,291,0,0,1280,720,,FF000000
60,292,307,331,666,65,,FFFFFFFF
61,295,0,0,1280,720,,
61,296,0,0,1280,720,,FF000000
61,297,307,331,666,65,,FFFFFFFF
62,300,0,0,1280,720,,
62,301,0,0,1280,720,,FF000000
62,302,307,331,666,65,,FFFFFFFF
63,305,0,0,1280,720,,
63,306,0,0,1280,720,,FF000000
63,307,307,331,666,65,,FFFFFFFF
64,310,0,0,1280,720,,
64,311,0,0,1280,720,,FF000000
64,312,307,331,666,65,,FFFFFFFF
65,315,0,0,1280,720,,
65,316,0,0,1280,720,,FF000000
65,317,307,331,666,65,,FFFFFFFF
66,320,0,0,1280,720,,
66,321,0,0,1280,720,,FF000000
66,322,307,331,666,65,,FFFFFFFF
67,325,0,0,1280,720,,
67,326,0,0,1280,720,,FF000000
67,327,307,331,666,65,,FFFFFFFF
68,330,0,0,1280,720,,
68,331,0,0,1280,720,,FF000000
68,332,307,331,666,65,,FFFFFFFF
69,335,0,0,1280,720,,
69,336,0,0,1280,720,,FF000000
69,337,307,331,666,65,,FFFFFFFF
70,340,0,0,1280,720,,
70,341,0,0,1280,720,,FF000000
70,342,307,331,666,65,,FFFFFFFF
71,345,0,0,1280,720,,
71,346,0,0,1280,720,,FF000000
71,347,307,331,666,65,,FFFFFFFF
72,350,0,0,1280,720,,
72,351,0,0,1280,720,,FF000000
72,352,307,331,666,65,,FFFFFFFF
73,355,0,0,1280,720,,
73,356,0,0,1280,720,,FF000000
73,357,307,331,666,65,,FFFFFFFF
74,360,0,0,1280,720,,
74,361,0,0,1280,720,,FF000000
74,362,307,331,666,65,,FFFFFFFF
75,365,0,0,1280,720,,
75,366,0,0,1280,720,,FF000000
75,367,307,331,666,65,,FFFFFFFF
76,370,0,0,1280,720,,
76,371,0,0,1280,720,,FF000000
76,372,307,331,666,65,,FFFFFFFF
77,375,0,0,1280,720,,
77,376,0,0,1280,720,,FF000000
77,377,307,331,666,65,,FFFFFFFF
78,380,0,0,1280,720,,
78,381,0,0,1280,720,,FF000000
78,382,307,331,666,65,,FEFFFFFF
79,385,0,0,1280,720,,
79,386,0,0,1280,720,,FF000000
79,387,307,331,666,65,,F9FFFFFF
80,390,0,0,1280,720,,
80,391,0,0,1280,720,,FF000000
80,392,307,331,666,65,,E7FFFFFF
81,395,0,0,1280,720,,
81,396,0,0,1280,720,,FF000000
81,397,307,331,666,65,,D7FFFFFF
82,400,0,0,1280,720,,
82,401,0,0,1280,720,,FF000000
82,402,307,331,666,65,,B5FFFFFF
83,405,0,0,1280,720,,
83,406,0,0,1280,720,,FF000000
83,407,307,331,666,65,,A5FFFFFF
84,410,0,0,1280,720,,
84,411,0,0,1280,720,,FF000000
84,412,307,331,666,65,,83FFFFFF
85,415,0,0,1280,720,,
85,416,0,0,1280,720,,FF000000
85,417,307,331,666,65,,73FFFFFF
86,420,0,0,1280,720,,
86,421,0,0,1280,720,,FF000000
86,422,307,331,666,65,,51FFFFFF
87,425,0,0,1280,720,,
87,426,0,0,1280,720,,FF000000
87,427,307,331,666,65,,41FFFFFF
88,430,0,0,1280,720,,
88,431,0,0,1280,720,,FF000000
88,432,307,331,666,65,,1FFFFFFF
89,435,0,0,1280,720,,
89,436,0,0,1280,720,,FF000000
89,437,307,331,666,65,,0FFFFFFF
90,440,0,0,1280,720,,
90,441,0,0,1280,720,,FF000000
90,442,307,331,666,65,,07FFFFFF
91,445,0,0,1280,720,,
91,446,0,0,1280,720,,FF000000
92,449,0,0,1280,720,,
92,450,0,0,1280,720,,FF000000
93,453,0,0,1280,720,,
93,454,0,0,1280,720,,FF000000
94,457,0,0,1280,720,,
94,458,0,0,1280,720,,FF000000
94,459,378,155,525,90,,22FFFFFF
94,459,512,306,262,108,,22FFFFFF
95,462,0,0,1280,720,,
95,463,0,0,1280,720,,FF000000
95,464,378,155,525,90,,44FFFFFF
95,464,512,306,262,108,,44FFFFFF
96,467,0,0,1280,720,,
96,468,0,0,1280,720,,FF000000
96,469,378,155,525,90,,66FFFFFF
96,469,512,306,262,108,,66FFFFFF
97,472,0,0,1280,720,,
97,473,0,0,1280,720,,FF000000
97,474,378,155,525,90,,88FFFFFF
97,474,512,306,262,108,,88FFFFFF
98,477,0,0,1280,720,,
98,478,0,0,1280,720,,FF000000
98,479,378,155,525,90,,AAFFFFFF
98,479,512,306,262,108,,AAFFFFFF
99,482,0,0,1280,720,,
99,483,0,0,1280,720,,FF000000
99,484,378,155,525,90,,CCFFFFFF
99,484,512,306,262,108,,CCFFFFFF
100,487,0,0,1280,720,,
100,488,0,0,1280,720,,FF000000
100,489,378,155,525,90,,EEFFFFFF
100,489,512,306,262,108,,EEFFFFFF
101,492,0,0,1280,720,,
101,493,0,0,1280,720,,FF000000
101,494,378,155,525,90,,FFFFFFFF
101,494,512,306,262,108,,FFFFFFFF
102,497,0,0,1280,720,,
102,498,0,0,1280,720,,FF000000
102,499,378,155,525,90,,FFFFFFFF
102,499,512,306,262,108,,FFFFFFFF
103,502,0,0,1280,720,,
103,503,0,0,1280,720,,FF000000
103,504,378,155,525,90,,FFFFFFFF
103,504,512,306,262,108,,FFFFFFFF
104,507,0,0,1280,720,,
104,508,0,0,1280,720,,FF000000
104,509,378,155,525,90,,FFFFFFFF
104,509,512,306,262,108,,FFFFFFFF
105,512,0,0,1280,720,,
105,513,0,0,1280,720,,FF000000
105,514,378,155,525,90,,FFFFFFFF
105,514,512,306,262,108,,FFFFFFFF
106,517,0,0,1280,720,,
106,518,0,0,1280,720,,FF000000
106,519,378,155,525,90,,FFFFFFFF
106,519,512,306,262,108,,FFFFFFFF
107,522,0,0,1280,720,,
107,523,0,0,1280,720,,FF000000
107,524,378,155,525,90,,FFFFFFFF
107,524,512,306,262,108,,FFFFFFFF
108,527,0,0,1280,720,,
108,528,0,0,1280,720,,FF000000
108,529,378,155,525,90,,FEFFFFFF
108,529,512,306,262,108,,FEFFFFFF
109,532,0,0,1280,720,,
109,533,0,0,1280,720,,FF000000
109,534,378,155,525,90,,DCFFFFFF
109,534,512,306,262,108,,DCFFFFFF
110,537,0,0,1280,720,,
110,538,0,0,1280,720,,FF000000
110,539,378,155,525,90,,BAFFFFFF
110,539,512,306,262,108,,BAFFFFFF
111,542,0,0,1280,720,,
111,543,0,0,1280,720,,FF000000
111,544,378,155,525,90,,98FFFFFF
111,544,512,306,262,108,,98FFFFFF
112,547,0,0,1280,720,,
112,548,0,0,1280,720,,FF000000
112,549,378,155,525,90,,76FFFFFF
112,549,512,306,262,108,,76FFFFFF
113,552,0,0,1280,720,,
113,553,0,0,1280,720,,FF000000
113,554,378,155,525,90,,54FFFFFF
113,554,512,306,262,108,,54FFFFFF
114,557,0,0,1280,720,,
114,558,0,0,1280,720,,FF000000
114,559,378,155,525,90,,43FFFFFF
114,559,512,306,262,108,,43FFFFFF
115,562,0,0,1280,720,,
115,563,0,0,1280,720,,FF000000
115,564,378,155,525,90,,21FFFFFF
115,564,512,306,262,108,,21FFFFFF
116,567,0,0,1280,720,,
116,568,0,0,1280,720,,FF000000
116,569,390,162,499,72,,FFFFFFFF
116,569,518,317,243,86,,FFFFFFFF
117,572,0,0,1280,720,,
117,573,0,0,1280,720,,FF000000
117,574,390,162,499,72,,FFFFFFFF
117,574,518,317,243,86,,FFFFFFFF
118,577,0,0,1280,720,,
118,578,0,0,1280,720,,FF000000
118,579,390,162,499,72,,FFFFFFFF
118,579,518,317,243,86,,FFFFFFFF
119,582,0,0,1280,720,,
119,583,0,0,1280,720,,FF000000
119,584,390,162,499,72,,FFFFFFFF
119,584,518,317,243,86,,FFFFFFFF
120,587,0,0,1280,720,,
120,588,0,0,1280,720,,FF000000
120,589,390,162,499,72,,FFFFFFFF
120,589,518,317,243,86,,FFFFFFFF
121,592,0,0,1280,720,,
121,593,0,0,1280,720,,FF000000
121,594,390,162,499,72,,FFFFFFFF
121,594,518,317,243,86,,FFFFFFFF
122,597,0,0,1280,720,,
122,598,0,0,1280,720,,FF000000
122,599,390,162,499,72,,FFFFFFFF
122,599,518,317,243,86,,FFFFFFFF
123,602,0,0,1280,720,,
123,603,0,0,1280,720,,FF000000
123,604,390,162,499,72,,FFFFFFFF
123,604,518,317,243,86,,FFFFFFFF
124,607,0,0,1280,720,,
124,608,0,0,1280,720,,FF000000
124,609,390,162,499,72,,FFFFFFFF
124,609,518,317,243,86,,FFFFFFFF
125,612,0,0,1280,720,,
125,613,0,0,1280,720,,FF000000
125,614,390,162,499,72,,FFFFFFFF
125,614,518,317,243,86,,FFFFFFFF
126,617,0,0,1280,720,,
126,618,0,0,1280,720,,FF000000
126,619,390,162,499,72,,FFFFFFFF
126,619,518,317,243,86,,FFFFFFFF
127,622,0,0,1280,720,,
127,623,0,0,1280,720,,FF000000
127,624,390,162,499,72,,FFFFFFFF
127,624,518,317,243,86,,FFFFFFFF
128,627,0,0,1280,720,,
128,628,0,0,1280,720,,FF000000
128,629,390,162,499,72,,FFFFFFFF
128,629,518,317,243,86,,FFFFFFFF
129,632,0,0,1280,720,,
129,633,0,0,1280,720,,FF000000
129,634,390,162,499,72,,FFFFFFFF
129,634,518,317,243,86,,FFFFFFFF
130,637,0,0,1280,720,,
130,638,0,0,1280,720,,FF000000
130,639,390,162,499,72,,FFFFFFFF
130,639,518,317,243,86,,FFFFFFFF
131,642,0,0,1280,720,,
131,643,0,0,1280,720,,FF000000
131,644,390,162,499,72,,FFFFFFFF
131,644,518,317,243,86,,FFFFFFFF
132,647,0,0,1280,720,,
132,648,0,0,1280,720,,FF000000
132,649,390,162,499,72,,FFFFFFFF
132,649,518,317,243,86,,FFFFFFFF
133,652,0,0,1280,720,,
133,653,0,0,1280,720,,FF000000
133,654,390,162,499,72,,FFFFFFFF
133,654,518,317,243,86,,FFFFFFFF
134,657,0,0,1280,720,,
134,658,0,0,1280,720,,FF000000
134,659,390,162,499,72,,FFFFFFFF
134,659,518,317,243,86,,FFFFFFFF
135,662,0,0,1280,720,,
135,663,0,0,1280,720,,FF000000
135,664,390,162,499,72,,FFFFFFFF
135,664,518,317,243,86,,FFFFFFFF
136,667,0,0,1280,720,,
136,668,0,0,1280,720,,FF000000
136,669,390,162,499,72,,FFFFFFFF
136,669,518,317,243,86,,FFFFFFFF
137,672,0,0,1280,720,,
137,673,0,0,1280,720,,FF000000
137,674,390,162,499,72,,FFFFFFFF
137,674,518,317,243,86,,FFFFFFFF
138,677,0,0,1280,720,,
138,678,0,0,1280,720,,FF000000
138,679,390,162,499,72,,FFFFFFFF
138,679,518,317,243,86,,FFFFFFFF
140,682,0,0,1280,720,,
140,683,0,0,1280,720,,FF000000
140,684,390,162,499,72,,FFFFFFFF
140,684,518,317,243,86,,FFFFFFFF
141,687,0,0,1280,720,,
141,688,0,0,1280,720,,FF000000
141,689,390,162,499,72,,FFFFFFFF
141,689,518,317,243,86,,FFFFFFFF
142,692,0,0,1280,720,,
142,693,0,0,1280,720,,FF000000
142,694,390,162,499,72,,FFFFFFFF
142,694,518,317,243,86,,FFFFFFFF
143,697,0,0,1280,720,,
143,698,0,0,1280,720,,FF000000
143,699,390,162,499,72,,FFFFFFFF
143,699,518,317,243,86,,FFFFFFFF
144,702,0,0,1280,720,,
144,703,0,0,1280,720,,FF000000
144,704,390,162,499,72,,FFFFFFFF
144,704,518,317,243,86,,FFFFFFFF
145,707,0,0,1280,720,,
145,708,0,0,1280,720,,FF000000
145,709,390,162,499,72,,FFFFFFFF
145,709,518,317,243,86,,FFFFFFFF
146,712,0,0,1280,720,,
146,713,0,0,1280,720,,FF000000
146,714,390,162,499,72,,FFFFFFFF
146,714,518,317,243,86,,FFFFFFFF
147,717,0,0,1280,720,,
147,718,0,0,1280,720,,FF000000
147,719,390,162,499,72,,FFFFFFFF
147,719,518,317,243,86,,FFFFFFFF
148,722,0,0,1280,720,,
148,723,0,0,1280,720,,FF000000
148,724,390,162,499,72,,FFFFFFFF
148,724,518,317,243,86,,FFFFFFFF
149,727,0,0,1280,720,,
149,728,0,0,1280,720,,FF000000
149,729,390,162,499,72,,FFFFFFFF
149,729,518,317,243,86,,FFFFFFFF
150,732,0,0,1280,720,,
150,733,0,0,1280,720,,FF000000
150,734,390,162,499,72,,FFFFFFFF
150,734,518,317,243,86,,FFFFFFFF
151,737,0,0,1280,720,,
151,738,0,0,1280,720,,FF000000
151,739,390,162,499,72,,FFFFFFFF
151,739,518,317,243,86,,FFFFFFFF
152,742,0,0,1280,720,,
152,743,0,0,1280,720,,FF000000
152,744,390,162,499,72,,FFFFFFFF
152,744,518,317,243,86,,FFFFFFFF
153,747,0,0,1280,720,,
153,748,0,0,1280,720,,FF000000
153,749,390,162,499,72,,FFFFFFFF
153,749,518,317,243,86,,FFFFFFFF
154,752,0,0,1280,720,,
154,753,0,0,1280,720,,FF000000
154,754,390,162,499,72,,FFFFFFFF
154,754,518,317,243,86,,FFFFFFFF
155,757,0,0,1280,720,,
155,758,0,0,1280,720,,FF000000
155,759,390,162,499,72,,FFFFFFFF
155,759,518,317,243,86,,FFFFFFFF
156,762,0,0,1280,720,,
156,763,0,0,1280,720,,FF000000
156,764,390,162,499,72,,FFFFFFFF
156,764,518,317,243,86,,FFFFFFFF
157,767,0,0,1280,720,,
157,768,0,0,1280,720,,FF000000
157,769,390,162,499,72,,FFFFFFFF
157,769,518,317,243,86,,FFFFFFFF
158,772,0,0,1280,720,,
158,773,0,0,1280,720,,FF000000
158,774,390,162,499,72,,FFFFFFFF
158,774,518,317,243,86,,FFFFFFFF
159,777,0,0,1280,720,,
159,778,0,0,1280,720,,FF000000
159,779,390,162,499,72,,FFFFFFFF
159,779,518,317,243,86,,FFFFFFFF
160,782,0,0,1280,720,,
160,783,0,0,1280,720,,FF000000
160,784,390,162,499,72,,FFFFFFFF
160,784,518,317,243,86,,FFFFFFFF
161,787,0,0,1280,720,,
161,788,0,0,1280,720,,FF000000
161,789,390,162,499,72,,FFFFFFFF
161,789,518,317,243,86,,FFFFFFFF
162,792,0,0,1280,720,,
162,793,0,0,1280,720,,FF000000
162,794,390,162,499,72,,FFFFFFFF
162,794,518,317,243,86,,FFFFFFFF
163,797,0,0,1280,720,,
163,798,0,0,1280,720,,FF000000
163,799,390,162,499,72,,FFFFFFFF
163,799,518,317,243,86,,FFFFFFFF
164,802,0,0,1280,720,,
164,803,0,0,1280,720,,FF000000
164,804,390,162,499,72,,FFFFFFFF
164,804,518,317,243,86,,FFFFFFFF
165,807,0,0,1280,720,,
165,808,0,0,1280,720,,FF000000
165,809,390,162,499,72,,FFFFFFFF
165,809,518,317,243,86,,FFFFFFFF
166,812,0,0,1280,720,,
166,813,0,0,1280,720,,FF000000
166,814,390,162,499,72,,FFFFFFFF
166,814,518,317,243,86,,FFFFFFFF
167,817,0,0,1280,720,,
167,818,0,0,1280,720,,FF000000
167,819,390,162,499,72,,FFFFFFFF
167,819,518,317,243,86,,FFFFFFFF
168,822,0,0,1280,720,,
168,823,0,0,1280,720,,FF000000
168,824,390,162,499,72,,FFFFFFFF
168,824,518,317,243,86,,FFFFFFFF
169,827,0,0,1280,720,,
169,828,0,0,1280,720,,FF000000
169,829,390,162,499,72,,FFFFFFFF
169,829,518,317,243,86,,FFFFFFFF
170,832,0,0,1280,720,,
170,833,0,0,1280,720,,FF000000
170,834,390,162,499,72,,FFFFFFFF
170,834,518,317,243,86,,FFFFFFFF
171,837,0,0,1280,720,,
171,838,0,0,1280,720,,FF000000
171,839,390,162,499,72,,FFFFFFFF
171,839,518,317,243,86,,FFFFFFFF
172,842,0,0,1280,720,,
172,843,0,0,1280,720,,FF000000
172,844,390,162,499,72,,FFFFFFFF
172,844,518,317,243,86,,FFFFFFFF
173,847,0,0,1280,720,,
173,848,0,0,1280,720,,FF000000
173,849,390,162,499,72,,FFFFFFFF
173,849,518,317,243,86,,FFFFFFFF
174,852,0,0,1280,720,,
174,853,0,0,1280,720,,FF000000
174,854,390,162,499,72,,FFFFFFFF
174,854,518,317,243,86,,FFFFFFFF
175,857,0,0,1280,720,,
175,858,0,0,1280,720,,FF000000
175,859,390,162,499,72,,FFFFFFFF
175,859,518,317,243,86,,FFFFFFFF
176,862,0,0,1280,720,,
176,863,0,0,1280,720,,FF000000
176,864,390,162,499,72,,FFFFFFFF
176,864,518,317,243,86,,FFFFFFFF
177,867,0,0,1280,720,,
177,868,0,0,1280,720,,FF000000
177,869,390,162,499,72,,FFFFFFFF
177,869,518,317,243,86,,FFFFFFFF
178,872,0,0,1280,720,,
178,873,0,0,1280,720,,FF000000
178,874,390,162,499,72,,FFFFFFFF
178,874,518,317,243,86,,FFFFFFFF
179,877,0,0,1280,720,,
179,878,0,0,1280,720,,FF000000
179,879,390,162,499,72,,FFFFFFFF
179,879,518,317,243,86,,FFFFFFFF
180,882,0,0,1280,720,,
180,883,0,0,1280,720,,FF000000
180,884,390,162,499,72,,FFFFFFFF
180,884,518,317,243,86,,FFFFFFFF
181,887,0,0,1280,720,,
181,888,0,0,1280,720,,FF000000
181,889,390,162,499,72,,FFFFFFFF
181,889,518,317,243,86,,FFFFFFFF
182,892,0,0,1280,720,,
182,893,0,0,1280,720,,FF000000
182,894,390,162,499,72,,FFFFFFFF
182,894,518,317,243,86,,FFFFFFFF
183,897,0,0,1280,720,,
183,898,0,0,1280,720,,FF000000
183,899,390,162,499,72,,FFFFFFFF
183,899,518,317,243,86,,FFFFFFFF
184,902,0,0,1280,720,,
184,903,0,0,1280,720,,FF000000
184,904,390,162,499,72,,FFFFFFFF
184,904,518,317,243,86,,FFFFFFFF
185,907,0,0,1280,720,,
185,908,0,0,1280,720,,FF000000
185,909,390,162,499,72,,FFFFFFFF
185,909,518,317,243,86,,FFFFFFFF
186,912,0,0,1280,720,,
186,913,0,0,1280,720,,FF000000
186,914,390,162,499,72,,FFFFFFFF
186,914,518,317,243,86,,FFFFFFFF
187,917,0,0,1280,720,,
187,918,0,0,1280,720,,FF000000
187,919,390,162,499,72,,FFFFFFFF
187,919,518,317,243,86,,FFFFFFFF
188,922,0,0,1280,720,,
188,923,0,0,1280,720,,FF000000
188,924,390,162,499,72,,FFFFFFFF
188,924,518,317,243,86,,FFFFFFFF
189,927,0,0,1280,720,,
189,928,0,0,1280,720,,FF000000
189,929,390,162,499,72,,FFFFFFFF
189,929,518,317,243,86,,FFFFFFFF
190,932,0,0,1280,720,,
190,933,0,0,1280,720,,FF000000
190,934,390,162,499,72,,FFFFFFFF
190,934,518,317,243,86,,FFFFFFFF
191,937,0,0,1280,720,,
191,938,0,0,1280,720,,FF000000
191,939,390,162,499,72,,FFFFFFFF
191,939,518,317,243,86,,FFFFFFFF
192,942,0,0,1280,720,,
192,943,0,0,1280,720,,FF000000
192,944,390,162,499,72,,FFFFFFFF
192,944,518,317,243,86,,FFFFFFFF
193,947,0,0,1280,720,,
193,948,0,0,1280,720,,FF000000
193,949,390,162,499,72,,FFFFFFFF
193,949,518,317,243,86,,FFFFFFFF
194,952,0,0,1280,720,,
194,953,0,0,1280,720,,FF000000
194,954,390,162,499,72,,FFFFFFFF
194,954,518,317,243,86,,FFFFFFFF
195,957,0,0,1280,720,,
195,958,0,0,1280,720,,FF000000
195,959,390,162,499,72,,FFFFFFFF
195,959,518,317,243,86,,FFFFFFFF
196,962,0,0,1280,720,,
196,963,0,0,1280,720,,FF000000
196,964,390,162,499,72,,FFFFFFFF
196,964,518,317,243,86,,FFFFFFFF
197,967,0,0,1280,720,,
197,968,0,0,1280,720,,FF000000
197,969,390,162,499,72,,FFFFFFFF
197,969,518,317,243,86,,FFFFFFFF
198,972,0,0,1280,720,,
198,973,0,0,1280,720,,FF000000
198,974,390,162,499,72,,FFFFFFFF
198,974,518,317,243,86,,FFFFFFFF
199,977,0,0,1280,720,,
199,978,0,0,1280,720,,FF000000
199,979,390,162,499,72,,FEFFFFFF
199,979,518,317,243,86,,FEFFFFFF
200,982,0,0,1280,720,,
200,983,0,0,1280,720,,FF000000
200,984,390,162,499,72,,F3FFFFFF
200,984,518,317,243,86,,F3FFFFFF
201,987,0,0,1280,720,,
201,988,0,0,1280,720,,FF000000
201,989,390,162,499,72,,EDFFFFFF
201,989,518,317,243,86,,EDFFFFFF
202,992,0,0,1280,720,,
202,993,0,0,1280,720,,FF000000
202,994,390,162,499,72,,D7FFFFFF
202,994,518,317,243,86,,D7FFFFFF
203,997,0,0,1280,720,,
203,998,0,0,1280,720,,FF000000
203,999,390,162,499,72,,B5FFFFFF
203,999,518,317,243,86,,B5FFFFFF
204,1002,0,0,1280,720,,
204,1003,0,0,1280,720,,FF000000
204,1004,390,162,499,72,,94FFFFFF
204,1004,518,317,243,86,,94FFFFFF
205,1007,0,0,1280,720,,
205,1008,0,0,1280,720,,FF000000
205,1009,390,162,499,72,,83FFFFFF
205,1009,518,317,243,86,,83FFFFFF
206,1012,0,0,1280,720,,
206,1013,0,0,1280,720,,FF000000
206,1014,390,162,499,72,,62FFFFFF
206,1014,518,317,243,86,,62FFFFFF
207,1017,0,0,1280,720,,
207,1018,0,0,1280,720,,FF000000
207,1019,390,162,499,72,,41FFFFFF
207,1019,518,317,243,86,,41FFFFFF
208,1022,0,0,1280,720,,
208,1023,0,0,1280,720,,FF000000
208,1024,390,162,499,72,,30FFFFFF
208,1024,518,317,243,86,,30FFFFFF
209,1027,0,0,1280,720,,
209,1028,0,0,1280,720,,FF000000
209,1029,390,162,499,72,,17FFFFFF
209,1029,518,317,243,86,,17FFFFFF
210,1032,0,0,1280,720,,
210,1033,0,0,1280,720,,FF000000
210,1034,390,162,499,72,,0FFFFFFF
210,1034,518,317,243,86,,0FFFFFFF
211,1037,0,0,1280,720,,
211,1038,0,0,1280,720,,FF000000
211,1039,390,162,499,72,,07FFFFFF
211,1039,518,317,243,86,,07FFFFFF
212,1042,0,0,1280,720,,
212,1043,0,0,1280,720,,FF000000
213,1046,0,0,1280,720,,
213,1047,0,0,1280,720,,FF000000
214,1050,0,0,1280,720,,
214,1051,0,0,1280,720,,FF000000
215,1054,0,0,1280,720,,
216,1057,0,0,1280,720,,
217,1060,0,0,1280,720,,
218,1063,0,0,1280,720,,
219,1066,0,0,1280,720,,
220,1069,0,0,1280,720,,
221,1072,0,0,1280,720,,
221,1073,0,0,1280,720,,
223,1076,0,0,1280,720,,
223,1077,0,0,1280,720,,
224,1080,0,0,1280,720,,
224,1081,0,0,1280,720,,
226,1084,0,0,1280,720,,
226,1085,0,0,1280,720,,
227,1088,0,0,1280,720,,
227,1089,0,0,1280,720,,
228,1092,0,0,1280,720,,
228,1093,0,0,1280,720,,
229,1096,0,0,1280,720,,
229,1097,0,0,1280,720,,
230,1100,0,0,1280,720,,
230,1101,0,0,1280,720,,
231,1104,0,0,1280,720,,
231,1105,0,0,1280,720,,
232,1108,0,0,1280,720,,
232,1109,0,0,1280,720,,
233,1112,0,0,1280,720,,
233,1113,0,0,1280,720,,
234,1116,0,0,1280,720,,
234,1117,0,0,1280,720,,
236,1120,0,0,1280,720,,
236,1121,0,0,1280,720,,
237,1124,0,0,1280,720,,
237,1125,0,0,1280,720,,
238,1128,0,0,1280,720,,
238,1129,0,0,1280,720,,
239,1132,0,0,1280,720,,
239,1133,0,0,1280,720,,
240,1136,0,0,1280,720,,
240,1137,0,0,1280,720,,
241,1140,0,0,1280,720,,
241,1141,0,0,1280,720,,
1 frame draw x y w h rot col
2 1 0 0 0 1280 720
3 1 1 0 0 1280 720 FF000000
4 1 2 307 331 666 65 FFFFFFFF
5 2 5 0 0 1280 720
6 2 6 0 0 1280 720 FF000000
7 2 7 307 331 666 65 FFFFFFFF
8 3 10 0 0 1280 720
9 3 11 0 0 1280 720 FF000000
10 3 12 307 331 666 65 FFFFFFFF
11 4 15 0 0 1280 720
12 4 16 0 0 1280 720 FF000000
13 4 17 307 331 666 65 FFFFFFFF
14 5 20 0 0 1280 720
15 5 21 0 0 1280 720 FF000000
16 5 22 307 331 666 65 FFFFFFFF
17 6 25 0 0 1280 720
18 6 26 0 0 1280 720 FF000000
19 6 27 307 331 666 65 FFFFFFFF
20 7 30 0 0 1280 720
21 7 31 0 0 1280 720 FF000000
22 7 32 307 331 666 65 FFFFFFFF
23 8 35 0 0 1280 720
24 8 36 0 0 1280 720 FF000000
25 8 37 307 331 666 65 FFFFFFFF
26 9 40 0 0 1280 720
27 9 41 0 0 1280 720 FF000000
28 9 42 307 331 666 65 FFFFFFFF
29 10 45 0 0 1280 720
30 10 46 0 0 1280 720 FF000000
31 10 47 307 331 666 65 FFFFFFFF
32 11 50 0 0 1280 720
33 11 51 0 0 1280 720 FF000000
34 11 52 307 331 666 65 FFFFFFFF
35 12 55 0 0 1280 720
36 12 56 0 0 1280 720 FF000000
37 12 57 307 331 666 65 FFFFFFFF
38 13 60 0 0 1280 720
39 13 61 0 0 1280 720 FF000000
40 13 62 307 331 666 65 FFFFFFFF
41 14 65 0 0 1280 720
42 14 66 0 0 1280 720 FF000000
43 14 67 307 331 666 65 FFFFFFFF
44 15 70 0 0 1280 720
45 15 71 0 0 1280 720 FF000000
46 15 72 307 331 666 65 FFFFFFFF
47 16 75 0 0 1280 720
48 16 76 0 0 1280 720 FF000000
49 16 77 307 331 666 65 FFFFFFFF
50 17 80 0 0 1280 720
51 17 81 0 0 1280 720 FF000000
52 17 82 307 331 666 65 FFFFFFFF
53 18 85 0 0 1280 720
54 18 86 0 0 1280 720 FF000000
55 18 87 307 331 666 65 FFFFFFFF
56 19 90 0 0 1280 720
57 19 91 0 0 1280 720 FF000000
58 19 92 307 331 666 65 FFFFFFFF
59 20 95 0 0 1280 720
60 20 96 0 0 1280 720 FF000000
61 20 97 307 331 666 65 FFFFFFFF
62 21 100 0 0 1280 720
63 21 101 0 0 1280 720 FF000000
64 21 102 307 331 666 65 FFFFFFFF
65 22 105 0 0 1280 720
66 22 106 0 0 1280 720 FF000000
67 22 107 307 331 666 65 FFFFFFFF
68 23 110 0 0 1280 720
69 23 111 0 0 1280 720 FF000000
70 23 112 307 331 666 65 FFFFFFFF
71 24 115 0 0 1280 720
72 24 116 0 0 1280 720 FF000000
73 24 117 307 331 666 65 FFFFFFFF
74 25 120 0 0 1280 720
75 25 121 0 0 1280 720 FF000000
76 25 122 307 331 666 65 FFFFFFFF
77 26 125 0 0 1280 720
78 26 126 0 0 1280 720 FF000000
79 26 127 307 331 666 65 FFFFFFFF
80 27 130 0 0 1280 720
81 27 131 0 0 1280 720 FF000000
82 27 132 307 331 666 65 FFFFFFFF
83 28 135 0 0 1280 720
84 28 136 0 0 1280 720 FF000000
85 28 137 307 331 666 65 FFFFFFFF
86 29 140 0 0 1280 720
87 29 141 0 0 1280 720 FF000000
88 29 142 307 331 666 65 FFFFFFFF
89 30 145 0 0 1280 720
90 30 146 0 0 1280 720 FF000000
91 30 147 307 331 666 65 FFFFFFFF
92 31 150 0 0 1280 720
93 31 151 0 0 1280 720 FF000000
94 31 152 307 331 666 65 FFFFFFFF
95 32 155 0 0 1280 720
96 32 156 0 0 1280 720 FF000000
97 32 157 307 331 666 65 FFFFFFFF
98 33 160 0 0 1280 720
99 33 161 0 0 1280 720 FF000000
100 33 162 307 331 666 65 FFFFFFFF
101 34 165 0 0 1280 720
102 34 166 0 0 1280 720 FF000000
103 34 167 307 331 666 65 FFFFFFFF
104 35 170 0 0 1280 720
105 35 171 0 0 1280 720 FF000000
106 35 172 307 331 666 65 FFFFFFFF
107 36 175 0 0 1280 720
108 36 176 0 0 1280 720 FF000000
109 36 177 307 331 666 65 FFFFFFFF
110 37 180 0 0 1280 720
111 37 181 0 0 1280 720 FF000000
112 37 182 307 331 666 65 FFFFFFFF
113 38 185 0 0 1280 720
114 38 186 0 0 1280 720 FF000000
115 38 187 307 331 666 65 FFFFFFFF
116 39 190 0 0 1280 720
117 39 191 0 0 1280 720 FF000000
118 39 192 307 331 666 65 FFFFFFFF
119 40 195 0 0 1280 720
120 40 196 0 0 1280 720 FF000000
121 40 197 307 331 666 65 FFFFFFFF
122 41 200 0 0 1280 720
123 41 201 0 0 1280 720 FF000000
124 41 202 307 331 666 65 FFFFFFFF
125 42 205 0 0 1280 720
126 42 206 0 0 1280 720 FF000000
127 42 207 307 331 666 65 FFFFFFFF
128 43 210 0 0 1280 720
129 43 211 0 0 1280 720 FF000000
130 43 212 307 331 666 65 FFFFFFFF
131 44 215 0 0 1280 720
132 44 216 0 0 1280 720 FF000000
133 44 217 307 331 666 65 FFFFFFFF
134 45 220 0 0 1280 720
135 45 221 0 0 1280 720 FF000000
136 45 222 307 331 666 65 FFFFFFFF
137 47 225 0 0 1280 720
138 47 226 0 0 1280 720 FF000000
139 47 227 307 331 666 65 FFFFFFFF
140 48 230 0 0 1280 720
141 48 231 0 0 1280 720 FF000000
142 48 232 307 331 666 65 FFFFFFFF
143 49 235 0 0 1280 720
144 49 236 0 0 1280 720 FF000000
145 49 237 307 331 666 65 FFFFFFFF
146 50 240 0 0 1280 720
147 50 241 0 0 1280 720 FF000000
148 50 242 307 331 666 65 FFFFFFFF
149 51 245 0 0 1280 720
150 51 246 0 0 1280 720 FF000000
151 51 247 307 331 666 65 FFFFFFFF
152 52 250 0 0 1280 720
153 52 251 0 0 1280 720 FF000000
154 52 252 307 331 666 65 FFFFFFFF
155 53 255 0 0 1280 720
156 53 256 0 0 1280 720 FF000000
157 53 257 307 331 666 65 FFFFFFFF
158 54 260 0 0 1280 720
159 54 261 0 0 1280 720 FF000000
160 54 262 307 331 666 65 FFFFFFFF
161 55 265 0 0 1280 720
162 55 266 0 0 1280 720 FF000000
163 55 267 307 331 666 65 FFFFFFFF
164 56 270 0 0 1280 720
165 56 271 0 0 1280 720 FF000000
166 56 272 307 331 666 65 FFFFFFFF
167 57 275 0 0 1280 720
168 57 276 0 0 1280 720 FF000000
169 57 277 307 331 666 65 FFFFFFFF
170 58 280 0 0 1280 720
171 58 281 0 0 1280 720 FF000000
172 58 282 307 331 666 65 FFFFFFFF
173 59 285 0 0 1280 720
174 59 286 0 0 1280 720 FF000000
175 59 287 307 331 666 65 FFFFFFFF
176 60 290 0 0 1280 720
177 60 291 0 0 1280 720 FF000000
178 60 292 307 331 666 65 FFFFFFFF
179 61 295 0 0 1280 720
180 61 296 0 0 1280 720 FF000000
181 61 297 307 331 666 65 FFFFFFFF
182 62 300 0 0 1280 720
183 62 301 0 0 1280 720 FF000000
184 62 302 307 331 666 65 FFFFFFFF
185 63 305 0 0 1280 720
186 63 306 0 0 1280 720 FF000000
187 63 307 307 331 666 65 FFFFFFFF
188 64 310 0 0 1280 720
189 64 311 0 0 1280 720 FF000000
190 64 312 307 331 666 65 FFFFFFFF
191 65 315 0 0 1280 720
192 65 316 0 0 1280 720 FF000000
193 65 317 307 331 666 65 FFFFFFFF
194 66 320 0 0 1280 720
195 66 321 0 0 1280 720 FF000000
196 66 322 307 331 666 65 FFFFFFFF
197 67 325 0 0 1280 720
198 67 326 0 0 1280 720 FF000000
199 67 327 307 331 666 65 FFFFFFFF
200 68 330 0 0 1280 720
201 68 331 0 0 1280 720 FF000000
202 68 332 307 331 666 65 FFFFFFFF
203 69 335 0 0 1280 720
204 69 336 0 0 1280 720 FF000000
205 69 337 307 331 666 65 FFFFFFFF
206 70 340 0 0 1280 720
207 70 341 0 0 1280 720 FF000000
208 70 342 307 331 666 65 FFFFFFFF
209 71 345 0 0 1280 720
210 71 346 0 0 1280 720 FF000000
211 71 347 307 331 666 65 FFFFFFFF
212 72 350 0 0 1280 720
213 72 351 0 0 1280 720 FF000000
214 72 352 307 331 666 65 FFFFFFFF
215 73 355 0 0 1280 720
216 73 356 0 0 1280 720 FF000000
217 73 357 307 331 666 65 FFFFFFFF
218 74 360 0 0 1280 720
219 74 361 0 0 1280 720 FF000000
220 74 362 307 331 666 65 FFFFFFFF
221 75 365 0 0 1280 720
222 75 366 0 0 1280 720 FF000000
223 75 367 307 331 666 65 FFFFFFFF
224 76 370 0 0 1280 720
225 76 371 0 0 1280 720 FF000000
226 76 372 307 331 666 65 FFFFFFFF
227 77 375 0 0 1280 720
228 77 376 0 0 1280 720 FF000000
229 77 377 307 331 666 65 FFFFFFFF
230 78 380 0 0 1280 720
231 78 381 0 0 1280 720 FF000000
232 78 382 307 331 666 65 FEFFFFFF
233 79 385 0 0 1280 720
234 79 386 0 0 1280 720 FF000000
235 79 387 307 331 666 65 F9FFFFFF
236 80 390 0 0 1280 720
237 80 391 0 0 1280 720 FF000000
238 80 392 307 331 666 65 E7FFFFFF
239 81 395 0 0 1280 720
240 81 396 0 0 1280 720 FF000000
241 81 397 307 331 666 65 D7FFFFFF
242 82 400 0 0 1280 720
243 82 401 0 0 1280 720 FF000000
244 82 402 307 331 666 65 B5FFFFFF
245 83 405 0 0 1280 720
246 83 406 0 0 1280 720 FF000000
247 83 407 307 331 666 65 A5FFFFFF
248 84 410 0 0 1280 720
249 84 411 0 0 1280 720 FF000000
250 84 412 307 331 666 65 83FFFFFF
251 85 415 0 0 1280 720
252 85 416 0 0 1280 720 FF000000
253 85 417 307 331 666 65 73FFFFFF
254 86 420 0 0 1280 720
255 86 421 0 0 1280 720 FF000000
256 86 422 307 331 666 65 51FFFFFF
257 87 425 0 0 1280 720
258 87 426 0 0 1280 720 FF000000
259 87 427 307 331 666 65 41FFFFFF
260 88 430 0 0 1280 720
261 88 431 0 0 1280 720 FF000000
262 88 432 307 331 666 65 1FFFFFFF
263 89 435 0 0 1280 720
264 89 436 0 0 1280 720 FF000000
265 89 437 307 331 666 65 0FFFFFFF
266 90 440 0 0 1280 720
267 90 441 0 0 1280 720 FF000000
268 90 442 307 331 666 65 07FFFFFF
269 91 445 0 0 1280 720
270 91 446 0 0 1280 720 FF000000
271 92 449 0 0 1280 720
272 92 450 0 0 1280 720 FF000000
273 93 453 0 0 1280 720
274 93 454 0 0 1280 720 FF000000
275 94 457 0 0 1280 720
276 94 458 0 0 1280 720 FF000000
277 94 459 378 155 525 90 22FFFFFF
278 94 459 512 306 262 108 22FFFFFF
279 95 462 0 0 1280 720
280 95 463 0 0 1280 720 FF000000
281 95 464 378 155 525 90 44FFFFFF
282 95 464 512 306 262 108 44FFFFFF
283 96 467 0 0 1280 720
284 96 468 0 0 1280 720 FF000000
285 96 469 378 155 525 90 66FFFFFF
286 96 469 512 306 262 108 66FFFFFF
287 97 472 0 0 1280 720
288 97 473 0 0 1280 720 FF000000
289 97 474 378 155 525 90 88FFFFFF
290 97 474 512 306 262 108 88FFFFFF
291 98 477 0 0 1280 720
292 98 478 0 0 1280 720 FF000000
293 98 479 378 155 525 90 AAFFFFFF
294 98 479 512 306 262 108 AAFFFFFF
295 99 482 0 0 1280 720
296 99 483 0 0 1280 720 FF000000
297 99 484 378 155 525 90 CCFFFFFF
298 99 484 512 306 262 108 CCFFFFFF
299 100 487 0 0 1280 720
300 100 488 0 0 1280 720 FF000000
301 100 489 378 155 525 90 EEFFFFFF
302 100 489 512 306 262 108 EEFFFFFF
303 101 492 0 0 1280 720
304 101 493 0 0 1280 720 FF000000
305 101 494 378 155 525 90 FFFFFFFF
306 101 494 512 306 262 108 FFFFFFFF
307 102 497 0 0 1280 720
308 102 498 0 0 1280 720 FF000000
309 102 499 378 155 525 90 FFFFFFFF
310 102 499 512 306 262 108 FFFFFFFF
311 103 502 0 0 1280 720
312 103 503 0 0 1280 720 FF000000
313 103 504 378 155 525 90 FFFFFFFF
314 103 504 512 306 262 108 FFFFFFFF
315 104 507 0 0 1280 720
316 104 508 0 0 1280 720 FF000000
317 104 509 378 155 525 90 FFFFFFFF
318 104 509 512 306 262 108 FFFFFFFF
319 105 512 0 0 1280 720
320 105 513 0 0 1280 720 FF000000
321 105 514 378 155 525 90 FFFFFFFF
322 105 514 512 306 262 108 FFFFFFFF
323 106 517 0 0 1280 720
324 106 518 0 0 1280 720 FF000000
325 106 519 378 155 525 90 FFFFFFFF
326 106 519 512 306 262 108 FFFFFFFF
327 107 522 0 0 1280 720
328 107 523 0 0 1280 720 FF000000
329 107 524 378 155 525 90 FFFFFFFF
330 107 524 512 306 262 108 FFFFFFFF
331 108 527 0 0 1280 720
332 108 528 0 0 1280 720 FF000000
333 108 529 378 155 525 90 FEFFFFFF
334 108 529 512 306 262 108 FEFFFFFF
335 109 532 0 0 1280 720
336 109 533 0 0 1280 720 FF000000
337 109 534 378 155 525 90 DCFFFFFF
338 109 534 512 306 262 108 DCFFFFFF
339 110 537 0 0 1280 720
340 110 538 0 0 1280 720 FF000000
341 110 539 378 155 525 90 BAFFFFFF
342 110 539 512 306 262 108 BAFFFFFF
343 111 542 0 0 1280 720
344 111 543 0 0 1280 720 FF000000
345 111 544 378 155 525 90 98FFFFFF
346 111 544 512 306 262 108 98FFFFFF
347 112 547 0 0 1280 720
348 112 548 0 0 1280 720 FF000000
349 112 549 378 155 525 90 76FFFFFF
350 112 549 512 306 262 108 76FFFFFF
351 113 552 0 0 1280 720
352 113 553 0 0 1280 720 FF000000
353 113 554 378 155 525 90 54FFFFFF
354 113 554 512 306 262 108 54FFFFFF
355 114 557 0 0 1280 720
356 114 558 0 0 1280 720 FF000000
357 114 559 378 155 525 90 43FFFFFF
358 114 559 512 306 262 108 43FFFFFF
359 115 562 0 0 1280 720
360 115 563 0 0 1280 720 FF000000
361 115 564 378 155 525 90 21FFFFFF
362 115 564 512 306 262 108 21FFFFFF
363 116 567 0 0 1280 720
364 116 568 0 0 1280 720 FF000000
365 116 569 390 162 499 72 FFFFFFFF
366 116 569 518 317 243 86 FFFFFFFF
367 117 572 0 0 1280 720
368 117 573 0 0 1280 720 FF000000
369 117 574 390 162 499 72 FFFFFFFF
370 117 574 518 317 243 86 FFFFFFFF
371 118 577 0 0 1280 720
372 118 578 0 0 1280 720 FF000000
373 118 579 390 162 499 72 FFFFFFFF
374 118 579 518 317 243 86 FFFFFFFF
375 119 582 0 0 1280 720
376 119 583 0 0 1280 720 FF000000
377 119 584 390 162 499 72 FFFFFFFF
378 119 584 518 317 243 86 FFFFFFFF
379 120 587 0 0 1280 720
380 120 588 0 0 1280 720 FF000000
381 120 589 390 162 499 72 FFFFFFFF
382 120 589 518 317 243 86 FFFFFFFF
383 121 592 0 0 1280 720
384 121 593 0 0 1280 720 FF000000
385 121 594 390 162 499 72 FFFFFFFF
386 121 594 518 317 243 86 FFFFFFFF
387 122 597 0 0 1280 720
388 122 598 0 0 1280 720 FF000000
389 122 599 390 162 499 72 FFFFFFFF
390 122 599 518 317 243 86 FFFFFFFF
391 123 602 0 0 1280 720
392 123 603 0 0 1280 720 FF000000
393 123 604 390 162 499 72 FFFFFFFF
394 123 604 518 317 243 86 FFFFFFFF
395 124 607 0 0 1280 720
396 124 608 0 0 1280 720 FF000000
397 124 609 390 162 499 72 FFFFFFFF
398 124 609 518 317 243 86 FFFFFFFF
399 125 612 0 0 1280 720
400 125 613 0 0 1280 720 FF000000
401 125 614 390 162 499 72 FFFFFFFF
402 125 614 518 317 243 86 FFFFFFFF
403 126 617 0 0 1280 720
404 126 618 0 0 1280 720 FF000000
405 126 619 390 162 499 72 FFFFFFFF
406 126 619 518 317 243 86 FFFFFFFF
407 127 622 0 0 1280 720
408 127 623 0 0 1280 720 FF000000
409 127 624 390 162 499 72 FFFFFFFF
410 127 624 518 317 243 86 FFFFFFFF
411 128 627 0 0 1280 720
412 128 628 0 0 1280 720 FF000000
413 128 629 390 162 499 72 FFFFFFFF
414 128 629 518 317 243 86 FFFFFFFF
415 129 632 0 0 1280 720
416 129 633 0 0 1280 720 FF000000
417 129 634 390 162 499 72 FFFFFFFF
418 129 634 518 317 243 86 FFFFFFFF
419 130 637 0 0 1280 720
420 130 638 0 0 1280 720 FF000000
421 130 639 390 162 499 72 FFFFFFFF
422 130 639 518 317 243 86 FFFFFFFF
423 131 642 0 0 1280 720
424 131 643 0 0 1280 720 FF000000
425 131 644 390 162 499 72 FFFFFFFF
426 131 644 518 317 243 86 FFFFFFFF
427 132 647 0 0 1280 720
428 132 648 0 0 1280 720 FF000000
429 132 649 390 162 499 72 FFFFFFFF
430 132 649 518 317 243 86 FFFFFFFF
431 133 652 0 0 1280 720
432 133 653 0 0 1280 720 FF000000
433 133 654 390 162 499 72 FFFFFFFF
434 133 654 518 317 243 86 FFFFFFFF
435 134 657 0 0 1280 720
436 134 658 0 0 1280 720 FF000000
437 134 659 390 162 499 72 FFFFFFFF
438 134 659 518 317 243 86 FFFFFFFF
439 135 662 0 0 1280 720
440 135 663 0 0 1280 720 FF000000
441 135 664 390 162 499 72 FFFFFFFF
442 135 664 518 317 243 86 FFFFFFFF
443 136 667 0 0 1280 720
444 136 668 0 0 1280 720 FF000000
445 136 669 390 162 499 72 FFFFFFFF
446 136 669 518 317 243 86 FFFFFFFF
447 137 672 0 0 1280 720
448 137 673 0 0 1280 720 FF000000
449 137 674 390 162 499 72 FFFFFFFF
450 137 674 518 317 243 86 FFFFFFFF
451 138 677 0 0 1280 720
452 138 678 0 0 1280 720 FF000000
453 138 679 390 162 499 72 FFFFFFFF
454 138 679 518 317 243 86 FFFFFFFF
455 140 682 0 0 1280 720
456 140 683 0 0 1280 720 FF000000
457 140 684 390 162 499 72 FFFFFFFF
458 140 684 518 317 243 86 FFFFFFFF
459 141 687 0 0 1280 720
460 141 688 0 0 1280 720 FF000000
461 141 689 390 162 499 72 FFFFFFFF
462 141 689 518 317 243 86 FFFFFFFF
463 142 692 0 0 1280 720
464 142 693 0 0 1280 720 FF000000
465 142 694 390 162 499 72 FFFFFFFF
466 142 694 518 317 243 86 FFFFFFFF
467 143 697 0 0 1280 720
468 143 698 0 0 1280 720 FF000000
469 143 699 390 162 499 72 FFFFFFFF
470 143 699 518 317 243 86 FFFFFFFF
471 144 702 0 0 1280 720
472 144 703 0 0 1280 720 FF000000
473 144 704 390 162 499 72 FFFFFFFF
474 144 704 518 317 243 86 FFFFFFFF
475 145 707 0 0 1280 720
476 145 708 0 0 1280 720 FF000000
477 145 709 390 162 499 72 FFFFFFFF
478 145 709 518 317 243 86 FFFFFFFF
479 146 712 0 0 1280 720
480 146 713 0 0 1280 720 FF000000
481 146 714 390 162 499 72 FFFFFFFF
482 146 714 518 317 243 86 FFFFFFFF
483 147 717 0 0 1280 720
484 147 718 0 0 1280 720 FF000000
485 147 719 390 162 499 72 FFFFFFFF
486 147 719 518 317 243 86 FFFFFFFF
487 148 722 0 0 1280 720
488 148 723 0 0 1280 720 FF000000
489 148 724 390 162 499 72 FFFFFFFF
490 148 724 518 317 243 86 FFFFFFFF
491 149 727 0 0 1280 720
492 149 728 0 0 1280 720 FF000000
493 149 729 390 162 499 72 FFFFFFFF
494 149 729 518 317 243 86 FFFFFFFF
495 150 732 0 0 1280 720
496 150 733 0 0 1280 720 FF000000
497 150 734 390 162 499 72 FFFFFFFF
498 150 734 518 317 243 86 FFFFFFFF
499 151 737 0 0 1280 720
500 151 738 0 0 1280 720 FF000000
501 151 739 390 162 499 72 FFFFFFFF
502 151 739 518 317 243 86 FFFFFFFF
503 152 742 0 0 1280 720
504 152 743 0 0 1280 720 FF000000
505 152 744 390 162 499 72 FFFFFFFF
506 152 744 518 317 243 86 FFFFFFFF
507 153 747 0 0 1280 720
508 153 748 0 0 1280 720 FF000000
509 153 749 390 162 499 72 FFFFFFFF
510 153 749 518 317 243 86 FFFFFFFF
511 154 752 0 0 1280 720
512 154 753 0 0 1280 720 FF000000
513 154 754 390 162 499 72 FFFFFFFF
514 154 754 518 317 243 86 FFFFFFFF
515 155 757 0 0 1280 720
516 155 758 0 0 1280 720 FF000000
517 155 759 390 162 499 72 FFFFFFFF
518 155 759 518 317 243 86 FFFFFFFF
519 156 762 0 0 1280 720
520 156 763 0 0 1280 720 FF000000
521 156 764 390 162 499 72 FFFFFFFF
522 156 764 518 317 243 86 FFFFFFFF
523 157 767 0 0 1280 720
524 157 768 0 0 1280 720 FF000000
525 157 769 390 162 499 72 FFFFFFFF
526 157 769 518 317 243 86 FFFFFFFF
527 158 772 0 0 1280 720
528 158 773 0 0 1280 720 FF000000
529 158 774 390 162 499 72 FFFFFFFF
530 158 774 518 317 243 86 FFFFFFFF
531 159 777 0 0 1280 720
532 159 778 0 0 1280 720 FF000000
533 159 779 390 162 499 72 FFFFFFFF
534 159 779 518 317 243 86 FFFFFFFF
535 160 782 0 0 1280 720
536 160 783 0 0 1280 720 FF000000
537 160 784 390 162 499 72 FFFFFFFF
538 160 784 518 317 243 86 FFFFFFFF
539 161 787 0 0 1280 720
540 161 788 0 0 1280 720 FF000000
541 161 789 390 162 499 72 FFFFFFFF
542 161 789 518 317 243 86 FFFFFFFF
543 162 792 0 0 1280 720
544 162 793 0 0 1280 720 FF000000
545 162 794 390 162 499 72 FFFFFFFF
546 162 794 518 317 243 86 FFFFFFFF
547 163 797 0 0 1280 720
548 163 798 0 0 1280 720 FF000000
549 163 799 390 162 499 72 FFFFFFFF
550 163 799 518 317 243 86 FFFFFFFF
551 164 802 0 0 1280 720
552 164 803 0 0 1280 720 FF000000
553 164 804 390 162 499 72 FFFFFFFF
554 164 804 518 317 243 86 FFFFFFFF
555 165 807 0 0 1280 720
556 165 808 0 0 1280 720 FF000000
557 165 809 390 162 499 72 FFFFFFFF
558 165 809 518 317 243 86 FFFFFFFF
559 166 812 0 0 1280 720
560 166 813 0 0 1280 720 FF000000
561 166 814 390 162 499 72 FFFFFFFF
562 166 814 518 317 243 86 FFFFFFFF
563 167 817 0 0 1280 720
564 167 818 0 0 1280 720 FF000000
565 167 819 390 162 499 72 FFFFFFFF
566 167 819 518 317 243 86 FFFFFFFF
567 168 822 0 0 1280 720
568 168 823 0 0 1280 720 FF000000
569 168 824 390 162 499 72 FFFFFFFF
570 168 824 518 317 243 86 FFFFFFFF
571 169 827 0 0 1280 720
572 169 828 0 0 1280 720 FF000000
573 169 829 390 162 499 72 FFFFFFFF
574 169 829 518 317 243 86 FFFFFFFF
575 170 832 0 0 1280 720
576 170 833 0 0 1280 720 FF000000
577 170 834 390 162 499 72 FFFFFFFF
578 170 834 518 317 243 86 FFFFFFFF
579 171 837 0 0 1280 720
580 171 838 0 0 1280 720 FF000000
581 171 839 390 162 499 72 FFFFFFFF
582 171 839 518 317 243 86 FFFFFFFF
583 172 842 0 0 1280 720
584 172 843 0 0 1280 720 FF000000
585 172 844 390 162 499 72 FFFFFFFF
586 172 844 518 317 243 86 FFFFFFFF
587 173 847 0 0 1280 720
588 173 848 0 0 1280 720 FF000000
589 173 849 390 162 499 72 FFFFFFFF
590 173 849 518 317 243 86 FFFFFFFF
591 174 852 0 0 1280 720
592 174 853 0 0 1280 720 FF000000
593 174 854 390 162 499 72 FFFFFFFF
594 174 854 518 317 243 86 FFFFFFFF
595 175 857 0 0 1280 720
596 175 858 0 0 1280 720 FF000000
597 175 859 390 162 499 72 FFFFFFFF
598 175 859 518 317 243 86 FFFFFFFF
599 176 862 0 0 1280 720
600 176 863 0 0 1280 720 FF000000
601 176 864 390 162 499 72 FFFFFFFF
602 176 864 518 317 243 86 FFFFFFFF
603 177 867 0 0 1280 720
604 177 868 0 0 1280 720 FF000000
605 177 869 390 162 499 72 FFFFFFFF
606 177 869 518 317 243 86 FFFFFFFF
607 178 872 0 0 1280 720
608 178 873 0 0 1280 720 FF000000
609 178 874 390 162 499 72 FFFFFFFF
610 178 874 518 317 243 86 FFFFFFFF
611 179 877 0 0 1280 720
612 179 878 0 0 1280 720 FF000000
613 179 879 390 162 499 72 FFFFFFFF
614 179 879 518 317 243 86 FFFFFFFF
615 180 882 0 0 1280 720
616 180 883 0 0 1280 720 FF000000
617 180 884 390 162 499 72 FFFFFFFF
618 180 884 518 317 243 86 FFFFFFFF
619 181 887 0 0 1280 720
620 181 888 0 0 1280 720 FF000000
621 181 889 390 162 499 72 FFFFFFFF
622 181 889 518 317 243 86 FFFFFFFF
623 182 892 0 0 1280 720
624 182 893 0 0 1280 720 FF000000
625 182 894 390 162 499 72 FFFFFFFF
626 182 894 518 317 243 86 FFFFFFFF
627 183 897 0 0 1280 720
628 183 898 0 0 1280 720 FF000000
629 183 899 390 162 499 72 FFFFFFFF
630 183 899 518 317 243 86 FFFFFFFF
631 184 902 0 0 1280 720
632 184 903 0 0 1280 720 FF000000
633 184 904 390 162 499 72 FFFFFFFF
634 184 904 518 317 243 86 FFFFFFFF
635 185 907 0 0 1280 720
636 185 908 0 0 1280 720 FF000000
637 185 909 390 162 499 72 FFFFFFFF
638 185 909 518 317 243 86 FFFFFFFF
639 186 912 0 0 1280 720
640 186 913 0 0 1280 720 FF000000
641 186 914 390 162 499 72 FFFFFFFF
642 186 914 518 317 243 86 FFFFFFFF
643 187 917 0 0 1280 720
644 187 918 0 0 1280 720 FF000000
645 187 919 390 162 499 72 FFFFFFFF
646 187 919 518 317 243 86 FFFFFFFF
647 188 922 0 0 1280 720
648 188 923 0 0 1280 720 FF000000
649 188 924 390 162 499 72 FFFFFFFF
650 188 924 518 317 243 86 FFFFFFFF
651 189 927 0 0 1280 720
652 189 928 0 0 1280 720 FF000000
653 189 929 390 162 499 72 FFFFFFFF
654 189 929 518 317 243 86 FFFFFFFF
655 190 932 0 0 1280 720
656 190 933 0 0 1280 720 FF000000
657 190 934 390 162 499 72 FFFFFFFF
658 190 934 518 317 243 86 FFFFFFFF
659 191 937 0 0 1280 720
660 191 938 0 0 1280 720 FF000000
661 191 939 390 162 499 72 FFFFFFFF
662 191 939 518 317 243 86 FFFFFFFF
663 192 942 0 0 1280 720
664 192 943 0 0 1280 720 FF000000
665 192 944 390 162 499 72 FFFFFFFF
666 192 944 518 317 243 86 FFFFFFFF
667 193 947 0 0 1280 720
668 193 948 0 0 1280 720 FF000000
669 193 949 390 162 499 72 FFFFFFFF
670 193 949 518 317 243 86 FFFFFFFF
671 194 952 0 0 1280 720
672 194 953 0 0 1280 720 FF000000
673 194 954 390 162 499 72 FFFFFFFF
674 194 954 518 317 243 86 FFFFFFFF
675 195 957 0 0 1280 720
676 195 958 0 0 1280 720 FF000000
677 195 959 390 162 499 72 FFFFFFFF
678 195 959 518 317 243 86 FFFFFFFF
679 196 962 0 0 1280 720
680 196 963 0 0 1280 720 FF000000
681 196 964 390 162 499 72 FFFFFFFF
682 196 964 518 317 243 86 FFFFFFFF
683 197 967 0 0 1280 720
684 197 968 0 0 1280 720 FF000000
685 197 969 390 162 499 72 FFFFFFFF
686 197 969 518 317 243 86 FFFFFFFF
687 198 972 0 0 1280 720
688 198 973 0 0 1280 720 FF000000
689 198 974 390 162 499 72 FFFFFFFF
690 198 974 518 317 243 86 FFFFFFFF
691 199 977 0 0 1280 720
692 199 978 0 0 1280 720 FF000000
693 199 979 390 162 499 72 FEFFFFFF
694 199 979 518 317 243 86 FEFFFFFF
695 200 982 0 0 1280 720
696 200 983 0 0 1280 720 FF000000
697 200 984 390 162 499 72 F3FFFFFF
698 200 984 518 317 243 86 F3FFFFFF
699 201 987 0 0 1280 720
700 201 988 0 0 1280 720 FF000000
701 201 989 390 162 499 72 EDFFFFFF
702 201 989 518 317 243 86 EDFFFFFF
703 202 992 0 0 1280 720
704 202 993 0 0 1280 720 FF000000
705 202 994 390 162 499 72 D7FFFFFF
706 202 994 518 317 243 86 D7FFFFFF
707 203 997 0 0 1280 720
708 203 998 0 0 1280 720 FF000000
709 203 999 390 162 499 72 B5FFFFFF
710 203 999 518 317 243 86 B5FFFFFF
711 204 1002 0 0 1280 720
712 204 1003 0 0 1280 720 FF000000
713 204 1004 390 162 499 72 94FFFFFF
714 204 1004 518 317 243 86 94FFFFFF
715 205 1007 0 0 1280 720
716 205 1008 0 0 1280 720 FF000000
717 205 1009 390 162 499 72 83FFFFFF
718 205 1009 518 317 243 86 83FFFFFF
719 206 1012 0 0 1280 720
720 206 1013 0 0 1280 720 FF000000
721 206 1014 390 162 499 72 62FFFFFF
722 206 1014 518 317 243 86 62FFFFFF
723 207 1017 0 0 1280 720
724 207 1018 0 0 1280 720 FF000000
725 207 1019 390 162 499 72 41FFFFFF
726 207 1019 518 317 243 86 41FFFFFF
727 208 1022 0 0 1280 720
728 208 1023 0 0 1280 720 FF000000
729 208 1024 390 162 499 72 30FFFFFF
730 208 1024 518 317 243 86 30FFFFFF
731 209 1027 0 0 1280 720
732 209 1028 0 0 1280 720 FF000000
733 209 1029 390 162 499 72 17FFFFFF
734 209 1029 518 317 243 86 17FFFFFF
735 210 1032 0 0 1280 720
736 210 1033 0 0 1280 720 FF000000
737 210 1034 390 162 499 72 0FFFFFFF
738 210 1034 518 317 243 86 0FFFFFFF
739 211 1037 0 0 1280 720
740 211 1038 0 0 1280 720 FF000000
741 211 1039 390 162 499 72 07FFFFFF
742 211 1039 518 317 243 86 07FFFFFF
743 212 1042 0 0 1280 720
744 212 1043 0 0 1280 720 FF000000
745 213 1046 0 0 1280 720
746 213 1047 0 0 1280 720 FF000000
747 214 1050 0 0 1280 720
748 214 1051 0 0 1280 720 FF000000
749 215 1054 0 0 1280 720
750 216 1057 0 0 1280 720
751 217 1060 0 0 1280 720
752 218 1063 0 0 1280 720
753 219 1066 0 0 1280 720
754 220 1069 0 0 1280 720
755 221 1072 0 0 1280 720
756 221 1073 0 0 1280 720
757 223 1076 0 0 1280 720
758 223 1077 0 0 1280 720
759 224 1080 0 0 1280 720
760 224 1081 0 0 1280 720
761 226 1084 0 0 1280 720
762 226 1085 0 0 1280 720
763 227 1088 0 0 1280 720
764 227 1089 0 0 1280 720
765 228 1092 0 0 1280 720
766 228 1093 0 0 1280 720
767 229 1096 0 0 1280 720
768 229 1097 0 0 1280 720
769 230 1100 0 0 1280 720
770 230 1101 0 0 1280 720
771 231 1104 0 0 1280 720
772 231 1105 0 0 1280 720
773 232 1108 0 0 1280 720
774 232 1109 0 0 1280 720
775 233 1112 0 0 1280 720
776 233 1113 0 0 1280 720
777 234 1116 0 0 1280 720
778 234 1117 0 0 1280 720
779 236 1120 0 0 1280 720
780 236 1121 0 0 1280 720
781 237 1124 0 0 1280 720
782 237 1125 0 0 1280 720
783 238 1128 0 0 1280 720
784 238 1129 0 0 1280 720
785 239 1132 0 0 1280 720
786 239 1133 0 0 1280 720
787 240 1136 0 0 1280 720
788 240 1137 0 0 1280 720
789 241 1140 0 0 1280 720
790 241 1141 0 0 1280 720

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,909 @@
# Every call site of ISL built-ins 26, 28, 29 and 101 across all 28 stage scripts.
#
# Regenerate with:
# python3 tools/re-capture/isl_builtin_sites.py 26,28,29,101 \
# --ssb <dir with StageNN.ssb> --craft <unitgroup.py --all output>
#
# Columns: stage offset builtin slot8 slot4-name slot4-raw symtype before after
# slot8 is the double operand; the handler multiplies it by 0.01 before sending it.
#
Stage01 0x0009C8 29 0 TCN001 0x1B type2 [35,118,12] [12,29,15]
Stage01 0x000A44 29 0 TCN002 0x1E type2 [12,29,12] [15,12,30]
Stage01 0x001A5C 29 100 TCN001 0x1B type2 [127,123,8] [35,11,58]
Stage01 0x0028A8 101 - ? None typeNone [11,95,116] [100,124,93]
Stage01 0x002CF8 101 - ? None typeNone [20,20,116] [100,124,93]
Stage01 0x003210 101 - ? None typeNone [11,9,116] [100,124,93]
Stage01 0x007370 28 10 TCN002 0x1E type2 [1,1,1] [29,30,64]
Stage01 0x0073B0 29 0 TCN002 0x1E type2 [1,1,28] [30,64,64]
Stage01 0x00EDAC 29 0 TCN001 0x1B type2 [35,118,12] [12,15,29]
Stage01 0x00EE80 29 0 TCN002 0x1E type2 [29,12,15] [12,15,29]
Stage01 0x00EF54 29 0 TCN003 0x20 type2 [29,12,15] [12,15,30]
Stage01 0x00F094 28 100 ADT201 0x3 type2 [12,15,30] [12,15,30]
Stage01 0x00F1D4 28 100 ADT202 0x6 type2 [12,15,30] [12,3,15]
Stage01 0x00F348 28 100 ADS221 0x7 type2 [12,3,15] [12,3,15]
Stage01 0x00F4BC 28 200 ADS222 0xB type2 [12,3,15] [12,15,28]
Stage01 0x00F590 28 100 ADS223 0xE type2 [28,12,15] [12,15,12]
Stage01 0x0100B0 29 100 TCN001 0x1B type2 [127,123,8] [35,103,1]
Stage01 0x010398 28 10 TCN002 0x1E type2 [1,1,1] [29,30,11]
Stage01 0x0103D8 29 0 TCN002 0x1E type2 [1,1,28] [30,11,58]
Stage01 0x01060C 28 100 ADT203 0xA type2 [15,30,47] [103,69,63]
Stage01 0x0108B4 28 100 ADT204 0xD type2 [58,15,30] [47,1,1]
Stage01 0x010B40 28 100 ADS224 0x11 type2 [58,3,15] [1,11,58]
Stage01 0x010CE8 28 100 ADS225 0x13 type2 [58,3,15] [1,11,58]
Stage01 0x010DF0 28 100 ADT205 0x10 type2 [11,58,15] [30,47,20]
Stage01 0x01115C 28 200 ADS227 0x17 type2 [57,3,15] [58,15,28]
Stage01 0x011230 28 100 ADS226 0x15 type2 [28,58,15] [48,92,1]
Stage01 0x0115CC 28 100 ADS228 0x19 type2 [58,3,15] [1,11,58]
Stage01 0x011774 28 100 ADS229 0x1A type2 [58,3,15] [1,11,95]
Stage01 0x01184C 101 - ? None typeNone [11,95,116] [100,124,93]
Stage01 0x011D58 101 - ? None typeNone [20,20,116] [100,124,93]
Stage01 0x0127AC 101 - ? None typeNone [11,9,116] [100,124,93]
Stage02 0x000E6C 29 0 TCN001 0x4B type2 [35,118,12] [12,15,12]
Stage02 0x002D14 26 0 TCN131 0x57 type2 [14,77,11] [11,116,127]
Stage02 0x002E0C 29 100 TCN001 0x4B type2 [127,123,8] [35,103,30]
Stage02 0x003220 29 0 TCN002 0x4E type2 [1,1,1] [11,58,15]
Stage02 0x004DA8 101 - ? None typeNone [11,95,116] [100,124,93]
Stage02 0x005274 101 - ? None typeNone [11,20,116] [100,124,93]
Stage02 0x0059BC 101 - ? None typeNone [20,20,116] [100,124,93]
Stage02 0x0060A8 101 - ? None typeNone [11,9,116] [100,124,93]
Stage02 0x01586C 29 0 TCN001 0x4B type2 [35,118,12] [12,15,12]
Stage02 0x018544 29 100 TCN001 0x4B type2 [123,123,8] [35,103,30]
Stage02 0x0186DC 29 0 TCN002 0x4E type2 [1,1,1] [11,58,3]
Stage02 0x019204 101 - ? None typeNone [11,95,116] [100,124,93]
Stage02 0x0196D0 101 - ? None typeNone [11,20,116] [100,124,93]
Stage02 0x01A21C 101 - ? None typeNone [20,20,116] [100,124,93]
Stage02 0x01E114 26 0 ADN207 0x2A type2 [79,7,70] [26,11,47]
Stage02 0x01E154 26 0 TCN208 0x68 type2 [7,70,26] [11,47,7]
Stage02 0x01E2C4 26 0 ADN207 0x2A type2 [47,7,70] [26,11,47]
Stage02 0x01E304 26 0 TCN208 0x68 type2 [7,70,26] [11,47,20]
Stage02 0x01E58C 26 0 ADN207 0x2A type2 [80,7,70] [26,11,47]
Stage02 0x01E5CC 26 0 TCN208 0x68 type2 [7,70,26] [11,47,20]
Stage02 0x01E854 26 0 ADN207 0x2A type2 [80,7,70] [26,11,47]
Stage02 0x01E894 26 0 TCN208 0x68 type2 [7,70,26] [11,47,18]
Stage02 0x025B14 29 0 TCN001 0x4B type2 [35,118,12] [12,15,12]
Stage02 0x025FCC 26 60 TCN324 0x67 type2 [15,12,15] [12,15,26]
Stage02 0x0260A0 26 80 TCN325 0x6A type2 [26,12,15] [12,15,26]
Stage02 0x026174 26 60 TCN326 0x6D type2 [26,12,15] [12,15,12]
Stage02 0x0262DC 26 80 TCN328 0x74 type2 [15,12,15] [12,15,12]
Stage02 0x026AD8 26 80 ADN322 0x1F type2 [15,12,15] [12,15,26]
Stage02 0x026BAC 26 60 ADN323 0x25 type2 [26,12,15] [12,15,26]
Stage02 0x026C80 26 80 ADN324 0x2C type2 [26,12,15] [12,15,26]
Stage02 0x026D54 26 30 ADN325 0x32 type2 [26,12,15] [12,15,26]
Stage02 0x026E28 26 80 ADN326 0x38 type2 [26,12,15] [12,15,26]
Stage02 0x026EFC 26 30 ADN327 0x3D type2 [26,12,15] [12,15,26]
Stage02 0x026FD0 26 60 ADN328 0x42 type2 [26,12,15] [12,15,26]
Stage02 0x0270A4 26 80 ADN329 0x45 type2 [26,12,15] [12,15,12]
Stage02 0x02720C 26 80 ADN331 0x20 type2 [15,12,15] [12,15,26]
Stage02 0x0272E0 26 60 ADN332 0x26 type2 [26,12,15] [47,47,47]
Stage02 0x028B5C 29 100 TCN001 0x4B type2 [123,123,8] [35,103,30]
Stage02 0x028E14 29 0 TCN002 0x4E type2 [1,1,1] [11,26,26]
Stage02 0x028E64 26 0 ADN313 0x1E type2 [1,29,11] [26,11,57]
Stage02 0x028EA4 26 0 TCN334 0x6B type2 [29,11,26] [11,57,30]
Stage02 0x02907C 29 100 ADT301 0x31 type2 [30,15,19] [57,30,15]
Stage02 0x029244 29 100 ADT302 0x37 type2 [30,15,19] [48,47,47]
Stage02 0x029B84 29 100 ADT303 0x3C type2 [30,15,19] [1,64,70]
Stage02 0x029ED0 29 100 ADT304 0x41 type2 [30,15,19] [57,30,15]
Stage02 0x02A098 29 100 ADT305 0x44 type2 [30,15,19] [58,15,48]
Stage02 0x02AAE8 29 100 ADT306 0x47 type2 [30,15,19] [57,30,15]
Stage02 0x02ACB0 29 100 ADT307 0x48 type2 [30,15,19] [57,30,15]
Stage02 0x02AE78 29 100 ADT308 0x49 type2 [30,15,19] [58,15,48]
Stage02 0x02B530 101 - ? None typeNone [11,95,116] [100,124,93]
Stage02 0x02B9FC 101 - ? None typeNone [11,20,116] [100,124,93]
Stage02 0x02C000 101 - ? None typeNone [6,11,116] [100,124,93]
Stage02 0x02C524 101 - ? None typeNone [20,20,116] [100,124,93]
Stage02 0x02D018 101 - ? None typeNone [11,9,116] [100,124,93]
Stage02 0x02D2DC 28 150 TCN004 0x56 type2 [20,105,115] [11,26,64]
Stage02 0x02D350 26 0 ADT301 0x31 type2 [115,28,11] [64,90,11]
Stage02 0x02D4D4 26 0 ADT302 0x37 type2 [64,90,11] [64,90,11]
Stage02 0x02D658 26 0 ADT303 0x3C type2 [64,90,11] [64,90,11]
Stage02 0x02D7DC 26 0 ADT304 0x41 type2 [64,90,11] [64,90,11]
Stage02 0x02D960 26 0 ADT305 0x44 type2 [64,90,11] [64,90,11]
Stage02 0x02DAF0 26 0 ADT306 0x47 type2 [64,90,11] [64,90,11]
Stage02 0x02DC74 26 0 ADT307 0x48 type2 [64,90,11] [64,90,11]
Stage02 0x02DDF8 26 0 ADT308 0x49 type2 [64,90,11] [64,90,11]
Stage03 0x0009F0 29 0 TCN001 0x21 type2 [35,118,12] [12,15,12]
Stage03 0x002514 26 0 TCN132 0x30 type2 [14,77,11] [11,116,127]
Stage03 0x00260C 29 100 TCN001 0x21 type2 [127,123,8] [35,103,1]
Stage03 0x002810 29 0 TCN002 0x24 type2 [30,91,47] [11,58,19]
Stage03 0x0034E8 101 - ? None typeNone [11,95,116] [100,124,93]
Stage03 0x0039B4 101 - ? None typeNone [11,20,116] [100,124,93]
Stage03 0x00424C 101 - ? None typeNone [20,20,116] [100,124,93]
Stage03 0x004720 101 - ? None typeNone [11,9,116] [100,124,93]
Stage03 0x00ABB4 29 0 TCN001 0x21 type2 [35,118,12] [12,15,12]
Stage03 0x00B0CC 29 0 TCT207 0x42 type2 [15,19,30] [12,15,19]
Stage03 0x00C720 29 100 TCN001 0x21 type2 [116,123,8] [35,11,58]
Stage03 0x00DD7C 101 - ? None typeNone [11,95,116] [100,124,93]
Stage03 0x00E248 101 - ? None typeNone [11,20,116] [100,124,93]
Stage03 0x00E84C 101 - ? None typeNone [6,11,116] [100,124,93]
Stage03 0x00EFF8 101 - ? None typeNone [20,20,116] [100,124,93]
Stage03 0x00F844 29 0 TCT207 0x42 type2 [64,19,11] [5,11,20]
Stage03 0x00FA58 29 0 TCT208 0x44 type2 [19,19,11] [5,11,20]
Stage03 0x00FC6C 29 0 TCT209 0x46 type2 [19,19,11] [5,11,20]
Stage03 0x00FE34 29 100 TCT207 0x42 type2 [11,20,20] [11,20,20]
Stage03 0x00FF50 29 100 TCT207 0x42 type2 [11,20,20] [11,20,105]
Stage03 0x01584C 29 0 TCN001 0x21 type2 [35,118,12] [12,15,12]
Stage03 0x0164CC 28 500 ADN301 0x4 type2 [117,12,15] [48,48,48]
Stage03 0x016FF8 29 100 TCN001 0x21 type2 [116,123,8] [35,11,58]
Stage03 0x01713C 28 500 ADN304 0xB type2 [58,15,91] [58,15,30]
Stage03 0x01727C 28 1000 ADT302 0x19 type2 [58,15,30] [91,103,4]
Stage03 0x017C6C 28 500 ADN306 0x11 type2 [11,58,15] [91,69,47]
Stage03 0x0182F4 28 500 ADN308 0x18 type2 [11,58,15] [91,69,47]
Stage03 0x018A4C 28 2000 ADT303 0x1D type2 [15,30,91] [69,47,69]
Stage03 0x0191FC 101 - ? None typeNone [11,95,116] [100,124,93]
Stage03 0x0196C8 101 - ? None typeNone [11,20,116] [100,124,93]
Stage03 0x019CCC 101 - ? None typeNone [6,11,116] [100,124,93]
Stage03 0x019ED4 101 - ? None typeNone [6,11,116] [100,124,93]
Stage03 0x01A744 101 - ? None typeNone [11,9,116] [100,124,93]
Stage03 0x01BC10 28 2000 ADT303 0x1D type2 [15,30,91] [69,47,69]
Stage03 0x01C358 28 150 TCN309 0x41 type2 [9,115,69] [64,11,64]
Stage04 0x000A38 29 0 TCN001 0x15 type2 [35,118,12] [12,15,12]
Stage04 0x002228 29 100 TCN001 0x15 type2 [127,123,8] [35,11,69]
Stage04 0x0026D0 101 - ? None typeNone [11,95,116] [100,124,93]
Stage04 0x002BCC 101 - ? None typeNone [11,20,116] [100,124,93]
Stage04 0x003390 101 - ? None typeNone [20,20,116] [100,124,93]
Stage04 0x003834 101 - ? None typeNone [11,9,116] [100,124,93]
Stage04 0x005ED0 29 0 TCN001 0x15 type2 [35,118,12] [12,15,12]
Stage04 0x006260 28 500 ADN201 0x0 type2 [15,12,15] [12,30,15]
Stage04 0x0063A0 28 1000 ADT202 0x9 type2 [12,30,15] [12,15,12]
Stage04 0x0074CC 29 100 TCN001 0x15 type2 [127,8,123] [35,11,58]
Stage04 0x007674 28 500 ADN206 0x4 type2 [58,15,3] [92,91,64]
Stage04 0x007990 28 500 ADN207 0x6 type2 [58,3,15] [47,1,11]
Stage04 0x007B8C 28 500 ADN208 0x8 type2 [58,15,3] [92,91,64]
Stage04 0x007FAC 28 2000 ADT203 0xD type2 [30,47,30] [64,64,8]
Stage04 0x008324 28 500 ADN209 0xC type2 [58,15,3] [92,91,1]
Stage04 0x0084AC 101 - ? None typeNone [11,95,116] [100,124,93]
Stage04 0x0089A8 101 - ? None typeNone [11,20,116] [100,124,93]
Stage04 0x009030 101 - ? None typeNone [11,20,116] [100,124,93]
Stage04 0x0099DC 101 - ? None typeNone [11,9,116] [100,124,93]
Stage04 0x009D40 28 150 TCN205 0x25 type2 [9,115,69] [64,11,20]
Stage04 0x00A004 28 2000 ADT203 0xD type2 [15,30,47] [64,64,64]
Stage05 0x000898 29 0 TCN001 0x13 type2 [35,118,12] [15,12,15]
Stage05 0x00181C 29 100 TCN001 0x13 type2 [8,127,123] [35,119,11]
Stage05 0x002E6C 101 - ? None typeNone [20,9,116] [100,93,124]
Stage05 0x003BAC 101 - ? None typeNone [20,20,116] [100,93,124]
Stage05 0x003F28 28 150 TCN003 0x19 type2 [9,115,70] [70,48,11]
Stage05 0x007F00 29 0 TCN001 0x13 type2 [35,118,12] [15,12,15]
Stage05 0x0081C0 26 80 TCN004 0x1E type2 [12,15,30] [12,15,30]
Stage05 0x008300 28 150 ADT201 0x4 type2 [12,15,30] [12,19,15]
Stage05 0x0084C8 28 150 ADT202 0x6 type2 [19,15,30] [12,3,15]
Stage05 0x008FF8 29 100 TCN001 0x13 type2 [8,127,123] [35,119,11]
Stage05 0x009734 101 - ? None typeNone [20,9,116] [100,93,124]
Stage05 0x00A228 101 - ? None typeNone [20,20,116] [100,93,124]
Stage05 0x00B1A0 28 150 TCN001 0x13 type2 [64,9,115] [28,28,64]
Stage05 0x00B1E0 28 150 TCN002 0x16 type2 [9,115,28] [28,64,64]
Stage05 0x00B220 28 150 TCN003 0x19 type2 [115,28,28] [64,64,11]
Stage06 0x000924 29 0 TCN001 0x26 type2 [118,12,15] [12,15,12]
Stage06 0x001460 29 100 TCN001 0x26 type2 [8,127,123] [35,119,11]
Stage06 0x00330C 101 - ? None typeNone [11,95,116] [100,93,124]
Stage06 0x003810 101 - ? None typeNone [11,20,116] [100,93,124]
Stage06 0x003E94 101 - ? None typeNone [11,9,116] [100,93,124]
Stage06 0x0040D4 101 - ? None typeNone [6,11,116] [100,93,124]
Stage06 0x009B50 29 0 TCN001 0x26 type2 [118,12,15] [12,15,12]
Stage06 0x00BEC4 29 100 TCN001 0x26 type2 [8,127,123] [35,119,11]
Stage06 0x00C168 101 - ? None typeNone [11,95,116] [100,93,124]
Stage06 0x00C66C 101 - ? None typeNone [11,20,116] [100,93,124]
Stage06 0x00CCF0 101 - ? None typeNone [11,9,116] [100,93,124]
Stage06 0x00D274 101 - ? None typeNone [20,20,116] [100,93,124]
Stage06 0x0119AC 29 0 TCN001 0x26 type2 [118,12,15] [12,15,29]
Stage06 0x011A80 29 0 TCN002 0x29 type2 [29,12,15] [28,12,15]
Stage06 0x011AC0 28 50 TCN002 0x29 type2 [12,15,29] [12,15,12]
Stage06 0x011EFC 26 50 TCN306 0x3D type2 [12,15,3] [12,15,3]
Stage06 0x013A7C 29 100 TCN001 0x26 type2 [8,127,123] [35,119,11]
Stage06 0x013B60 101 - ? None typeNone [11,95,116] [100,93,124]
Stage06 0x01405C 101 - ? None typeNone [11,20,116] [100,93,124]
Stage06 0x014724 101 - ? None typeNone [20,20,116] [100,93,124]
Stage06 0x014F64 101 - ? None typeNone [11,9,116] [100,93,124]
Stage06 0x01845C 26 0 TCN350 0x3A type2 [64,11,64] [1,1,1]
Stage06 0x01A7B0 26 80 TCN002 0x29 type2 [69,11,9] [64,64,1]
Stage06 0x01AAA0 26 50 TCN002 0x29 type2 [69,11,9] [11,64,1]
Stage06 0x01ACEC 26 30 TCN002 0x29 type2 [69,11,9] [11,64,11]
Stage07 0x000D3C 29 0 TCN001 0x1C type2 [12,12,12] [15,15,15]
Stage07 0x0026A4 29 100 TCN001 0x1C type2 [8,127,123] [35,119,11]
Stage07 0x002788 101 - ? None typeNone [11,95,116] [100,93,124]
Stage07 0x002BD0 101 - ? None typeNone [11,20,116] [100,93,124]
Stage07 0x00324C 101 - ? None typeNone [11,20,116] [100,93,124]
Stage07 0x0034D4 101 - ? None typeNone [11,9,116] [100,93,124]
Stage07 0x003714 101 - ? None typeNone [6,11,116] [100,93,124]
Stage07 0x00AB0C 29 0 TCN001 0x1C type2 [12,12,12] [15,15,15]
Stage07 0x00CCD4 29 100 TCN001 0x1C type2 [116,8,123] [35,119,11]
Stage07 0x00CDB8 101 - ? None typeNone [11,95,116] [100,93,124]
Stage07 0x00D200 101 - ? None typeNone [11,20,116] [100,93,124]
Stage07 0x00D87C 101 - ? None typeNone [11,20,116] [100,93,124]
Stage07 0x00E4A8 101 - ? None typeNone [20,69,116] [100,93,124]
Stage07 0x00ED78 101 - ? None typeNone [11,9,116] [100,93,124]
Stage08 0x001F44 29 0 TCN001 0x1E type2 [15,12,15] [15,15,15]
Stage08 0x0021C4 28 0.1 TCN104 0x30 type2 [92,15,30] [15,28,15]
Stage08 0x00225C 28 0.1 TCN105 0x35 type2 [30,28,15] [15,28,15]
Stage08 0x0022F4 28 0.1 TCN106 0x36 type2 [15,28,15] [15,30,28]
Stage08 0x0023F8 28 0.1 TCN012 0x29 type2 [28,15,30] [15,28,15]
Stage08 0x002490 28 0.1 TCN113 0x31 type2 [30,28,15] [15,92,30]
Stage08 0x003350 26 0 ADN126 0xF type2 [4,14,77] [11,26,11]
Stage08 0x0033A0 26 0 ADN126b 0x3D type2 [77,26,11] [11,26,11]
Stage08 0x0033F0 26 0 ADN126c 0x3F type2 [11,26,11] [11,116,8]
Stage08 0x0034E8 29 100 TCN001 0x1E type2 [8,127,123] [35,119,11]
Stage08 0x0036D0 101 - ? None typeNone [20,9,116] [100,93,124]
Stage08 0x004294 101 - ? None typeNone [6,11,116] [100,93,124]
Stage08 0x006C54 28 1000 ADS151a 0x45 type2 [15,47,30] [29,58,15]
Stage08 0x006C94 29 40 ADS151a 0x45 type2 [47,30,28] [58,15,47]
Stage08 0x006E28 28 1000 ADS151b 0x4B type2 [15,47,30] [29,64,8]
Stage08 0x006E68 29 40 ADS151b 0x4B type2 [47,30,28] [64,8,1]
Stage08 0x00BD3C 28 150 TCN001 0x1E type2 [105,9,115] [28,28,64]
Stage08 0x00BD7C 28 150 TCN002 0x21 type2 [9,115,28] [28,64,11]
Stage08 0x00BDBC 28 150 TCN003 0x24 type2 [115,28,28] [64,11,20]
Stage08 0x00D918 29 0 TCN001 0x1E type2 [12,12,12] [15,15,15]
Stage08 0x00E190 29 0 ADT201 0x11 type2 [15,92,15] [117,15,92]
Stage08 0x00EE0C 29 100 TCN001 0x1E type2 [8,127,123] [35,119,11]
Stage08 0x00EFF4 101 - ? None typeNone [20,9,116] [100,93,124]
Stage08 0x00FD4C 101 - ? None typeNone [20,20,116] [100,93,124]
Stage09 0x000974 29 0 TCN001 0x28 type2 [35,118,12] [12,15,12]
Stage09 0x002958 29 100 TCN001 0x28 type2 [127,123,8] [35,11,58]
Stage09 0x003610 101 - ? None typeNone [11,95,116] [100,124,93]
Stage09 0x003A20 101 - ? None typeNone [11,20,116] [100,124,93]
Stage09 0x003C68 101 - ? None typeNone [11,20,116] [100,124,93]
Stage09 0x00426C 101 - ? None typeNone [6,11,116] [100,124,93]
Stage09 0x004A44 101 - ? None typeNone [11,9,116] [100,124,93]
Stage09 0x00851C 29 0 TCN001 0x28 type2 [35,118,12] [12,15,12]
Stage09 0x009E64 29 100 TCN001 0x28 type2 [116,123,8] [35,11,58]
Stage09 0x00A1C0 28 1000 ADS251a 0x44 type2 [12,15,30] [29,12,15]
Stage09 0x00A200 29 40 ADS251a 0x44 type2 [15,30,28] [12,15,30]
Stage09 0x00A340 28 1000 ADS251b 0x46 type2 [12,15,30] [29,47,47]
Stage09 0x00A380 29 40 ADS251b 0x46 type2 [15,30,28] [47,47,1]
Stage09 0x00A6D0 101 - ? None typeNone [11,95,116] [100,124,93]
Stage09 0x00AAE0 101 - ? None typeNone [11,20,116] [100,124,93]
Stage09 0x00ACE8 101 - ? None typeNone [6,11,116] [100,124,93]
Stage09 0x00B2D0 101 - ? None typeNone [7,9,116] [100,124,93]
Stage09 0x00C38C 28 150 TCN001 0x28 type2 [115,9,115] [28,11,64]
Stage09 0x00C3CC 28 150 TCN002 0x2B type2 [9,115,28] [11,64,64]
Stage09 0x00FDC4 29 0 TCN001 0x28 type2 [114,114,12] [12,15,12]
Stage09 0x010684 28 500 ADT301 0x14 type2 [15,12,15] [12,15,12]
Stage09 0x011790 29 100 TCN001 0x28 type2 [116,123,8] [35,11,58]
Stage09 0x011E84 101 - ? None typeNone [11,95,116] [100,124,93]
Stage09 0x012294 101 - ? None typeNone [11,20,116] [100,124,93]
Stage09 0x012B60 101 - ? None typeNone [69,69,116] [100,124,93]
Stage09 0x0131AC 101 - ? None typeNone [11,9,116] [100,124,93]
Stage09 0x017BE4 28 500 ADT302 0x18 type2 [3,15,47] [64,1,1]
Stage10 0x0006DC 29 0 TCN001 0xE type2 [118,75,12] [12,3,15]
Stage10 0x000910 28 2000 ADT001 0x2 type2 [15,30,47] [12,15,47]
Stage10 0x000A38 28 500 ADS002 0x1 type2 [12,15,47] [12,15,47]
Stage10 0x000B60 28 500 ADS003 0x5 type2 [12,15,47] [1,1,1]
Stage10 0x000F38 29 100 TCN001 0xE type2 [8,127,123] [35,119,11]
Stage10 0x00101C 101 - ? None typeNone [11,95,116] [100,93,124]
Stage10 0x0013A8 101 - ? None typeNone [11,9,116] [100,93,124]
Stage10 0x001620 101 - ? None typeNone [11,20,116] [100,93,124]
Stage10 0x002394 28 500 ADS004 0x7 type2 [64,128,47] [1,11,20]
Stage10 0x002BA0 28 500 ADS005 0x9 type2 [64,128,47] [1,11,20]
Stage10 0x0033AC 28 500 ADS006 0xA type2 [64,128,47] [1,11,20]
Stage10 0x003BB8 28 500 ADS007 0xB type2 [64,128,47] [1,11,20]
Stage10 0x0043C4 28 500 ADS008 0xC type2 [64,128,47] [1,11,20]
Stage10 0x004BD0 28 500 ADS009 0xD type2 [64,128,47] [1,11,20]
Stage10 0x0053DC 28 500 ADS010 0x0 type2 [64,128,47] [1,11,20]
Stage10 0x005BE8 28 500 ADS011 0x4 type2 [64,128,47] [1,11,20]
Stage10 0x006554 28 500 ADS013 0x8 type2 [64,128,47] [1,11,20]
Stage11 0x000744 29 0 TCN001 0x18 type2 [35,118,12] [12,15,12]
Stage11 0x001610 29 100 TCN001 0x18 type2 [8,127,123] [35,11,58]
Stage11 0x0017D8 28 1000 ADS151a 0x27 type2 [15,47,30] [29,58,15]
Stage11 0x001818 29 40 ADS151a 0x27 type2 [47,30,28] [58,15,47]
Stage11 0x0019AC 28 1000 ADS151b 0x29 type2 [15,47,30] [29,1,1]
Stage11 0x0019EC 29 40 ADS151b 0x29 type2 [47,30,28] [1,1,1]
Stage11 0x001B0C 101 - ? None typeNone [11,95,116] [100,124,93]
Stage11 0x001F54 101 - ? None typeNone [11,20,116] [100,124,93]
Stage11 0x0025D8 101 - ? None typeNone [11,9,116] [100,124,93]
Stage11 0x002818 101 - ? None typeNone [6,11,116] [100,124,93]
Stage11 0x00B070 29 0 TCN001 0x18 type2 [35,118,12] [12,15,12]
Stage11 0x00BFB8 29 100 TCN001 0x18 type2 [8,127,123] [35,1,1]
Stage11 0x00C0D8 101 - ? None typeNone [11,95,116] [100,124,93]
Stage11 0x00C520 101 - ? None typeNone [11,20,116] [100,124,93]
Stage11 0x00CBA4 101 - ? None typeNone [11,9,116] [100,124,93]
Stage11 0x00CDE4 101 - ? None typeNone [6,11,116] [100,124,93]
Stage12 0x0012D0 29 0 TCN001 0x20 type2 [12,12,12] [75,8,11]
Stage12 0x001564 29 100 TCN001 0x20 type2 [8,127,123] [35,119,1]
Stage12 0x0033CC 101 - ? None typeNone [20,9,116] [100,93,124]
Stage12 0x00403C 101 - ? None typeNone [6,11,116] [100,93,124]
Stage13 0x0012C4 29 0 TCN001 0x16 type2 [12,12,12] [15,15,15]
Stage13 0x001D74 26 50 TCN013 0x25 type2 [3,15,1] [3,15,1]
Stage13 0x0027DC 28 150 ADN004 0x8 type2 [47,92,3] [15,3,28]
Stage13 0x002914 28 150 ADN005 0xB type2 [28,15,3] [15,47,28]
Stage13 0x002A00 28 150 ADN012 0x6 type2 [28,15,47] [15,47,47]
Stage13 0x0031BC 29 100 TCN001 0x16 type2 [116,8,123] [35,119,11]
Stage13 0x003348 101 - ? None typeNone [20,20,116] [100,93,124]
Stage13 0x004EC4 101 - ? None typeNone [20,69,116] [100,93,124]
Stage13 0x0098A0 28 150 ADN002 0x3 type2 [20,9,57] [15,47,47]
Stage13 0x00A68C 28 500 ADS051a 0x34 type2 [64,11,58] [15,47,92]
Stage13 0x00A894 28 500 ADS051b 0x36 type2 [92,30,58] [15,47,92]
Stage13 0x00AA9C 28 500 ADS051c 0x37 type2 [92,30,58] [15,47,92]
Stage13 0x00BA00 28 150 ADN015 0xE type2 [64,11,57] [15,92,64]
Stage13 0x00BE08 28 150 ADN017 0x12 type2 [64,11,57] [15,47,92]
Stage14 0x001230 29 0 TCN001 0x18 type2 [12,12,12] [15,15,92]
Stage14 0x001394 26 80 TCN003 0x24 type2 [15,15,92] [25,15,30]
Stage14 0x001548 28 500 ADT102 0x3 type2 [15,30,15] [30,47,92]
Stage14 0x001714 28 500 ADT103 0x6 type2 [47,92,15] [30,47,92]
Stage14 0x0018E0 28 500 ADT104 0x8 type2 [47,92,15] [30,47,92]
Stage14 0x00242C 29 100 TCN001 0x18 type2 [8,127,123] [35,119,11]
Stage14 0x002510 101 - ? None typeNone [11,95,116] [100,93,124]
Stage14 0x002960 101 - ? None typeNone [11,9,116] [100,93,124]
Stage14 0x002D84 101 - ? None typeNone [20,20,116] [100,93,124]
Stage14 0x003784 28 500 ADT105 0xA type2 [64,58,15] [30,47,47]
Stage14 0x003E90 28 500 ADT108 0x10 type2 [64,58,15] [30,47,1]
Stage14 0x004548 28 500 ADT114 0xB type2 [64,58,15] [30,47,47]
Stage14 0x007334 29 0 TCN201 0x29 type2 [12,12,12] [15,30,47]
Stage14 0x00748C 28 500 ADT201 0x5 type2 [15,30,47] [15,15,15]
Stage14 0x007F18 29 100 TCN201 0x29 type2 [8,127,123] [35,119,11]
Stage14 0x007FFC 101 - ? None typeNone [11,95,116] [100,93,124]
Stage14 0x0083C4 101 - ? None typeNone [11,9,116] [100,93,124]
Stage14 0x008644 101 - ? None typeNone [11,20,116] [100,93,124]
Stage15 0x0014F8 29 0 TCN001 0x3A type2 [12,12,12] [15,15,15]
Stage15 0x002390 29 0 TCN008 0x59 type2 [15,47,30] [1,15,48]
Stage15 0x002C68 28 150 ADN101 0x1 type2 [15,48,92] [15,28,15]
Stage15 0x002D00 28 150 ADN102 0x2 type2 [92,28,15] [15,3,1]
Stage15 0x002FC8 28 150 ADN103 0x5 type2 [15,48,92] [15,3,15]
Stage15 0x0032C0 28 150 ADN104 0xB type2 [48,47,92] [15,3,15]
Stage15 0x003564 28 150 ADN106 0x18 type2 [15,48,92] [15,28,15]
Stage15 0x0035FC 28 150 ADN109 0x2C type2 [92,28,15] [15,15,47]
Stage15 0x003F50 29 100 TCN001 0x3A type2 [116,8,123] [35,119,11]
Stage15 0x0040DC 101 - ? None typeNone [20,20,116] [100,93,124]
Stage15 0x00563C 101 - ? None typeNone [20,69,116] [100,93,124]
Stage15 0x00DE90 29 0 TCN001 0x3A type2 [12,12,12] [15,15,15]
Stage15 0x00E970 29 0 TCN008 0x59 type2 [47,92,30] [15,48,92]
Stage15 0x00F198 28 150 ADT201 0x28 type2 [47,92,30] [15,1,30]
Stage15 0x00F2C0 28 150 ADT202 0x30 type2 [15,1,30] [15,1,28]
Stage15 0x00F37C 28 150 ADN203 0xF type2 [28,15,1] [15,28,15]
Stage15 0x00F414 28 150 ADN204 0x15 type2 [1,28,15] [15,28,15]
Stage15 0x00F4AC 28 150 ADN205 0x1C type2 [15,28,15] [15,28,15]
Stage15 0x00F544 28 150 ADN206 0x21 type2 [15,28,15] [15,28,15]
Stage15 0x00F5DC 28 150 ADN207 0x27 type2 [15,28,15] [15,28,15]
Stage15 0x00F674 28 150 ADN208 0x2F type2 [15,28,15] [15,28,15]
Stage15 0x00F70C 28 150 ADN209 0x36 type2 [15,28,15] [15,15,28]
Stage15 0x00F7FC 28 500 ADN210 0x4 type2 [28,15,15] [47,92,1]
Stage15 0x00F980 28 500 ADN211 0x9 type2 [92,1,15] [47,92,1]
Stage15 0x00FB04 28 500 ADN212 0x10 type2 [92,1,15] [47,47,92]
Stage15 0x00FCDC 28 500 ADN213 0x16 type2 [92,1,15] [47,47,92]
Stage15 0x00FEB4 28 500 ADN214 0x1D type2 [92,1,15] [47,47,92]
Stage15 0x0102DC 29 100 TCN001 0x3A type2 [8,127,123] [35,119,11]
Stage15 0x0104C4 101 - ? None typeNone [20,9,116] [100,93,124]
Stage15 0x0110CC 101 - ? None typeNone [20,20,116] [100,93,124]
Stage15 0x013368 28 500 ADN215 0x22 type2 [20,58,15] [47,92,1]
Stage15 0x01358C 28 500 ADN216 0x2A type2 [20,58,15] [47,92,11]
Stage15 0x01378C 28 500 ADN217 0x32 type2 [20,58,15] [47,47,92]
Stage15 0x0139E0 28 500 ADN218 0x37 type2 [20,58,15] [47,47,92]
Stage15 0x013C34 28 500 ADN219 0x39 type2 [20,58,15] [47,47,92]
Stage15 0x013E88 28 500 ADN220 0xA type2 [20,58,15] [47,92,11]
Stage15 0x014088 28 500 ADN221 0x11 type2 [20,58,15] [47,92,11]
Stage15 0x014924 28 150 ADN222 0x17 type2 [64,11,57] [15,64,64]
Stage15 0x014D28 28 500 ADN223 0x1E type2 [11,58,15] [47,47,92]
Stage15 0x015170 28 150 ADN224 0x23 type2 [64,11,57] [15,64,64]
Stage15 0x01553C 28 500 ADN225 0x2B type2 [11,58,15] [47,92,64]
Stage15 0x0158E8 28 500 ADN226 0x33 type2 [11,58,15] [47,92,58]
Stage15 0x015A84 28 500 ADN227 0x38 type2 [92,58,15] [47,92,64]
Stage16 0x0004F4 29 100 TCN001 0x1 type2 [8,127,123] [35,119,11]
Stage16 0x0005D8 101 - ? None typeNone [11,95,116] [100,93,124]
Stage16 0x000A28 101 - ? None typeNone [11,9,116] [100,93,124]
Stage16 0x000E4C 101 - ? None typeNone [140,140,116] [100,93,124]
Stage18 0x000488 28 0 TCN001 0xB type2 [35,8,12] [52,1,11]
Stage18 0x0009D4 26 0 ADT101 0x0 type2 [1,11,18] [13,0,97]
Stage18 0x000D2C 26 0 ADT102 0x2 type2 [1,11,18] [13,0,11]
Stage18 0x000E30 26 0 ADT103 0x4 type2 [0,11,18] [13,0,11]
Stage18 0x000F34 26 0 ADT104 0x5 type2 [0,11,18] [13,0,11]
Stage18 0x0013E4 26 0 ADT105 0x6 type2 [1,11,18] [13,0,11]
Stage18 0x0014E8 26 0 ADT106 0x7 type2 [0,11,18] [13,0,11]
Stage18 0x0015EC 26 0 ADT107 0x8 type2 [0,11,18] [13,0,11]
Stage18 0x001AB0 26 0 ADT108 0x9 type2 [1,11,18] [13,0,97]
Stage18 0x001E2C 28 100 TCN001 0xB type2 [4,1,11] [12,3,15]
Stage18 0x0029FC 26 100 TCN001 0xB type2 [6,11,20] []
Stage19 0x0003D4 28 0 TCN001 0x7 type2 [35,8,12] [52,1,11]
Stage19 0x000CD4 28 100 TCN001 0x7 type2 [11,104,4] [1,11,69]
Stage19 0x000F0C 26 0 ADS101 0x0 type2 [16,0,12] [26,84,4]
Stage19 0x000F4C 26 0 ADS102 0x1 type2 [0,12,26] [84,4,97]
Stage19 0x001010 28 0 TCN001 0x7 type2 [4,97,4] [12,3,15]
Stage19 0x0012F8 26 0 ADS104 0x2 type2 [4,16,0] [84,4,97]
Stage19 0x001394 28 100 TCN001 0x7 type2 [84,4,97] [12,3,15]
Stage19 0x0019F4 26 100 TCN001 0x7 type2 [6,11,20] []
Stage20 0x0026C0 26 100 TCN001 0xB type2 [6,11,20] []
Stage21 0x0002A8 28 0 TCN001 0x2 type2 [35,8,12] [26,52,1]
Stage21 0x0002E8 26 40 TCN001 0x2 type2 [8,12,28] [52,1,12]
Stage21 0x000B58 28 100 TCN001 0x2 type2 [0,84,4] [97,4,12]
Stage21 0x0012F4 26 100 TCN001 0x2 type2 [6,11,20] []
Stage22 0x00098C 26 0 ADT101 0x0 type2 [1,11,18] [0,11,69]
Stage22 0x000AD0 26 0 ADT102 0x2 type2 [69,11,18] [0,11,69]
Stage22 0x001384 26 0 ADS103 0x1 type2 [11,69,0] [97,1,11]
Stage22 0x001470 26 0 ADT104 0x3 type2 [1,11,18] [0,11,69]
Stage22 0x001868 26 0 ADT106 0x5 type2 [1,11,18] [0,11,69]
Stage22 0x001F1C 26 0 ADT107 0x6 type2 [1,11,18] [0,11,69]
Stage22 0x002130 26 100 TCN001 0x7 type2 [6,11,20] []
Stage23 0x0002A8 28 0 TCN001 0x3 type2 [35,8,12] [52,1,11]
Stage23 0x000898 26 0 ADS105 0x2 type2 [4,16,0] [84,4,97]
Stage23 0x000B98 28 100 TCN001 0x3 type2 [11,104,4] [1,11,69]
Stage23 0x0012AC 26 100 TCN001 0x3 type2 [6,11,20] []
Stage24 0x001274 29 0 ADT108 0x1E type2 [12,30,117] [12,12,12]
Stage24 0x001EE8 29 100 TCN001 0x2C type2 [127,123,8] [35,119,1]
Stage24 0x0029B0 29 100 ADT108 0x1E type2 [15,47,64] [20,64,11]
Stage24 0x0045F8 29 100 ADT109 0x22 type2 [15,48,47] [64,1,7]
Stage24 0x004A44 29 0 ADT109 0x22 type2 [58,30,117] [64,7,58]
Stage24 0x0051B8 29 100 ADT103 0x8 type2 [30,15,47] [64,64,1]
Stage24 0x0056B8 29 0 ADT103 0x8 type2 [58,30,117] [64,20,4]
Stage24 0x007094 101 - ? None typeNone [20,20,116] [100,124,93]
Stage24 0x007304 101 - ? None typeNone [11,95,116] [100,124,93]
Stage24 0x007710 101 - ? None typeNone [11,20,116] [100,124,93]
Stage24 0x007DBC 101 - ? None typeNone [11,9,116] [100,124,93]
Stage25 0x001790 28 100 ADN355 0x21 type2 [15,12,15] [12,15,28]
Stage25 0x001864 28 100 ADN350 0xF type2 [28,12,15] [12,15,28]
Stage25 0x001938 28 100 ADN351 0x13 type2 [28,12,15] [12,15,28]
Stage25 0x001A0C 28 100 ADN352 0x17 type2 [28,12,15] [12,15,28]
Stage25 0x001AE0 28 100 ADN353 0x1B type2 [28,12,15] [12,15,28]
Stage25 0x001BB4 28 100 ADN354 0x1E type2 [28,12,15] [12,15,28]
Stage25 0x001C88 28 200 ADN310 0x1 type2 [28,12,15] [12,15,117]
Stage25 0x004218 29 100 TCN001 0x27 type2 [127,123,8] [35,119,1]
Stage25 0x00440C 28 300 ADN312 0x5 type2 [11,57,16] [1,11,57]
Stage25 0x0044F8 28 300 ADN313 0x9 type2 [11,57,16] [1,11,58]
Stage25 0x004600 28 600 ADN314 0xD type2 [11,58,15] [47,47,1]
Stage25 0x004964 28 600 ADN315 0x11 type2 [11,58,15] [47,47,1]
Stage25 0x005348 28 100 ADN350 0xF type2 [143,57,15] [145,47,47]
Stage25 0x005814 28 200 ADN356 0x23 type2 [5,57,15] [145,47,47]
Stage25 0x005BAC 28 200 ADN356 0x23 type2 [143,57,15] [145,47,47]
Stage25 0x00709C 28 100 ADN351 0x13 type2 [143,57,15] [145,47,47]
Stage25 0x0074C8 28 200 ADN357 0x24 type2 [5,57,15] [145,47,47]
Stage25 0x007860 28 200 ADN357 0x24 type2 [143,57,15] [145,47,47]
Stage25 0x0088E8 28 100 ADN352 0x17 type2 [143,57,15] [145,47,47]
Stage25 0x008D14 28 200 ADN358 0x25 type2 [5,57,15] [145,47,47]
Stage25 0x0090AC 28 200 ADN358 0x25 type2 [143,57,15] [145,47,47]
Stage25 0x00A59C 28 100 ADN353 0x1B type2 [143,57,15] [145,47,47]
Stage25 0x00A9C8 28 200 ADN359 0x26 type2 [5,57,15] [145,47,47]
Stage25 0x00AD60 28 200 ADN359 0x26 type2 [143,57,15] [145,47,47]
Stage25 0x00BC70 28 100 ADN354 0x1E type2 [143,57,15] [145,47,47]
Stage25 0x00C09C 28 200 ADN360 0x14 type2 [5,57,15] [145,47,47]
Stage25 0x00C434 28 200 ADN360 0x14 type2 [143,57,15] [145,47,47]
Stage25 0x00D924 28 100 ADN355 0x21 type2 [143,57,15] [145,47,47]
Stage25 0x00DD50 28 200 ADN361 0x18 type2 [5,57,15] [145,47,47]
Stage25 0x00E0E8 28 200 ADN361 0x18 type2 [143,57,15] [145,47,47]
Stage25 0x00ECD4 28 200 ADN310 0x1 type2 [143,57,15] [19,64,20]
Stage25 0x00F390 28 300 ADN311 0x2 type2 [5,57,15] [19,1,64]
Stage25 0x00F704 28 300 ADN311 0x2 type2 [143,57,15] [19,64,8]
Stage25 0x010EFC 28 300 ADN312 0x5 type2 [9,143,57] [16,64,69]
Stage25 0x0111A8 26 0 ADN320 0x3 type2 [69,20,18] [69,8,69]
Stage25 0x011450 28 300 ADN320 0x3 type2 [9,128,15] [47,47,1]
Stage25 0x011C0C 28 300 ADN320 0x3 type2 [143,128,15] [47,47,1]
Stage25 0x011FFC 28 300 ADN320 0x3 type2 [143,128,15] [47,47,1]
Stage25 0x012240 26 0 ADN321 0x7 type2 [69,20,18] [69,8,69]
Stage25 0x0124E8 28 300 ADN321 0x7 type2 [9,128,15] [47,47,1]
Stage25 0x012CA4 28 300 ADN321 0x7 type2 [143,128,15] [47,47,1]
Stage25 0x013094 28 300 ADN321 0x7 type2 [143,128,15] [47,47,1]
Stage25 0x0134C0 28 300 ADN328 0x20 type2 [9,128,15] [47,1,69]
Stage25 0x013BE8 28 300 ADN328 0x20 type2 [143,128,15] [47,1,69]
Stage25 0x0142CC 28 300 ADN313 0x9 type2 [9,143,57] [16,64,69]
Stage25 0x0146F4 28 300 ADN325 0x16 type2 [9,128,15] [47,1,69]
Stage25 0x014E1C 28 300 ADN325 0x16 type2 [143,128,15] [47,1,18]
Stage25 0x0151F4 28 300 ADN329 0x22 type2 [9,128,15] [47,1,69]
Stage25 0x01591C 28 300 ADN329 0x22 type2 [143,128,15] [47,1,69]
Stage25 0x016058 28 600 ADN314 0xD type2 [143,57,15] [47,47,145]
Stage25 0x016B84 28 600 ADN315 0x11 type2 [143,57,15] [47,47,145]
Stage25 0x017094 101 - ? None typeNone [64,95,116] [100,124,93]
Stage25 0x01748C 101 - ? None typeNone [11,20,116] [100,124,93]
Stage25 0x017AE8 101 - ? None typeNone [11,9,116] [100,124,93]
Stage26 0x000EF0 29 0 TCN001 0x16 type2 [118,146,12] [15,12,15]
Stage26 0x001614 29 50 TCT012 0x28 type2 [15,3,30] [12,15,3]
Stage26 0x0017F4 29 50 TCT013 0x2A type2 [15,3,30] [12,15,3]
Stage26 0x0019D4 29 50 TCT014 0x2C type2 [15,3,30] [12,15,3]
Stage26 0x001BB4 29 50 TCT015 0x2E type2 [15,3,30] [12,15,3]
Stage26 0x001D94 29 50 TCT016 0x30 type2 [15,3,30] [12,15,3]
Stage26 0x001F74 29 50 TCT017 0x32 type2 [15,3,30] [12,15,3]
Stage26 0x002154 29 50 TCT018 0x34 type2 [15,3,30] [12,15,3]
Stage26 0x002334 29 50 TCT019 0x36 type2 [15,3,30] [12,15,3]
Stage26 0x003928 29 100 TCN001 0x16 type2 [127,123,8] [35,11,95]
Stage26 0x003A00 101 - ? None typeNone [11,95,116] [100,124,93]
Stage26 0x003E10 101 - ? None typeNone [11,20,116] [100,124,93]
Stage26 0x004414 101 - ? None typeNone [6,11,116] [100,124,93]
Stage26 0x004664 101 - ? None typeNone [11,9,116] [100,124,93]
Stage26 0x008DD0 29 20 ADN128 0x14 type2 [47,11,57] [15,3,46]
Stage26 0x00AD0C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
Stage26 0x00AD4C 26 0 TCT012 0x28 type2 [5,5,26] [4,64,11]
Stage26 0x00AF4C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
Stage26 0x00AF8C 26 0 TCT013 0x2A type2 [5,5,26] [4,64,11]
Stage26 0x00B18C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
Stage26 0x00B1CC 26 0 TCT014 0x2C type2 [5,5,26] [4,64,11]
Stage26 0x00B3CC 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
Stage26 0x00B40C 26 0 TCT015 0x2E type2 [5,5,26] [4,64,11]
Stage26 0x00B60C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
Stage26 0x00B64C 26 0 TCT016 0x30 type2 [5,5,26] [4,64,11]
Stage26 0x00B84C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
Stage26 0x00B88C 26 0 TCT017 0x32 type2 [5,5,26] [4,64,11]
Stage26 0x00BA8C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
Stage26 0x00BACC 26 0 TCT018 0x34 type2 [5,5,26] [4,64,11]
Stage26 0x00BCCC 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
Stage26 0x00BD0C 26 0 TCT019 0x36 type2 [5,5,26] [4,64,11]
Stage27 0x000AAC 29 0 TCN001 0x12 type2 [118,146,12] [15,12,15]
Stage27 0x002088 29 100 TCN001 0x12 type2 [127,123,8] [35,11,95]
Stage27 0x002160 101 - ? None typeNone [11,95,116] [100,124,93]
Stage27 0x002570 101 - ? None typeNone [11,20,116] [100,124,93]
Stage27 0x002BBC 101 - ? None typeNone [11,9,116] [100,124,93]
Stage27 0x00339C 101 - ? None typeNone [20,20,116] [100,124,93]
Stage28 0x00101C 29 0 TCN001 0x13 type2 [118,146,12] [15,12,30]
Stage28 0x0020EC 29 100 TCN001 0x13 type2 [127,123,8] [35,11,95]
Stage28 0x0021C4 101 - ? None typeNone [11,95,116] [100,124,93]
Stage28 0x00256C 101 - ? None typeNone [20,20,116] [100,124,93]
Stage28 0x002958 101 - ? None typeNone [11,9,116] [100,124,93]
Stage28 0x002E4C 29 0 ADT102 0x2 type2 [12,12,12] [29,29,136]
Stage28 0x002E8C 29 0 ADT103 0x4 type2 [12,12,29] [29,136,13]
Stage28 0x002ECC 29 0 ADT104 0x6 type2 [12,29,29] [136,13,1]
Stage28 0x003BE8 29 0 ADT107 0xB type2 [12,12,12] [29,29,136]
Stage28 0x003C28 29 0 ADT108 0xC type2 [12,12,29] [29,136,13]
Stage28 0x003C68 29 0 ADT109 0xD type2 [12,29,29] [136,13,64]
Stage28 0x004848 29 0 ADT113 0x7 type2 [12,12,12] [29,29,136]
Stage28 0x004888 29 0 ADT114 0x8 type2 [12,12,29] [29,136,13]
Stage28 0x0048C8 29 0 ADT115 0xA type2 [12,29,29] [136,13,1]
Stage28 0x0050F8 26 0 ADT102 0x2 type2 [11,70,18] [13,70,136]
Stage28 0x005330 26 0 ADT103 0x4 type2 [11,70,18] [13,70,11]
Stage28 0x005520 26 0 ADT104 0x6 type2 [11,70,18] [13,11,70]
Stage28 0x005638 26 0 ADT107 0xB type2 [11,70,18] [13,70,136]
Stage28 0x005870 26 0 ADT108 0xC type2 [11,70,18] [13,70,11]
Stage28 0x005A60 26 0 ADT109 0xD type2 [11,70,18] [13,11,70]
Stage28 0x005B78 26 0 ADT113 0x7 type2 [11,70,18] [13,70,136]
Stage28 0x005DB0 26 0 ADT114 0x8 type2 [11,70,18] [13,70,11]
Stage28 0x005FA0 26 0 ADT115 0xA type2 [11,70,18] [13,11,64]
Stage29 0x0083A0 29 100 TCN001 0x11 type2 [12,12,12] [15,29,15]
Stage29 0x008438 29 100 TCN002 0x14 type2 [12,29,15] [15,29,15]
Stage29 0x0084D0 29 50 TCN003 0x18 type2 [15,29,15] [15,92,15]
Stage29 0x008740 29 75 TCN004 0x1F type2 [15,30,3] [15,3,29]
Stage29 0x008878 29 50 TCN005 0x21 type2 [29,15,3] [15,30,19]
Stage29 0x008AA4 29 75 TCT006 0x2C type2 [30,19,3] [15,3,3]
Stage29 0x008C7C 29 50 TCN007 0x25 type2 [15,3,3] [15,3,3]
Stage29 0x008E54 29 50 TCN110 0x17 type2 [15,3,3] [15,3,3]
Stage29 0x00902C 29 50 TCN111 0x1D type2 [15,3,3] [15,3,3]
Stage29 0x009204 29 50 TCN113 0x22 type2 [15,3,3] [15,30,29]
Stage29 0x009308 29 0 TCN008 0x26 type2 [29,15,30] [92,15,48]
Stage29 0x0094B4 29 50 TCN009 0x29 type2 [92,15,48] [15,92,29]
Stage29 0x0095C0 29 50 TCN114 0x24 type2 [29,15,92] [1,15,92]
Stage29 0x0096F0 29 50 TCN116 0x28 type2 [1,15,92] [1,15,48]
Stage29 0x00984C 29 50 TCN119 0x2B type2 [1,15,48] [1,1,75]
Stage29 0x00CCF0 29 100 TCN001 0x11 type2 [127,123,8] [35,119,11]
Stage29 0x010580 101 - ? None typeNone [69,95,116] [100,93,124]
Stage29 0x0109A8 101 - ? None typeNone [11,20,116] [100,124,93]
Stage29 0x011004 101 - ? None typeNone [11,20,116] [100,124,93]
Stage29 0x01128C 101 - ? None typeNone [11,9,116] [100,93,124]
Stage29 0x01158C 101 - ? None typeNone [6,11,116] [100,93,124]
Stage29 0x011D74 29 50 TCN120 0x1E type2 [128,15,92] [11,20,11]
Stage29 0x011FD0 29 50 TCN121 0x20 type2 [128,15,92] [11,69,57]
Stage29 0x012D0C 28 200 ADN101ea1 0x0 type2 [145,128,15] [145,47,47]
Stage29 0x013294 28 200 ADN101ea2 0x1 type2 [1,128,15] [29,145,47]
Stage29 0x0132D4 29 50 ADN101ea2 0x1 type2 [128,15,28] [145,47,47]
Stage29 0x01381C 28 200 ADN102ea1 0x2 type2 [145,128,15] [145,47,47]
Stage29 0x013DA4 28 200 ADN102ea2 0x3 type2 [1,128,15] [29,145,47]
Stage29 0x013DE4 29 50 ADN102ea2 0x3 type2 [128,15,28] [145,47,47]
Stage29 0x01432C 28 200 ADN103ea1 0x4 type2 [145,128,15] [145,47,47]
Stage29 0x0148B4 28 200 ADN103ea2 0x6 type2 [1,128,15] [29,145,47]
Stage29 0x0148F4 29 50 ADN103ea2 0x6 type2 [128,15,28] [145,47,47]
Stage29 0x014E3C 28 200 ADN104ea1 0x7 type2 [145,128,15] [145,47,47]
Stage29 0x0153C4 28 200 ADN104ea2 0xA type2 [1,128,15] [29,145,47]
Stage29 0x015404 29 50 ADN104ea2 0xA type2 [128,15,28] [145,47,47]
Stage29 0x01594C 28 200 ADN121ea1 0x5 type2 [145,128,15] [145,47,47]
Stage29 0x015EB0 28 200 ADN121ea2 0x8 type2 [92,128,15] [29,145,47]
Stage29 0x015EF0 29 50 ADN121ea2 0x8 type2 [128,15,28] [145,47,47]
Stage29 0x016338 28 200 ADN122ea1 0x9 type2 [145,128,15] [145,47,47]
Stage29 0x01689C 28 200 ADN122ea2 0xB type2 [92,128,15] [29,145,47]
Stage29 0x0168DC 29 50 ADN122ea2 0xB type2 [128,15,28] [145,47,47]
Stage29 0x016D24 28 200 ADN123ea1 0xC type2 [145,128,15] [145,47,47]
Stage29 0x017288 28 200 ADN123ea2 0xD type2 [92,128,15] [29,145,47]
Stage29 0x0172C8 29 50 ADN123ea2 0xD type2 [128,15,28] [145,47,47]
Stage29 0x017710 28 200 ADN124ea1 0xE type2 [145,128,15] [145,47,47]
Stage29 0x017C74 28 200 ADN124ea2 0xF type2 [92,128,15] [29,145,47]
Stage29 0x017CB4 29 50 ADN124ea2 0xF type2 [128,15,28] [145,47,47]
Stage29 0x01C5FC 28 200 ADN101b 0x2F type2 [64,57,15] [1,4,57]
Stage29 0x01C71C 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
Stage29 0x01C83C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
Stage29 0x01C95C 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
Stage29 0x01CA7C 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
Stage29 0x01CB9C 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
Stage29 0x01CCBC 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
Stage29 0x01CDDC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
Stage29 0x01CEFC 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
Stage29 0x01D01C 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
Stage29 0x01D13C 28 300 ADN104e 0x5C type2 [4,57,15] [1,4,57]
Stage29 0x01D25C 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
Stage29 0x01D37C 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,57]
Stage29 0x01D49C 28 120 ADN107a 0x56 type2 [4,57,15] [1,4,57]
Stage29 0x01D5C4 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
Stage29 0x01D6E4 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
Stage29 0x01D804 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
Stage29 0x01D924 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
Stage29 0x01DA44 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
Stage29 0x01DB64 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
Stage29 0x01DC84 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
Stage29 0x01DDA4 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
Stage29 0x01DEC4 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
Stage29 0x01DFE4 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
Stage29 0x01E104 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
Stage29 0x01E224 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
Stage29 0x01E344 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,57]
Stage29 0x01E464 28 200 ADN104b 0x42 type2 [4,57,15] [1,4,57]
Stage29 0x01E58C 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
Stage29 0x01E6AC 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
Stage29 0x01E7CC 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
Stage29 0x01E8EC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
Stage29 0x01EA0C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
Stage29 0x01EB2C 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
Stage29 0x01EC4C 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
Stage29 0x01ED6C 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
Stage29 0x01EE8C 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
Stage29 0x01EFAC 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
Stage29 0x01F0CC 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
Stage29 0x01F1EC 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
Stage29 0x01F30C 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
Stage29 0x01F42C 28 300 ADN104e 0x5C type2 [4,57,15] [1,57,15]
Stage29 0x01F52C 28 200 ADN101c 0x32 type2 [1,57,15] [1,4,57]
Stage29 0x01F64C 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
Stage29 0x01F76C 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
Stage29 0x01F88C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
Stage29 0x01F9AC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
Stage29 0x01FACC 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
Stage29 0x01FBEC 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
Stage29 0x01FD0C 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
Stage29 0x01FE2C 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
Stage29 0x01FF4C 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
Stage29 0x02006C 28 300 ADN101d 0x37 type2 [4,57,15] [1,4,57]
Stage29 0x02018C 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
Stage29 0x0202AC 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
Stage29 0x0203CC 28 300 ADN104e 0x5C type2 [4,57,15] [1,4,57]
Stage29 0x0204F4 28 300 ADN101d 0x37 type2 [4,57,15] [1,4,57]
Stage29 0x020614 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
Stage29 0x020734 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
Stage29 0x020854 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
Stage29 0x020974 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
Stage29 0x020A94 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
Stage29 0x020BB4 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
Stage29 0x020CD4 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
Stage29 0x020DF4 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
Stage29 0x020F14 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
Stage29 0x021034 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
Stage29 0x021154 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
Stage29 0x021274 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
Stage29 0x021394 28 300 ADN104e 0x5C type2 [4,57,15] [1,4,57]
Stage29 0x0214BC 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
Stage29 0x0215DC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
Stage29 0x0216FC 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
Stage29 0x02181C 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
Stage29 0x02193C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
Stage29 0x021A5C 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
Stage29 0x021B7C 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
Stage29 0x021C9C 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
Stage29 0x021DBC 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
Stage29 0x021EDC 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,57]
Stage29 0x021FFC 28 200 ADN101c 0x32 type2 [4,57,15] [1,57,15]
Stage29 0x0220F4 28 200 ADN102b 0x33 type2 [1,57,15] [1,4,57]
Stage29 0x022214 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
Stage29 0x022334 28 300 ADN104e 0x5C type2 [4,57,15] [1,57,15]
Stage29 0x022434 28 300 ADN101e 0x40 type2 [1,57,15] [1,4,57]
Stage29 0x022554 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
Stage29 0x022674 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
Stage29 0x022794 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
Stage29 0x0228B4 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
Stage29 0x0229D4 28 300 ADN101d 0x37 type2 [4,57,15] [1,4,57]
Stage29 0x022AF4 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
Stage29 0x022C14 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
Stage29 0x022D34 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
Stage29 0x022E54 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
Stage29 0x022F74 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
Stage29 0x023094 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
Stage29 0x0231B4 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
Stage29 0x0232D4 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,11]
Stage29 0x023764 28 200 ADN141a 0x3E type2 [16,16,16] [145,47,47]
Stage29 0x023EFC 28 200 ADN141b 0x48 type2 [16,16,16] [29,145,47]
Stage29 0x023F3C 29 50 ADN141b 0x48 type2 [16,16,28] [145,47,47]
Stage29 0x024420 28 200 ADN142a 0x49 type2 [16,16,16] [145,47,47]
Stage29 0x024BB8 28 200 ADN142b 0x52 type2 [16,16,16] [29,145,47]
Stage29 0x024BF8 29 50 ADN142b 0x52 type2 [16,16,28] [145,47,47]
Stage29 0x02517C 28 200 ADN143a 0x53 type2 [16,16,16] [145,47,47]
Stage29 0x025914 28 200 ADN143b 0x5A type2 [16,16,16] [29,145,47]
Stage29 0x025954 29 50 ADN143b 0x5A type2 [16,16,28] [145,47,47]
Stage29 0x025F78 28 200 ADN144a 0x5B type2 [16,16,16] [145,47,47]
Stage29 0x026710 28 200 ADN144b 0x61 type2 [16,16,16] [29,145,47]
Stage29 0x026750 29 50 ADN144b 0x61 type2 [16,16,28] [145,47,47]
Stage29 0x026CD4 28 200 ADN145a 0x62 type2 [16,16,16] [145,47,47]
Stage29 0x02746C 28 200 ADN145b 0x65 type2 [16,16,16] [29,145,47]
Stage29 0x0274AC 29 50 ADN145b 0x65 type2 [16,16,28] [145,47,47]
Stage29 0x02783C 28 200 ADN121b 0x35 type2 [64,57,15] [1,4,57]
Stage29 0x02795C 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
Stage29 0x027A7C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
Stage29 0x027B9C 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
Stage29 0x027CBC 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
Stage29 0x027DDC 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
Stage29 0x027EFC 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
Stage29 0x02801C 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
Stage29 0x02813C 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
Stage29 0x02825C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
Stage29 0x02837C 28 300 ADN124e 0x63 type2 [4,57,15] [1,4,57]
Stage29 0x02849C 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
Stage29 0x0285BC 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,57]
Stage29 0x0286DC 28 120 ADN127a 0x60 type2 [4,57,15] [1,4,64]
Stage29 0x0288A4 28 200 ADN121c 0x3B type2 [64,57,15] [1,4,57]
Stage29 0x0289C4 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
Stage29 0x028AE4 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
Stage29 0x028C04 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
Stage29 0x028D24 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
Stage29 0x028E44 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
Stage29 0x028F64 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
Stage29 0x029084 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
Stage29 0x0291A4 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
Stage29 0x0292C4 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
Stage29 0x0293E4 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
Stage29 0x029504 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
Stage29 0x029624 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,57]
Stage29 0x029744 28 200 ADN124b 0x50 type2 [4,57,15] [1,4,64]
Stage29 0x0299AC 28 200 ADN121c 0x3B type2 [64,57,15] [1,4,57]
Stage29 0x029ACC 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
Stage29 0x029BEC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
Stage29 0x029D0C 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
Stage29 0x029E2C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
Stage29 0x029F4C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
Stage29 0x02A06C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
Stage29 0x02A18C 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
Stage29 0x02A2AC 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
Stage29 0x02A3CC 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
Stage29 0x02A4EC 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
Stage29 0x02A60C 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
Stage29 0x02A72C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
Stage29 0x02A84C 28 300 ADN124e 0x63 type2 [4,57,15] [1,64,64]
Stage29 0x02AA8C 28 200 ADN121c 0x3B type2 [64,57,15] [1,4,57]
Stage29 0x02ABAC 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
Stage29 0x02ACCC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
Stage29 0x02ADEC 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
Stage29 0x02AF0C 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
Stage29 0x02B02C 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
Stage29 0x02B14C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
Stage29 0x02B26C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
Stage29 0x02B38C 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
Stage29 0x02B4AC 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
Stage29 0x02B5CC 28 300 ADN121d 0x44 type2 [4,57,15] [1,4,57]
Stage29 0x02B6EC 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
Stage29 0x02B80C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
Stage29 0x02B92C 28 300 ADN124e 0x63 type2 [4,57,15] [1,4,64]
Stage29 0x02BB94 28 300 ADN121d 0x44 type2 [64,57,15] [1,4,57]
Stage29 0x02BCB4 28 200 ADN121c 0x3B type2 [4,57,15] [1,4,57]
Stage29 0x02BDD4 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
Stage29 0x02BEF4 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
Stage29 0x02C014 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
Stage29 0x02C134 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
Stage29 0x02C254 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
Stage29 0x02C374 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
Stage29 0x02C494 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
Stage29 0x02C5B4 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
Stage29 0x02C6D4 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
Stage29 0x02C7F4 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
Stage29 0x02C914 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
Stage29 0x02CA34 28 300 ADN124e 0x63 type2 [4,57,15] [1,4,64]
Stage29 0x02CC9C 28 200 ADN121b 0x35 type2 [64,57,15] [1,4,57]
Stage29 0x02CDBC 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
Stage29 0x02CEDC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
Stage29 0x02CFFC 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
Stage29 0x02D11C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
Stage29 0x02D23C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
Stage29 0x02D35C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
Stage29 0x02D47C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
Stage29 0x02D59C 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
Stage29 0x02D6BC 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,57]
Stage29 0x02D7DC 28 200 ADN121c 0x3B type2 [4,57,15] [1,4,57]
Stage29 0x02D8FC 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
Stage29 0x02DA1C 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
Stage29 0x02DB3C 28 300 ADN124e 0x63 type2 [4,57,15] [1,64,64]
Stage29 0x02DD7C 28 300 ADN121e 0x4E type2 [64,57,15] [1,4,57]
Stage29 0x02DE9C 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
Stage29 0x02DFBC 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
Stage29 0x02E0DC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
Stage29 0x02E1FC 28 200 ADN121c 0x3B type2 [4,57,15] [1,4,57]
Stage29 0x02E31C 28 300 ADN121d 0x44 type2 [4,57,15] [1,4,57]
Stage29 0x02E43C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
Stage29 0x02E55C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
Stage29 0x02E67C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
Stage29 0x02E79C 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
Stage29 0x02E8BC 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
Stage29 0x02E9DC 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
Stage29 0x02EAFC 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
Stage29 0x02EC1C 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,64]
Stage29 0x02F1EC 28 200 ADN146a 0x66 type2 [16,16,16] [145,47,47]
Stage29 0x02F960 28 200 ADN146b 0x67 type2 [16,16,16] [29,145,47]
Stage29 0x02F9A0 29 50 ADN146b 0x67 type2 [16,16,28] [145,47,47]
Stage29 0x02FF24 28 200 ADN147a 0x68 type2 [16,16,16] [145,47,47]
Stage29 0x030698 28 200 ADN147b 0x69 type2 [16,16,16] [29,145,47]
Stage29 0x0306D8 29 50 ADN147b 0x69 type2 [16,16,28] [145,47,47]
Stage29 0x030C5C 28 200 ADN148a 0x6A type2 [16,16,16] [145,47,47]
Stage29 0x0313D0 28 200 ADN148b 0x6B type2 [16,16,16] [29,145,47]
Stage29 0x031410 29 50 ADN148b 0x6B type2 [16,16,28] [145,47,47]
Stage29 0x031994 28 200 ADN149a 0x6C type2 [16,16,16] [145,47,47]
Stage29 0x032108 28 200 ADN149b 0x6D type2 [16,16,16] [29,145,47]
Stage29 0x032148 29 50 ADN149b 0x6D type2 [16,16,28] [145,47,47]
Stage29 0x0326CC 28 200 ADN150a 0x3F type2 [16,16,16] [145,47,47]
Stage29 0x032E40 28 200 ADN150b 0x4A type2 [16,16,16] [29,145,47]
Stage29 0x032E80 29 50 ADN150b 0x4A type2 [16,16,28] [145,47,47]
# builtin 26: 97 sites, 7 distinct values
# values: 0 x69, 80 x10, 100 x6, 60 x5, 30 x3, 50 x3, 40 x1
# units: 66 distinct, top: ADN128 x8, TCN001 x7, ADN207 x4, TCN208 x4, TCN002 x3, ADT102 x3, ADT104 x3, ADT107 x3, ADT101 x2, ADT103 x2
# craft cross-tab (0 unresolved):
# UN_e201_ADAN_ISCMissile 17 0x17
# UN_n001_TTRL_Box 16 0x16
# UN_f202_TCAF_Cargo 9 0x9
# UN_e106_ADAN_Destroyer 8 0x4, 80x2, 30x1, 60x1
# UN_f106_TCAF_Destroyer 8 0x4, 60x2, 80x1, 50x1
# UN_f001_TCAF_DeltaSaber_T_Player_Ttrl1 6 100x5, 40x1
# UN_f105_TCAF_Cruiser 5 0x3, 80x1, 50x1
# UN_e015_ADAN_Puppy_2 5 0x5
# UN_e015_ADAN_Puppy 5 0x5
# UN_e108_ADAN_ASFrigate 4 80x2, 60x1, 30x1
# UN_f001_TCAF_DeltaSaber_T 3 80x1, 50x1, 30x1
# UN_mn500_ADAN_FloatingMine 3 0x3
# UN_e105_ADAN_Cruiser 2 80x1, 60x1
# UN_e011_ADAN_Attacker_B 2 0x2
# UN_f101_TCAF_Acropolis 1 80x1
# UN_f002_TCAF_DeltaSaber_W 1 80x1
# UN_n001_TTRL_Box_move 1 0x1
# UN_f001_TCAF_DeltaSaber_T_Player_Ttrl2 1 100x1
# UN_f001_TCAF_DeltaSaber_T_Ttrl 1 100x1
# builtin 28: 410 sites, 13 distinct values
# values: 200 x116, 120 x76, 300 x74, 500 x48, 150 x37, 100 x29, 1000 x8, 2000 x5, 0.1 x5, 0 x5, 600 x4, 10 x2, 50 x1
# units: 190 distinct, top: TCN001 x13, ADN101b x7, ADN101a x7, ADN102a x7, ADN102b x7, ADN103a x7, ADN104a x7, ADN101e x7, ADN102e x7, ADN121b x7
# craft cross-tab (0 unresolved):
# UN_e106_ADAN_Destroyer 105 120x76, 150x15, 100x12, 200x2
# UN_e104_ADAN_Carrier 55 300x54, 150x1
# UN_e105_ADAN_CruiserEX 52 200x52
# UN_e102_ADAN_BattleshipEX 26 200x26
# UN_e001_ADAN_Elan 24 200x18, 300x6
# UN_e010_ADAN_Attacker_S 21 200x18, 100x3
# UN_e004_ADAN_ElanPlus_N 19 500x19
# UN_e009_ADAN_Phantom 16 500x16
# UN_f001_TCAF_DeltaSaber_T_Player_Ttrl1 10 0x5, 100x5
# UN_e007_ADAN_Turret 9 100x9
# UN_e002_ADAN_Elan_N 9 500x9
# UN_e105_ADAN_Cruiser 8 150x8
# UN_e101_ADAN_SDBattleshipEX 8 300x8
# UN_f001_TCAF_DeltaSaber_T 7 150x4, 10x2, 50x1
# UN_e011_ADAN_Attacker_B 6 300x6
# UN_f002_TCAF_DeltaSaber_W 5 150x5
# UN_e003_ADAN_ElanPlus_Margras 4 2000x4
# UN_e108_ADAN_ASFrigateEX 4 600x4
# UN_f106_TCAF_Destroyer_Inv 3 0.1x2, 150x1
# UN_e001_ADAN_Elan_GR_Violeta 3 1000x3
# UN_e001_ADAN_Elan_GR 3 1000x3
# UN_e101_ADAN_SDBattleship 3 150x3
# UN_f101_TCAF_Acropolis 2 150x1, 0.1x1
# UN_e013_ADAN_ElanPlus_Taskent 2 1000x2
# UN_f002_TCAF_DeltaSaber_W_Player 2 150x2
# UN_e011_ADAN_Attacker_B_HF 2 500x2
# UN_e102_ADAN_Battleship 2 150x2
# UN_f106_TCAF_Destroyer 1 150x1
# UN_f001_TCAF_DeltaSaber_T_Player 1 150x1
# UN_f105_TCAF_Cruiser 1 0.1x1
# UN_f102_TCAF_LightCarrier_Inv 1 0.1x1
# UN_e005_ADAN_ElanTypeQ_Margras 1 2000x1
# UN_e108_ADAN_ASFrigate 1 150x1
# UN_e011_ADAN_Attacker_B_HF_Wayne 1 500x1
# UN_e006_ADAN_Vindicator_Margras 1 500x1
# builtin 29: 164 sites, 6 distinct values
# values: 0 x64, 100 x53, 50 x38, 40 x6, 75 x2, 20 x1
# units: 70 distinct, top: TCN001 x71, TCN002 x10, TCT207 x4, TCN008 x3, ADT108 x3, ADT109 x3, ADT103 x3, TCN003 x2, ADS151a x2, ADS151b x2
# craft cross-tab (0 unresolved):
# UN_f002_TCAF_DeltaSaber_W_Player 41 100x23, 0x18
# UN_f001_TCAF_DeltaSaber_T 40 0x25, 100x15
# UN_f002_TCAF_DeltaSaber_W 38 100x21, 0x16, 50x1
# UN_f001_TCAF_DeltaSaber_T_Player 30 0x15, 100x15
# UN_e010_ADAN_Attacker_S 18 50x18
# UN_f202_TCAF_Cargo 14 50x8, 0x4, 100x2
# UN_e201_ADAN_ISCMissile 9 100x8, 20x1
# UN_n001_TTRL_Box 9 0x9
# UN_e104_ADAN_Carrier 6 0x3, 100x3
# UN_f003_TCAF_ArrowHead 5 50x5
# UN_e001_ADAN_Elan_GR_Violeta 3 40x3
# UN_e001_ADAN_Elan_GR 3 40x3
# UN_e006_ADAN_Vindicator_MargrasF 3 0x3
# UN_f004_TCAF_DeltaSaber_A_Player 2 0x1, 100x1
# UN_f001_TCAF_DeltaSaber_T_EX5_el 2 0x1, 100x1
# UN_f106_TCAF_Destroyer 2 50x2
# UN_f105_TCAF_Cruiser 2 50x2
# UN_be005_ADAN_SpaceFortress 1 0x1
# UN_f101_TCAF_Acropolis 1 75x1
# UN_f104_TCAF_Battleship 1 75x1
# UN_f102_TCAF_LightCarrier 1 50x1
# UN_e004_ADAN_ElanPlus_NF 1 50x1
# builtin 101: 133 sites, 1 distinct values
# values: - x133
# units: 1 distinct, top: ? x133
# craft cross-tab (133 unresolved):

921
docs/re/disc-atlas.html Normal file
View File

@@ -0,0 +1,921 @@
<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>

View File

@@ -0,0 +1,253 @@
# IDXD field keys that carry no name
- **Confidence:** ✅ the census and the identification of the hash-keyed set ·
✅ the last characters of 30 of the 42 names · ❌ **no name was recovered**
- **Tool:** `tools/re-capture/idxd_unnamed_keys.py` (`census` / `idtbl` / `crack` / `selftest`)
- **Data:** [`docs/re/data/idxd-unnamed-field-keys.txt`](../data/idxd-unnamed-field-keys.txt)
- **Seen in:** every `dat/*.pak` + `hidden/*.pak` on the disc; the interesting 42
are in `dat/GP_READY_ROOM.pak`, file `<lang>\script\ID.tbl`
- **Depends on:** the IDXD record/field table and `tag_hash`, which live on branch
`auto/re-idxd-container`**not on `main`**, so this note transcribes what it
needs and the tool carries its own copy of `tag_hash` rather than importing one.
An IDXD field entry is `(key, name_off, value_off)`. Usually `name_off` points at
the field's own name in the string pool and `key == tag_hash(name)`. When
`name_off` is `0xFFFFFFFF` the pool holds no name and the reader has to know what
`key` means from somewhere else. This note answers: how many such keys are there,
what are they, and can the names be inverted out of the hash?
**Short answer: 7 094 distinct keys, of which 7 052 are not hashes at all and 42
are. None of the 42 names was recovered — but they are now identified, their call
graph is reconstructed, and 30 of them have their last two-to-four characters
pinned algebraically.**
## ✅ The census
Measured by walking all 33 `dat/*.pak` plus `hidden/DefTables.pak` and
`hidden/MiscBin.pak` (`.pNN` segments joined, `Z1` payloads inflated):
| | |
|---|---|
| IDXD objects / records / field entries | 7 750 / 190 782 / **2 757 039** |
| named field entries | 1 271 462 |
| unnamed field entries (`name_off == 0xFFFFFFFF`) | 1 485 577 |
| distinct keys named *somewhere* on disc | 12 174 |
| **distinct keys never named anywhere** | **7 094** |
| … below `0x00010000` | 7 052 |
| … in `[0x10000, 0x01000000)` | **0** |
| … at or above `0x01000000` | **42** |
Parse failures: 0. `tag_hash` reproduces the key of **1 271 462 / 1 271 462**
named field entries disc-wide (`idxd_unnamed_keys.py selftest`), which is the
validation gate for everything below — far past the 200-pair bar this
investigation was asked to clear.
### ✅ "504 hash-keyed fields disc-wide" is 504 **entries**, not 504 keys
`INDEX.md` (on `auto/re-idxd-container`) records *504 fields disc-wide that are
hash-keyed with no name*. That number is exactly reproduced here — and it counts
**field entries**, not distinct keys:
```
42 distinct keys x 6 language copies of the same object x 2 records = 504
```
The three unnamed populations add up:
`504` (hash-keyed) `+ 1 380 324` (ordinal) `+ 104 749` (key `0x00000000`, i.e.
ordinal 0, held apart only because `tag_hash("") == 0` makes it look "named"
somewhere) `= 1 485 577`. So the brute-force target was never 504 names; it was
**42**.
### ✅ The gap at `[0x10000, 0x01000000)` is what separates hashes from ordinals
`tag_hash` puts the byte-sum checksum of the name in the **top byte**. A real
hash therefore falls below `0x01000000` only when the name's byte sum is `0 mod
256` — about 1 name in 256 — *and* its low 24 bits are also tiny. Of the 12 174
keys that do carry a name, 75 have a zero top byte, and the smallest of those is
`0x0002677C`; the never-named ordinal band tops out at `0x2198`. There is a clean
three-order-of-magnitude gap and **not one key in it**. The split is measured,
not assumed.
## ✅ The 7 052 ordinals are not hashes and never were
They are author-assigned element ids written straight into the tag slot. For
1 404 924 of the 1 485 577 unnamed entries the key **equals the field's index
within its record**; a further 42 750 are index + 1. The rest are hand-numbered
with deliberate gaps, e.g. the hangar `WEAPONS` record runs
`…0x30 0x31 … 0x35`, jumps to `0x50 … 0x55`, then `0x61` — an id space, not a
counter. `tables.pak`'s `FILES` record numbers 5 135 resources from `0x3E9`
(1001) upwards.
**Do not feed these to a reverse lookup.** Confirmed by the strongest possible
test: `tag_hash("BGM_001") = 0xC662435B`, while the key of the field whose value
is `BGM_001.slb` is `0x000003E9`. Zero of 5 135 `FILES` keys match a hash of
their own value under any stem/case/extension variant.
Full per-key listing: Part 3 of the data file. Per-schema value profiles: Part 2.
## ✅ What the 42 hash-shaped keys are
All 42 live in **six byte-identical copies** of one object — `eng jpn fra deu ita
esp \script\ID.tbl` inside `dat/GP_READY_ROOM.pak` (resolved through the IPFB TOC
by `name_hash("eng\\script\\ID.tbl") = 0xC40BC120`, and pointed at by the
`SCRIPT_PATH` field of `tables.pak`'s `BASE_INFO`, whose value is
`dat\GP_READY_ROOM.pak+eng\script\`).
The object has exactly two records, `FILE` and `OFFSET`, carrying **the same 42
keys in the same order** — it is a column store:
```
key(tag_hash of an ISL script symbol) -> FILE = the .isb it is defined in
OFFSET = where in that file
```
It is the artefact of the pass the executable announces as
`silph::GamePart_ReadyRoom::Impl::PrepareScript - isl script prescanning start.`
— a link map from symbol hash to definition site. The 34 `.isb` files it names
are all present in the same pak (`eng\script\stage01.isb` etc.).
### ✅ Independent confirmation: the keys appear in the bytecode
41 of the 42 keys occur verbatim as **little-endian 32-bit words** inside the
`.isb` bytecode, and where they occur reconstructs a coherent call graph
(Part 4 of the data file):
| defined in | referenced from |
|---|---|
| `stage01..16.isb`, `challenge01..06.isb`, `tutorial0N01.isb` (30 symbols) | `main.isb` only |
| `function.isb` (5 symbols) | the stage and challenge scripts |
| `function_tutorial.isb` (6 symbols) | the six tutorial scripts |
| `main.isb` (1 symbol) | **nothing** — the root entry point |
That is exactly the shape a launcher + shared-helper-library arrangement should
have, and it is the reason these keys can be called hashes with confidence rather
than "large ordinals": ordinals are not stored as call targets in code.
🟡 `OFFSET` is **base 36**. Every value uses only `[0-9a-z]`, and read as base 36
they are small ascending numbers per file: `function.isb` → 1, 9, 19, 35, 58;
`function_tutorial.isb` → 1, 12, 23, 34, 45, 50; `tutorial0101.isb` → 1, 61, 191.
The first symbol of every file is 1. Whether the unit is a statement index, a
line, or a word offset is ❔ — not settled here.
## ✅ 30 of the 42 names end in the digits of their own filename
This is *forced* by the hash, not guessed. `tag_hash`'s low 24 bits are a
base-256 polynomial mod `0x00FFFFDF`, so bumping the character `k` places from
the end changes the low bits by `256^k mod M` (1, `0x100`, `0x10000`, `0x21`, …)
and the top byte by 1. Measured:
| family | observed key deltas | forced conclusion |
|---|---|---|
| `stage01..09` | `+0x01000001` × 8 | last character increments by 1 |
| `stage10..16` | `+0x01000001` × 6 | ditto |
| `stage09 → stage10` | `0x8FA349EF → 0x87A34AE6` | low bits `+0xF7 = 0x100 9`, top `8`: the last two characters go `"09" → "10"` |
| `challenge01..06` | `+0x01000001` × 5 | last character increments by 1 |
| `tutorial0101 → 0201 … 0601` | `+0x01010000` × 5 | the character **two** from the end increments |
| `tutorial0101`'s two siblings | `+0x01000001`, `+0x02000002` | last character `+1`, `+2` |
So the sixteen stage symbols end `"01" … "16"`, the six challenge symbols end
`"01" … "06"`, and the eight tutorial symbols end `"0101" "0102" "0103" "0201"
"0301" "0401" "0501" "0601"` — matching their `.isb` filenames exactly. The
remaining 12 (`function.isb` × 5, `function_tutorial.isb` × 6, `main.isb` × 1) are
unconstrained.
That is the whole of the recovered name information. **The prefixes are not
recovered.**
## ❌ Cracking: a null result, and the arithmetic that says why
`tag_hash` maps onto `256 × 0x00FFFFDF ≈ 2^32` values, so a search over `S`
candidates yields on average `S / 2^32` **false** preimages *per target*. That is
the number every attempt below is judged against.
| attack | candidates (space `S`) | E[false] per target | hits |
|---|---|---|---|
| every distinct disc string (IDXD names, record names, field values) | 105 393 | 0.000025 | **0** |
| every string in the executable (`.pe`) | 75 912 | 0.000018 | **0** |
| every ASCII run ≥3 in every pak payload except `sound.pak` (incl. `.ssb` symbol tables, XML, XPR) | 1 023 671 | 0.00024 | **0** |
| all three combined, hashed against all 42 | 1 204 976 | 0.00028 | **0** |
| `printf`-style substitution: 49 139 format strings × 80 numeric substitutions | 161 200 | 0.00004 | **0** |
| hand-built guess list (`main`/`Stage`/`Mission`/`ReadyRoom`/… × prefixes × decorations × numbers) | 506 520 | 0.00012 | **0** |
| two-token composition over a vocabulary mined from disc + exe identifiers (`t1 + sep + t2`, sep ∈ `"" _ -`) — this is what `idxd_unnamed_keys.py crack --pe …` runs | 3.48 × 10⁸ | 0.081 | **0** |
| the same, with each family's forced digit suffix appended | 1.18 × 10¹⁰ across the sweep | 2.8 total | 1 (`"Cj-format 01"` — noise, and the same string for all six challenge keys, i.e. **one** false prefix) |
| meet-in-the-middle exhaustive preimage, `[A-Za-z0-9_]`, ≤ 6 chars before the known suffix | 6.3 × 10¹⁰ | ~15 | noise only |
The last row is the important one. Exhaustive search **succeeds** — it is not
that the search is broken. Fed `tag_hash("Stage01")` it returns
`['F4D_kl01', 'Stage01', 'be_zT01', 'cd_Yu01']`: the right answer plus three
collisions, which is what `63^5 / 2^32 ≈ 0.2``63^6 / 2^32 ≈ 15` predicts. Fed
the real target `0x87A349E7` with suffix `"01"` it returns **nothing**, so the
prefix is **longer than 6 characters** over that alphabet.
And that is where it stops being useful. Measured, not extrapolated: at 7
characters target `0xA40C6DF7` has **1 176** preimages (`'1T1E1B6'`,
`'24A7O0G'`, `'4quqkkC'`, …). At 8 the expectation is ≈ 58 000. A hash with a
24-bit modulus simply does not have enough entropy to name an 8+ character
identifier uniquely, so **no amount of compute recovers these names from the hash
alone** — only a corpus containing the actual string can, and the disc does not
contain it.
### 🔴 Refuted along the way (kept, not deleted)
* **"The symbol is the filename stem, in some case/decoration."** Refuted
directly: `tag_hash` of `stage01`, `Stage01`, `STAGE01`, `stage_01`,
`mission01`, and 30-odd relatives is not `0x87A349E7`.
* **"The symbol is `<common prefix> + <filename stem> + <number>`."** Refuted
*algebraically*, without searching. If `name = A + "stage01"` and
`name = A + "challenge01"` share one `A`, then `A`'s byte sum is pinned twice
and the two values must agree. They do not, for lowercase, Capitalised or
UPPER stems, with or without a `_`/`-` separator — 9 pairs tested, 9
contradictions.
* **"The name is somewhere in the `.isb` files."** Refuted: the `.isb` payloads
contain no ASCII symbol names at all. They reference symbols purely by hash —
which is *why* the prescan link map exists.
* **"The hash constants might be in the executable next to a name table."**
Refuted: none of the 42 values occurs in the `.pe` image in either endianness.
* **"IXUD might hold more hash-keyed nameless fields."** Refuted by measurement:
534 IXUD objects, 624 488 field entries, **8** distinct unnamed tags — all of
them ordinals `0…7` (the weapon `CATEGORY_DESC` list). Zero hash-shaped.
* **⚠️ "There are ~504 keys to crack."** Withdrawn — 504 is the *entry* count.
See above.
## Coverage / limits
- The census covers every IDXD object the disc has (7 750). `sound.pak` holds no
IDXD objects and `dat/movie/*.wmv` and `hidden/resource3d/*.xpr` are not
containers; strings from `resource3d` were nevertheless folded into the corpus
attack.
- The 42 targets are fully characterised as *references*; their *spelling* is
unrecovered and, per the arithmetic above, unrecoverable from the disc.
## Evidence log (append-only; newest last)
- `2026-08-26` — Disc-wide walk of 7 750 IDXD objects / 2 757 039 field entries
with `tag_hash` self-check at 1 271 462/1 271 462. Census → confidence
`CONFIRMED` for the counts and for the ordinal/hash split.
- `2026-08-26` — 42 hash-shaped keys localised to six copies of
`<lang>\script\ID.tbl`; cross-checked against the `.isb` bytecode, where 41 of
42 appear as little-endian call targets forming a consistent call graph →
`CONFIRMED` that they are ISL script-symbol hashes.
- `2026-08-26` — Key-delta arithmetic pins the trailing digits of 30 of the 42
names → `CONFIRMED` (forced by the hash's polynomial structure, three
independent families agreeing).
- `2026-08-26` — Seven corpus/composition/brute-force attacks, none producing a
hit above the noise floor; exhaustive search validated on a known name and
shown to return nothing at ≤ 6 characters → the names are **not recovered**,
and the negative result is quantified rather than asserted.
## Open questions / what would raise confidence
- **The 12 unconstrained names** (`function.isb` ×5, `function_tutorial.isb` ×6,
`main.isb` ×1) have no digit structure to exploit at all.
- **What would actually work:** a build artefact containing ISL source or a symbol
list (none on the retail disc); or observing the game *construct* one of these
strings at runtime — `PrepareScript` builds the table from `.isb` contents, but
whatever `main.isb`'s root symbol is called, the name existed only in the
compiler's input. A `--mem-watch` on the ReadyRoom lookup path would show
whether any caller ever passes a literal name rather than a precomputed hash;
that is the one remaining honest route and it was **not** attempted here (this
was a static-only investigation).
- **`OFFSET`'s unit** — base-36 is settled, what it counts is not.
- The `.isb` instruction encoding is only sketched here (a length byte at
`byte[2]`, call payloads leading with the target hash). It is not the `.ssb`
ISL bytecode documented elsewhere and deserves its own note.

View File

@@ -3,6 +3,9 @@
Status: ✅ table encoding, calling convention and the `ScriptPhase` state layout;
✅ ~135 of 147 handlers characterised from the disassembly; 🟡 three resolved
only partially; ❔ the interpreter-command table is only partly recovered.
✅ built-ins **26 / 29 / 101** are now named end to end (HP %, damage-taken %,
broadcast invulnerability); 🟡 **28** writes the matching damage-**dealt** field
but its one consumer is a single weapon path — see below.
Companion to [isl-bytecode](isl-bytecode.md) (the instruction encoding) and
[mission-phase-advance](../mission-phase-advance.md) (why phases hinge on these).
@@ -168,56 +171,269 @@ words**: a descriptor offset and the message id.
That fixes the id format as `0xED08 nn DE`, and the ids already known from other
work fit it: opcode 514 → `00DE`, 803 → `07DE`, 999 → `0FDE`.
🟡 The pump's arm for `0xED0802DE` does **not** apply an effect — it walks the
**The pump's arm for `0xED0802DE` applies no effect itself** — it walks the
unit's child list at `[unit+320]`/`[unit+324]` and **rebroadcasts** to each child
as `0xED0902DE`. So `0xED08…` is the to-unit family and `0xED09…` the to-child
one, and the terminal effect is one link further on. ❌ Not followed; 26/28/29
remain unnamed.
as `0xED0902DE`. So `0xED08…` is the to-group family and `0xED09…` the
to-individual one. That link is now followed all the way to the effect — see
below.
### 🟡 Built-ins 26 / 28 / 29 are one family — and `damage_unit` looks mis-named
### Built-ins 26 / 28 / 29 RESOLVED — HP, damage dealt, damage taken
Method-diffing put the structure beyond doubt but did not reach the semantics.
The chain was followed end to end for all three, from the dispatch table to the
field write. Nothing here is inferred from shape; every hop was read.
| built-in | vtable slot | method | opcode | sites |
|---|---|---|---|---|
| 26 `damage_unit` | 76 | `sub_8226ACD0` (67) | **800** | 97 |
| **28** | 84 | `sub_82268F98` (69) | **801** | 410 |
| **29** | 88 | `sub_822690B0` (69) | **802** | 164 |
| 101 | 276 | `sub_822691C8` (78) | 802 (broadcast) | 133 |
| built-in | vtable slot | method | opcode | cmd word | interp thunk | opcode handler | to-group msg | to-child msg | arm | writes |
|---|---|---|---|---|---|---|---|---|---|---|
| **26** | 76 | `sub_8226ACD0` | 800 | `0xAB0320BA` | `0x822FF118` | `sub_823008C8` | `0xED0802DE` | `0xED0902DE` | `0x82398E8C` | `[unit+532]` = **HP** |
| **28** | 84 | `sub_82268F98` | 801 | `0xAB0321BA` | `0x822FF120` | `sub_823009B8` | `0xED0803DE` | `0xED0903DE` | `0x8239926C` | `[unit+672]` |
| **29** | 88 | `sub_822690B0` | 802 | `0xAB0322BA` | `0x822FF128` | `sub_82300AA8` | `0xED0804DE` | `0xED0904DE` | `0x823994C0` | `[unit+676]` |
**28 and 29 differ in two words only** — the opcode (`0x21BA` vs `0x22BA`) and a
descriptor pointer 8 bytes apart. Otherwise instruction-identical. All three take
`(unit, double)`.
The cmd word decodes as `0xAB` `opcode:16` `0xBA`, which is the same encoding
already recorded for 256 (`0xAB0100BA`), 513 (`0xAB0201BA`) and 996
(`0xAB03E4BA`) — `0x320/0x321/0x322` = 800/801/802.
26 differs from both by one guard: it rejects only state 3, while **28 and 29
reject states 1 and 3** (2 = active, 1/3/4 = gone/dead/invalid).
Two links that the previous attempt did not have:
#### The operand distributions separate them
* the **to-group pump is `sub_8232C4C0`** (vtable `0x820AF1FC` slot 6), whose
arms at `0x8232C824` / `0x8232CB48` / `0x8232CC40` each walk the child list and
re-post with the `0xED09…` id, copying the float from `[msg+36]` unchanged;
* the **to-individual handler is `sub_82398CC0`** (vtable `0x820B192C` slot 6),
the entity base class. Its `0xED0902DE` arm is reached by a `sub.`/`cmplwi 0x8200`
pair rather than a `lis`+`ori` compare, which is why a constant scan for
`0xED0902DE` finds only the *sender*. That is worth remembering: **a
subtract-based compare hides the constant from any scan that pairs
`lis` with `ori`.**
| built-in | n | distinct | range | most common |
|---|---|---|---|---|
| 26 | 97 | 7 | **[0, 100]** | **0 ×69**, 80 ×10, 100 ×6 |
| 28 | 410 | 13 | **[0, 2000]** | 200 ×116, 120 ×76, 300 ×74 |
| 29 | 164 | 6 | **[0, 100]** | **0 ×64**, 100 ×53, 50 ×38 |
#### ✅ 26 is `set_unit_hp_pct` — NOT `damage_unit`
26 and 29 are percentage-shaped; 28 is an absolute quantity an order of magnitude
larger.
`sub_8226ACD0` multiplies the double operand by the constant at `0x820C5688`,
which is **0.01** — so the operand is a percentage and the message carries a
fraction. `0x82398E8C` then does, in its own terms:
#### 🟡 `damage_unit` (26) is doubtful
```
new = min( max(def.HP * frac, 0.0), def.HP ) ; def = [unit+496]
[unit+532] = new
if new < FLT_EPSILON and old > 0: ; crossed to zero
[unit+688] = 0.0 ; [unit+1340] = 1 ; [unit+692] = def.Delay
```
**69 of its 97 calls pass 0.** Dealing zero damage is a no-op, so 71 % of the
call sites would do nothing. *Setting* a percentage-valued property to 0 is a
perfectly natural thing to do 69 times, and 29 has the same shape (0 ×64 of 164).
The existing name predates this session and is not withdrawn, but it should not
be relied on.
Three independent facts pin the field:
#### ❌ Where this stopped
* `[unit+496]` is the **unit definition** — the entity constructor
`sub_82393868` sets it from `sub_82348830` (the per-member definition
`std::map::find` already identified for built-in 15) at `0x82393BE4`;
* `[def+84]` is **`HP`** in
`crates/sylpheed-formats/data/unit_definition_layout.txt`, and the same
constructor immediately does `[unit+532] = [def+84]` at `0x82393BF0` — so
`+532` *is* current HP, seeded from the datasheet maximum;
* the crossing branch loads **`[def+584]` = `Delay`** into `[unit+692]` and sets
a flag — the destruction sequence. So a percentage of **0 destroys the unit**,
with the definition's own death delay.
The three commands' descriptors sit at `0x820A8D10` / `+8` / `+16`. Following
them lands on data pointing into `0x8210E5xx`, which is **below the disassembly
DB's range** (it starts at `0x82150000`) and contains no code — so that route
does not reach an execute method. Reaching opcodes 800802's semantics needs the
interpreter's command table, not the command objects.
So the operand is *percent of maximum HP*, the write is absolute (a **set**, not
a subtract), and `damage_unit` is withdrawn: 100 heals to full, which no
"damage" primitive does.
#### ✅ 29 is `set_unit_damage_taken_pct` — `[unit+676]` scales incoming damage
`[unit+672]` and `[unit+676]` are **both initialised to 1.0** by the entity
constructor (`0x8239395C`/`0x82393964`, from the float at `0x8208583C`), so both
are multipliers whose neutral value is 1.0 and which built-ins 28/29 set to
`operand/100`.
`[unit+676]` has **three independent readers**, and every one of them multiplies
a damage amount immediately before it is subtracted from `[unit+532]`:
| reader | what it computes |
|---|---|
| `0x8237B20C` in `sub_8237B020` (craft subclass `0x820B0A3C` slot 6, the `0xED0200DE` damage arm) | `dmg = [msg+36] × def.ResistanceToPlayer × [unit+676]` |
| `0x823803A4` in `sub_823800A8` (another subclass's damage arm) | the same product |
| `0x82399FFC` in `sub_82398CC0` (the base class's continuous-damage arm) | `dmg = def.HP × globalRate × t × [unit+676]`, then `[unit+532] -= dmg` |
`def+128` is `ResistanceToPlayer` in the layout file, which is the second
corroboration that the object is the entity and the quantity is damage.
#### 🟡 28 sets `[unit+672]`, and its one consumer scales damage DEALT
The write is certain and the field's default (1.0) is certain. The **label** is
one link weaker than 29's, and this is the honest state:
`[unit+672]` is read in **exactly one place** in the image —
`0x8237A44C`, inside `sub_823785A0`, which is vtable `0x820B0A3C` **slot 1**,
i.e. the craft subclass's per-frame update. There it is copied into a 240-byte
projectile spawn record at `+100`; `sub_8238F4D0` turns that record into a
projectile with `[proj+300] = [rec+100]`; and `sub_8238FE10` builds the
`0xED0200DE` damage message with **`[msg+36] = [proj+300] × charge × globalScale`**
— the same `[msg+36]` that the readers above multiply by the *target's* `+676`.
So the two fields are the two ends of one damage product: **`+672` on the
shooter, `+676` on the target.** That is a tidy story and it is exactly the
shape that has produced wrong names in this file before, so it is 🟡 and not ✅.
**What is not established**: that `+672` reaches *every* weapon. The one read
site is on a charged-shot path (`[unit+2244]` is a charge accumulator compared
against thresholds, `[unit+2268]` the resulting power), and the other damage
sender in the same family, `sub_82388FF8`, computes
`dmg = ammo.base × [proj+268] × k` with **no `+672` term at all**. Either the
craft update funnels all firing through this one spawn, or `+672` scales only
some weapons. Not settled.
#### ✅ The operand-range test discriminates the three
All three built-ins have the identical signature `(unit, double)` and the
identical `× 0.01` conversion, so an operand ceiling is a real measurement rather
than a coincidence:
| built-in | n | distinct | min | max | inside [0, 100] |
|---|---|---|---|---|---|
| 26 | 97 | 7 | 0 | **100** | **97 / 97** |
| 29 | 164 | 6 | 0 | **100** | **164 / 164** |
| 28 | 410 | 13 | 0.1 | **2000** | 42 / 410 |
26 and 29 respect a hard 100 ceiling — they are percentages of something with a
natural maximum (HP; and "no more damage than normal"). 28 does not: it is a
free multiplier that reaches 20×. 🟡 Note that 29 therefore **only ever makes a
unit tougher**, never more fragile.
#### ✅ The craft cross-tab — the same test that settled built-in 15
Joining every call site to its squadron's craft through `UnitGroup_S<NN>.tbl`
resolves **671 of 671** sites (26 + 28 + 29), none unknown. Full dump in
[`data/isl-builtins-26-28-29-sites.txt`](../data/isl-builtins-26-28-29-sites.txt).
**26** splits into three groups with nothing in between:
| group | craft (sites) | values |
|---|---|---|
| disposable objects | `e201_ISCMissile` 17, `n001_TTRL_Box` 16, `f202_Cargo` 9, `e015_Puppy(_2)` 10, `mn500_FloatingMine` 3, `e011_Attacker_B` 2 | **0, and only 0** |
| warships | `e106_Destroyer` 8, `f106_Destroyer` 8, `f105_Cruiser` 5, `e108_ASFrigate` 4, `e105_Cruiser` 2, `f101_Acropolis` 1 | 30 / 50 / 60 / 80 |
| the player's craft in a tutorial | `..._Player_Ttrl1/2`, `..._T_Ttrl` (8) | **100** ×7, 40 ×1 |
Scripted removal of props, pre-damaged capital-ship spawns, and healing the
player to full at the start of a tutorial section — one field, three uses, and
no other reading covers all three.
**29** is dominated by the ships the mission must protect:
| craft (sites) | values |
|---|---|
| player + wingman `DeltaSaber` variants (`_T`, `_W`, `_A`, `_Player`) 113 | **0** and **100**, alternating |
| `n001_TTRL_Box` 9 | **0** only |
| escorted TCAF hulls — `f202_Cargo` 14, `f106_Destroyer` 2, `f105_Cruiser` 2, `f104_Battleship` 1, `f102_LightCarrier` 1, `f101_Acropolis` 1 | 50 / 75 |
| named ADAN aces — `e001_Elan_GR`, `_GR_Violeta` 6; `e010_Attacker_S` 18 | 40 / 50 |
**28** stratifies by combat class and is applied almost only to ADAN craft:
| class | craft (sites) | value |
|---|---|---|
| named bosses | `e003_ElanPlus_Margras` 4, `e005_ElanTypeQ_Margras` 1 | **2000** |
| named aces | `e001_Elan_GR(_Violeta)` 6, `e013_ElanPlus_Taskent` 2 | **1000** |
| elite fighters | `e004_ElanPlus_N` 19, `e009_Phantom` 16, `e002_Elan_N` 9, `e011_Attacker_B_HF` 3 | **500600** |
| line fighters | `e001_Elan` 24, `e010_Attacker_S` 21, `e011_Attacker_B` 6 | 200300 |
| capital hulls | `e106_Destroyer` 105, `e104_Carrier` 55, `e105_CruiserEX` 52, `e102_BattleshipEX` 26 | 100300 |
| background props | `f106_Destroyer_Inv`, `f102_LightCarrier_Inv`, `f101_Acropolis`, `f105_Cruiser` (5) | **0.1** |
A monotone boss > ace > elite > line > prop ordering is what a firepower knob
looks like. 🟡 It is also what several other knobs would look like, which is why
the name stays PROBABLE and rests on the disassembly link above rather than on
this table.
#### ✅ The idiom that ties all three together
The per-unit setup block that follows every deployment reads, e.g. Stage 06
phase 3 at `0x0119AC` (from
[`data/isl-builtins-26-28-29-sites.txt`](../data/isl-builtins-26-28-29-sites.txt)):
```
activate_unit(TCN001) set_group_speed(TCN001, 400) 29(TCN001, 0)
activate_unit(TCN002) set_group_speed(TCN002, 400) 29(TCN002, 0) 28(TCN002, 50)
activate_unit(TCN003) set_group_speed(TCN003, 400)
... 26(TCN306, 50)
```
`15 / 29 / 28` is a **speed / toughness / firepower trio**, each a percentage
override of the craft's datasheet, applied immediately after `activate_unit`;
`26` joins it wherever a unit should arrive pre-damaged.
And the scripted-drama use is unambiguous. Stage 06 makes the wingman flight
TCN002 invulnerable at phase start (`29(TCN002, 0)`), then walks its HP down as
the voice lines fire:
```
0x01A7B0 26(TCN002, 80) -> request_script_message(MSG_VOICE_D_065)
0x01AAA0 26(TCN002, 50) -> request_script_message(MSG_VOICE_D_069)
0x01ACEC 26(TCN002, 30)
```
You would only combine those two built-ins that way if they meant exactly "this
unit cannot be hurt by combat" and "set this unit's HP to N %".
#### ✅ Refutation attempts, and what they found
Recorded because two of them turned into confirmations.
* **"26 kills a unit the mission still needs."** Over all 28 stages there are
200 (stage, unit) pairs where a predicate on `u` follows a `26(u, 0)` in file
order. Every one inspected is a `unit_alive` / `unit_state` / `hp_pct_test`
**poll waiting for that death** — which is what a scripted kill implies, not a
contradiction. Not a counterexample, but the count is recorded rather than
hidden.
* **"29 makes a unit invulnerable that the player is required to destroy."**
The nine `n001_TTRL_Box` targets in Stage 28 do get `29(box, 0)`. They are
then destroyed by **`26(box, 0)`** later in the same file — nine boxes, nine
pairs, invulnerability first (offsets 0x2E4C0x48C8) and the scripted removal
second (0x50F80x5FA0). The tutorial target cannot be shot down; the script
removes it when the lesson ends. The counterexample became a confirmation.
* **"28 is applied to something with no weapons."** Of the 35 craft classes it
touches, none is an unarmed prop — no asteroid, box, cargo or mine ever
receives 28, while 26 and 29 both do. The only near-miss is the four
`…_Inv` / background hulls at 0.1, i.e. deliberately harmless.
* **"`+672`/`+676` have another consumer that contradicts damage."** Searched
every `lfs`/`stfs`/`addi`/`lfsx` reference to offsets 672 and 676 across the
whole `.text`, then filtered by base register. **No further entity-side reader
of either exists.** ⚠️ The search also produced the trap this file keeps
warning about: `sub_8238E0F0` writes `676(r3)` and `sub_82389558` reads
`672(r31)`, but in both `r3`/`r31` is a **projectile**, which has its own
fields at those offsets. Three of the five "extra readers" the raw offset
search returned were that mistake.
* **What I could not refute and could not confirm**: that `+672` reaches every
weapon (see the ❔ above).
#### ✅ Bonus: built-in 101 is `all_units_invulnerable`
`sub_822691C8` is 29's **broadcast** twin — same descriptor `0x820A8D20`, same
opcode 802 — but it takes **no operand**: the float it sends is the constant at
`0x8209FD28`, which is **0.0**, and it loops over the phase's whole unit array
posting `0xED0804DE` to each live entry.
The usage confirms it. All **133** sites sit in one fixed phase-teardown idiom,
with no exceptions:
```
builtin116(0) -> builtin101 -> reset_phase_threads -> timer_stop -> clear_flag(-1) -> builtin118
```
133/133 preceded by 116 and 133/133 followed by 100. Freezing all damage is
exactly the first step of tearing a phase down, and no other reading of "post
0.0 to every unit" fits a teardown.
#### ❌ Two corrections to this file's earlier entries
* **The state guards were mis-stated.** Read directly: **26 rejects states 3 and
4**; **28 and 29 reject 1, 3 and 4**. This file said 26 rejects "only state 3"
and 28/29 "states 1 and 3". So 26 alone will still act on a state-1 unit.
* **"28 is an absolute quantity, 26 and 29 are percentage-shaped"** was half
right for the wrong reason. All three are percentages of *something*; only 26
and 29 are percentages of a thing with a ceiling.
#### 🗒️ The route that failed, kept
The three commands' descriptors at `0x820A8D10` / `+8` / `+16` are **vtables**,
not data: slot 0 of each is `0x82301C20` and slot 1 points into `0x8210E5xx`,
below the disassembly DB's range. Chasing them still leads nowhere. The route
that works is the one this file already recommended — the **interpreter command
table** → `0x822FF118/120/128` → the three opcode handlers — and from there the
**unit-message pump**, which is the part that was missing.
### ✅ Built-in 108 is `deploy_squadron_ex` — `deploy_squadron` plus a `1 << n` selector

View File

@@ -1,8 +1,16 @@
# `REGN` — a per-map spatial grid (and `MCOL` beside it)
# `REGN` — a stage's tetrahedral navigation mesh (and `MCOL` beside it)
**Status: ✅ `CONFIRMED` for the header**, which self-checks on all 11 objects on
the disc. ❔ the four data sections are undecoded. **New to this corpus** — no
document mentioned `REGN`, `MCOL` or `hidden/MiscBin.pak` before 2026-08-24.
**Status: ✅ `CONFIRMED`.** A `REGN` object is a **tetrahedral navigation mesh
of the whole play volume** — vertices, tetrahedra, faces with full adjacency,
per-tetrahedron portal costs — plus a uniform grid for point location. The
decode is complete except for two small fields; see
[the 2026-08-26 section](#-2026-08-26--the-reader-found-regn-is-a-tetrahedral-navigation-mesh),
which supersedes the offsets in the older sections below (they were read
**16 bytes early** — the loader's fixup base is `chunk + 0x10`). The older
sections are kept because their refutations are still instructive.
**New to this corpus** — no document mentioned `REGN`, `MCOL` or
`hidden/MiscBin.pak` before 2026-08-24.
## Where it is
@@ -131,23 +139,6 @@ thing is indexed *by*.
partition and no more; the sections are unread. Treat this as the location of the
world's spatial data, not as the wave table.
> ❌ **SETTLED 2026-08-26 — `REGN` is not the wave scheduler, and cannot be.**
> It is a **tetrahedral navigation mesh**: vertices, faces with plane equations
> and adjacency, tetrahedra with portal costs between their face pairs, and a
> grid that indexes which tetrahedra fall in each cell. Every section is
> accounted for by that structure. There is nowhere in it for "what spawns, when,
> on what trigger" to live — no time field, no unit reference, no trigger.
>
> The hedge above was right to keep the reading provisional, but the reasoning
> it hedged ("a grid is what a scheduler would be indexed by") was a **guess from
> shape**, and the shape belonged to pathfinding. Worth noting because that guess
> is what pointed this whole investigation at `REGN` in the first place.
>
> The wave-scheduler search should now treat `REGN` as **excluded**, not as
> unread. The arrival timetable already found in `Route_S<NN>.tbl` — keyframed
> `(time, quat, pos)` per squadron per phase, with `t` in seconds — remains the
> only located part of that mechanism.
## ✅ Update: `REGN` is a stage's **MapPath**
The per-stage definition record (see
@@ -391,119 +382,338 @@ object in the executable and watch which fields it dereferences. That is static
PE work (`/work/*.pe`, offset = VA 0x82000000) of the same kind that cracked
the `.slb` packing phase, and it is the honest next step rather than a
twenty-first correlation.
---
## ⚠️ Open conflict with `auto/regn-reader` over the `+0x10` base
# ✅ 2026-08-26 — the reader found: `REGN` is a **tetrahedral navigation mesh**
**2026-08-26.** Branch `auto/regn-reader` decodes `REGN` as a **tetrahedral
navigation mesh** and reports the cell→geometry link solved, with strong checks
(faces passing through 3 of 4 tet vertices, 253 722/253 722; portal cost equal to
the distance between face centroids, 380 460/380 460). Its load-bearing structural
claim is that the `POF0` fixup base is **`chunk + 0x10`**, and therefore that
"every offset previously recorded on that page was read 16 bytes early" — offered
as the reason the ~20 correlation tests on this page returned chance.
The previous section closed with "find what reads a `REGN` object in the
executable and watch which fields it dereferences". That worked, and it did not
need the consumer: the **deserialiser** answers the question, because the file
carries its own pointer map and the deserialiser tells you how to read it.
**I could not reproduce that as stated, on the one thing here that is
independently checkable.** Re-reading the plane list at both bases:
## ✅ The reader — `sub_82465110` / `sub_82465138` / `sub_82465200`
| | records | unit normals |
|---|---|---|
| section at `chunk + offset` (what this page used) | 133 573 | **133 573 (100.00 %)** |
| section at `chunk + 16 + offset` | 133 573 | **0 (0.00 %)** |
Three functions, all in the resource module around `0x82460000`:
And the plane identity `n·p + d = 0` holds to float round-off at the **unshifted**
base. A 16-byte shift destroys it completely. So for *this* record the unshifted
reading is right, and the blanket statement is not.
```
sub_82465110 find_pof0(chunk)
82465110 lwz r11, 4(r3) ; datasize
82465118 add r11, r11, r3
82465120 addi r3, r11, 16 ; -> chunk + 16 + datasize
82465124 lwz r11, 0(r3)
82465114 lis r10, 0x504F / 8246511c ori r10, r10, 0x4630 ; 'POF0'
82465128 cmplw cr6, r11, r10
8246512c beqlr cr6 ; else return 0
```
🟡 **The likely reconciliation is bookkeeping, not disagreement.** The other
branch describes a 48-byte **face** record whose plane fields sit at a different
intra-record offset; `chunk+16` with the plane at `+0` addresses the same bytes as
`chunk+0` with the plane at `+16`, which is exactly what this page uses. If so
both readings are correct and only the origin convention differs — but that is a
guess, and I am not adopting either page's wording until it is checked.
```
sub_82465138 relocate_chunk_chain(chunk)
82465164 lbz r11, 8(r31) / clrlwi r11,r11,31 ; already-relocated bit
82465178 bl 0x82465110 ; find the POF0 chunk
82465194 addi r4, r11, 16 ; POF0 payload
82465198 lwz r5, 4(r11) ; POF0 payload size
8246519c addi r3, r31, 16 ; ← THE FIXUP BASE = chunk + 0x10
824651a0 bl 0x82465200
824651a8 ori r11, r11, 0x1 / stb r11, 8(r31) ; set the bit
```
**Also confirmed here, incidentally**: my section-0 point test passes at **100 %
at both bases**, so it never had the power to distinguish them. That test could
not have caught a 16-byte error and should not be cited as if it validated the
offsets.
```
sub_82465200 apply_pof0(base=r3, table=r4, size=r5)
82465224 clrrwi r8, r10, 6 ; top two bits of the lead byte select
82465228 cmplwi cr6, r8, 0x40 ; 0x40 → 6-bit delta, 1 byte
82465230 cmplwi cr6, r8, 0x80 ; 0x80 → 14-bit delta, 2 bytes
82465238 cmplwi cr6, r8, 0xC0 ; 0xC0 → 22-bit delta, 3 bytes
82465258 add r11, r10, r11 ; running WORD index, never reset
8246525c slwi r8, r11, 2
82465260 lwzx r10, r8, r3 ; slot = base[word]
82465264 cmplwi cr6, r10, 0x0
8246526c add r10, r10, r3 ; *slot += base (skipped when *slot == 0)
82465270 stwx r10, r8, r3
```
❔ Until the conflict is resolved, treat the plane/face layout on this page as
verified by its own arithmetic and the tetrahedral decode on `auto/regn-reader`
as verified by its own — and do not merge the two offset conventions without
re-running one check from each.
Two things follow, and both are load-bearing:
## ✅ RESOLVED — the base IS `chunk + 0x10`, and my section-0 offsets were wrong
1. **The fixup base is `chunk + 0x10`, not `chunk + 0`** (`82465198`:
`addi r3, r31, 16`). Every stored "offset" in a `REGN` file is relative to
file offset `0x10`, and every **record origin** on this page above is
therefore 16 bytes early.
2. **The `POF0` table is an exact list of which words are pointers.** It is not
a heuristic — it is the data the retail loader itself walks. Decoding it
gives the pointer graph directly, with no guessing.
**2026-08-26, same day.** The conflict above is settled, and it settles against
me on the boundary while leaving the plane arithmetic intact.
### 🔴 A correction to how I first stated point 1
**The plane fields were never in dispute.** For object `3506e972`, the other
branch's face record 0 begins at `0x1c700`, and its plane normal begins at the
same address; my `chunk + offset_at_0x78 + 16` gives `0x1c700` too. **Same bytes,
different bookkeeping** — exactly the reconciliation guessed above. The
`n·p + d = 0` result stands unchanged.
The first version of this section said "everything on this page above was read
16 bytes early", which is **too broad and I withdraw it**. For the plane fields
it is *false*: the older reading put the section at `chunk + offset` and the
normal at intra-record `+16`, which addresses **exactly the same bytes** as
`chunk + 0x10 + offset` with the normal at `+0`. Its unit-normal and
`n·p + d = 0` results were measuring real planes and stand unchanged. On
`3506e972`, both conventions put face record 0's normal at file offset
**`0x1c700` (116 480)** = `0x10 + 0x1c6f0`, where the header word at `0x78` is
`0x1c6f0`.
**The base is `chunk + 0x10`**, on evidence that has power where my tests did
not:
What is genuinely wrong is the **record boundary**, and it matters for one
reason: it decides which 48-byte record the four integer words belong to — and
those integer words are the face adjacency, i.e. precisely the thing the
correlation search was hunting for. It also mis-resolves every stored pointer
by 16 bytes, twice over along the cell → item → ref → tetrahedron chain, which
is the mechanical reason that chain never landed on anything.
* the loader does it: `8246519c addi r3, r31, 16`;
* at `+0x10` the six `POF0`-relocated slots land exactly on `0x70``0x84`, the six
section pointers. At `+0` they would land on `0x60``0x74` — relocating the
`u16` **counts** and leaving two section pointers unrelocated. Not merely
wrong, non-functional;
* section-0 record 0 reads as a bbox corner at `+0x10` and as garbage at `+0`.
### ✅ Which tests actually decide the origin — and which have no power
### ❌ And my point-list evidence was vacuous
Most of the numbers on this page are **content** tests and address the same
bytes under either convention, so they cannot settle the origin. Stated
plainly, because the same trap caught the point-in-bbox test:
I reported "13 467/13 467 section-0 points lie inside the bbox" as the check that
made section 0 a decode. Read at the correct base, record 0 of four objects is:
base +0 (1.4e-41, 5.3e-41, 0) (2.2e-40, 2.7e-40, 0)
base +16 (-50000,-50000,-50000) (-250000, 250000, -250000)
So my offsets were 16 bytes early throughout. **Only 11 of 13 467 points read as
denormal at the wrong base** — the rest were still plausible coordinates inside
the box, because a 16-byte shift within a packed array of `f32` triples yields
*other floats from the same array*. That is the general form of the failure and
it is worth stating plainly:
> **A containment test cannot detect a shift inside a homogeneous array.** The
> shifted values are drawn from the same distribution as the correct ones, so the
> test passes at 100 % either way. It is not a weak check — for this class of
> error it is no check at all.
The conclusion "section 0 is a point list" happens to be right. The evidence I
gave for it was not evidence.
**Adopt `chunk + 0x10`.** The plane *fields* need no change; the record
*boundary* moves, and the four integer words I described as "four zeros" move
with it into the record they describe — which is where the face adjacency, and
the answer to the cell→geometry question, was hiding. See branch
`auto/regn-reader` for the tetrahedral decode.
## ✅ Independent confirmation of the face decode
**2026-08-26.** I re-derived the other branch's central check with my own code,
my own reading of the record, and my own control — rather than accepting the
number.
Reading at base `chunk + 0x10`, taking the plane at intra-record `+0` and the
**three `u16` vertex indices at `+32`**, and asking whether each named vertex
satisfies its own face's plane equation:
| | |
| test | discriminates the base? |
|---|---|
| faces where **all three** named vertices lie on the plane | **133 573 / 133 573 (100.00 %)** |
| a **random** vertex lying on that plane (control) | 2 782 / 400 719 = **0.69 %** |
| section-5 targets on 96-byte boundaries; ref-array packing | ❌ no — `target start` cancels the base |
| face passes through 3 of 4 vertices; portal-cost identity | ❌ no — same absolute bytes either way |
| sphere reaches its cell (98.7 % vs 1828 %) | ❌ no for the base; it discriminates the **axis order** |
| points inside the bbox (the older section) | ❌ no — a denormal reads as ≈ 0, which is inside a ±250 km box |
| **the loader's own `addi r3, r31, 16`** | ✅ direct |
| **where `POF0` says the header pointers are** | ✅ at `+0x10` the six relocated slots are exactly `0x70``0x84`, the six section pointers. At `+0` they would be `0x60``0x74`: the loader would relocate the `u16` **counts** and would leave the pointers at `0x78``0x84` unrelocated. Not merely wrong — non-functional. |
| **what section-0 record 0 contains** | ✅ at `+0x10` a real vertex (`-250000, 250000, -250000` on `d4c89536`); at `+0` the two remaining header pointer words read as floats — `2.2e-40, 2.7e-40, 0` |
| **the section-2 record boundary** | ✅ see below, 100.000 % against 0.000 % |
A 100 % against a 0.69 % baseline is not a fit — the face record genuinely names
the vertices of its own plane, and the `+0x10` base together with this layout is
right.
### ✅ The record boundary, settled
That also settles the record-boundary question from my side: the `u16`s that
describe a plane sit **after** it in the same 48-byte record. My earlier reading
of "four zeros at the start of each record" was those integers seen 16 bytes out
of position, one record late.
A tetrahedron names face `f`; the plane at record `f` is independently verified
(three of the tet's four vertices lie on it, 100 %). The question is whether the
adjacency block that says *"I am face `f`, my tetrahedra are A and B, at their
face slots i and j"* sits in the **same** 48-byte record as that plane.
This is worth having in the corpus independently of the other branch: two
different implementations, two different guesses at the intra-record layout, the
same 100 %.
Requiring `own == f` **and** `tet[A].faces[i] == f`:
adjacency block taken from record f (base + 0x10) : 100.000 %
adjacency block taken from record f + 1 (base + 0) : 0.000 %
11 of 11 objects, every face, both sides. The plane and the four integer words
that describe it are one record, and that record starts at `chunk + 0x10 +
offset + 48·f`.
`sub_82465138` is reached from exactly two callers, `sub_82461018` (vtable slot
9 of the class at `0x820af8bc`, the pak/resource file class) and
`sub_82461DE8`; both store `chunk + 16` as the object's data pointer, which
confirms the same `+0x10` base from the other side.
### 🔴 …and the magic is never compared
Worth recording because it is what sent the search to the fixup table:
**`'REGN'` and `'MCOL'` are not constructed anywhere in the executable.** The
title builds its four-character tags as `lis`/`ori` pairs, and a sweep of every
such pair recovers 156 tags — `RATC`, `T8aD`, `XBG7`, `IPFB`, `IDXD`, `LSTA`,
`POF0`, `PRMD`, `TBMD`, `WMV3` … — but neither of these. Nor does either half
appear as an immediate anywhere: `0x474E` (`'GN'`) occurs **zero** times in
1 865 751 instructions, and the flat PE image contains the byte string `REGN`
**zero** times. So no magic-dispatch site exists to find; `.rgn` objects are
handed to the map code by the stage record, not identified by their tag.
## ✅ The header is 0x70 bytes at `chunk + 0x10`, with **six** sections
Corrected, and re-derived from the `POF0` table, which relocates exactly six
header words (`0x70`, `0x74`, `0x78`, `0x7c`, `0x80`, `0x84`) on 11 of 11:
```
chunk +0x00 char[4] 'REGN'
+0x04 u32 data size (POF0 chunk at +0x04-value + 0x10)
+0x08 u8 flags; bit0 = "already relocated" (set by sub_82465138)
data = chunk + 0x10:
+0x00 f32[4] bbox min (w = 1.0)
+0x10 f32[4] bbox max
+0x20 f32[4] extent
+0x30 f32[4] cell size
+0x40 u32[4] grid dims
+0x50 u16[6] record counts, one per section
+0x60 ptr[6] section pointers
```
The old page had four sections because it read the last two pointers as data.
There are six, their strides are `12, 96, 48, 8, 32, 4`, and each section's
span divided by its stride is its `counts[]` entry exactly — 11 of 11, with the
only slack being 16-byte alignment padding and, for the face list, **two
all-zero sentinel records** (which is the unexplained "constant 96-byte tail"
from the section above: 2 × 48).
| # | contents | stride | count |
|---|---|---|---|
| 0 | vertices | 12 | `counts[0]` |
| 1 | **tetrahedra** | 96 | `counts[1]` |
| 2 | **faces** (plane + adjacency) | 48 | `counts[2]` |
| 3 | cell index, one per cell | 8 | `counts[3]` = cells |
| 4 | cell items, one per **occupied** cell | 32 | `counts[4]` |
| 5 | tetrahedron references | 4 | `counts[5]` |
## ✅ How a cell reaches its geometry — the question, answered
The `POF0` table places every pointer in the file, and there are only four
kinds. Verified on all 11 objects by `tools/re-capture/regn_decode.py verify`:
* the six header words, and nothing else in the header;
* **one pointer per occupied cell**, at section-3 record offset **`+4`**
— so a cell is `{ u32 count; item* }` and the *count* word is not a pointer;
* **exactly one pointer per section-4 record, at offset `+0x14`** — the 32-byte
cell item is `{ …, u32 n @+0x10, tetref* @+0x14, … }`;
* **every word of section 5**, all `counts[5]` of them.
So the chain is:
```
position ─▶ cell (x,y,z) index = (z·dimY + y)·dimX + x
─▶ sec3[index] {count, item*}
─▶ item {…, n @+0x10, refs* @+0x14, …}
─▶ refs[0 .. n) each a pointer into section 1
─▶ tetrahedron
```
Four independent checks, all 11 of 11 objects:
* **section-5 targets land on section-1 record boundaries** — every one of
**261 000** pointers is `sec1 + 96·k` with zero remainder;
* **the reference arrays are packed contiguously in cell order** —
`item[i].refs == sec5 + 4·Σ item[j<i].n`, exact, and the total equals
`counts[5]`;
* `counts[4]` equals the number of relocated section-3 pointers equals the
number of occupied cells;
* **the cell ordering is x-fastest, then y, then z.** A tetrahedron's bounding
sphere reaches the box of a cell that lists it in **98.7 100 %** of all
261 000 references; transposing the axes — the natural control — drops that
to **18 28 %**.
The list is a strict subset of "every cell the bounding sphere touches"
(Jaccard ≈ 0.4 1.0), i.e. the build used a tighter test than sphere-vs-box.
That is expected and is not claimed as decoded.
## ✅ Section 1 is a **tetrahedron** — 96 bytes
```
+0x00 f32[3] bounding-sphere centre
+0x0c f32 bounding-sphere radius
+0x10 u16[4] vertex indices → section 0
+0x18 u16[4] face indices → section 2
+0x20 6 × { f32 cost; f32 ❔ } one pair per face pair, in the order
(0,1) (0,2) (0,3) (1,2) (1,3) (2,3)
+0x50 u32 the record's own index
+0x54 u8[4] flags, values 0/1
```
**The check that makes this a decode, not a reading**: each of a tetrahedron's
four faces must pass through exactly three of its four vertices. Over
**253 722** (tetrahedron, face) pairs on the disc, the count of vertices lying
on the plane is **3 in 100.00 % of cases**. The control — the same test with a
randomly chosen face of the same object — gives 3 in **0.07 2.2 %**, and
touches no vertex at all 96 % of the time.
Two more, both 11 of 11: the bounding sphere encloses all four vertices in
100 % of the 63 410 records (with ~0.1 % padding), and the word at `+0x50` is
the record's own index in 100 %.
### ✅ The six floats at `+0x20` are the portal-graph edge costs
For face `i` of a tetrahedron, let `omit(i)` be the one vertex not on it. Then
cost[k] == | V[omit(i)] V[omit(j)] | / 3 for the k-th pair (i<j)
which is exactly the **distance between the centroids of faces `i` and `j`**
the A\* step cost of crossing the tetrahedron from one portal to another. It
holds to `1e-4` relative in **380 460 of 380 460** values, all 11 objects.
❔ The second float of each pair is unread. It is a genuine varied float, zero
in 8 26 % of slots. **🔴 Refuted**: "zero marks a hull edge" — `P(zero)` is
the same for edges on the hull as for interior ones (e.g. 2.9 % / 16.4 %
against 16.7 % / 64.0 % — no lift at all). A clearance width is the obvious
reading and is *not* established.
## ✅ Section 2 is a **face** — a plane plus the adjacency graph
```
+0x00 f32[3] unit normal
+0x0c f32 d
+0x10 f32[3] a point on the plane
+0x1c f32 1.0
+0x20 u16[3] the face's three vertex indices → section 0
+0x26 u16 the face's own index
+0x28 u16[2] the tetrahedra on either side; 0xFFFF = hull (no neighbour)
+0x2c u16[2] which of that tetrahedron's four face slots this is
```
The old page's plane reading was the same 16 bytes early, which is why it
reported "four zeros" at the front: those were the *previous* record's four
integer words, read as floats.
Verified over every face of every object (11 of 11, 100.00 % each):
* the word at `+0x26` is the record's own index;
* for each non-`0xFFFF` side, `tet[side].faces[slot[side]] == this face`;
* the face's three vertices are a subset of that tetrahedron's four.
And the Euler relation closes: `4·T == 2·(F B) + B` exactly on every object
(e.g. `4 × 1172 = 4688`, `F = 2584`, `B = 480` hull halves).
## ✅ What a `REGN` object is
A stage's `MapPath` is a **tetrahedral navigation mesh of the whole play
volume**, plus a uniform grid for point location:
* vertices, tetrahedra, and faces form a conforming tet mesh with full
face adjacency and hull markers;
* each tetrahedron carries the six face-to-face traversal costs, so A\* over
the portal graph needs no geometry at query time;
* the uniform grid answers "which tetrahedra could contain this point" in O(1),
which is what a flight game needs to localise a ship into the mesh each frame.
The 100 km-cube smoke-test map (`e993b93e`, `counts = [8, 6, 18, …]`) is the
textbook **six-tetrahedron decomposition of a cube** — 8 corners, 6 tets, 18
distinct faces (24 faces less the 6 shared internally) — which is as strong a
confirmation of the reading as the statistics are.
## 🔴 Corrections to the sections above, kept rather than deleted
Every one of these was a real observation; each fails for the same single
reason, and that is worth having on the page.
| earlier claim | status |
|---|---|
| "four section offsets at `0x70`" | 🔴 there are **six**, `0x70``0x84` |
| "the first section offset is always `0x80`" | 🔴 the *value* is `0x80`; the section is at `0x90` — the base is `+0x10` |
| "`counts[4]` = occupied cells **+ 2**, unexplained" | 🔴 artefact of the base error; `counts[4]` = occupied cells exactly |
| "the constant 96-byte tail after the plane list" | ✅ explained: two all-zero sentinel face records |
| "record `+0x1c` is a bounding-sphere radius (ratio 1.001 on one object, 0.130.27 on the rest)" | 🟡 **half right**`+0x0c` *is* a bounding-sphere radius, of the *tetrahedron*; the 1.001 was a coincidence of that map, where every tet shares the cube's circumsphere |
| "`(count, offset)` pairs point at leaf arrays of `count × 4` bytes" | 🟡 **right mechanism, wrong record** — it is the cell *item* at `+0x10`/`+0x14` pointing into section 5, not the cell index |
| "slots 811 are integers (denormal as float)" | ✅ correct, and they are the four vertex + four face `u16` indices |
| "section 2 records are 48 bytes, unit normal, `n·p + d = 0` in 133 573/133 573" | ✅ **stands** — the same bytes under either origin convention; only the record boundary moves, see the correction above |
| "section 0 points are inside the bbox, 13 467/13 467" | ⚠️ true but **uninformative** — it passes at either origin, because a denormal reads as ≈ 0 and 0 is inside the box |
| "slot 7 is a local scale, ~14× smaller than the inter-node distance" | ✅ correct as measured — it is a *tetrahedron's* bounding radius, which is exactly that small |
| "the BVH reading is refuted, no node sphere contains another" | ✅ stands, and is now explained: these are sibling tetrahedra, not a tree |
| "the cell payload holds section-1 indices" | ✅ stands — it holds a **pointer to an array of pointers**, and no index anywhere |
| "the static-correlation avenue is exhausted" | ✅ stands for correlations; the file's own fixup table was never a correlation |
## ❔ Still open
* the second float of each portal pair at tetrahedron `+0x24`, `+0x2c`, … ;
* the four flag bytes at tetrahedron `+0x54` (values 0/1);
* the exact predicate the tools used to decide cell membership (tighter than
sphere-vs-box);
* **the runtime consumer.** The deserialiser is found and proven; the code that
*queries* the grid is not. Searches that came up empty, so they are not
repeated: functions loading the header field groups off one base register
(only stack frames match); `vctsxs`/`vctuxs` float→int conversion (16
functions, only one outside the XDK ranges); and the magic-comparison route,
which cannot exist (above). The likely reason the field-offset search fails
is that a `float4`-aligned header is read with VMX loads, which carry no
useful displacement signature.
## Tooling
`tools/re-capture/regn_decode.py` — standalone, stdlib only.
```
regn_decode.py list hidden/MiscBin.pak
regn_decode.py dump hidden/MiscBin.pak [name_hash]
regn_decode.py verify hidden/MiscBin.pak # every check quoted above
```
`verify` prints, per object: the `POF0` pointer-slot shape, the reference-array
packing, the face/vertex incidence with its random control, the sphere/cell
agreement with its transposed-axis control, and the portal-cost identity.

View File

@@ -149,3 +149,63 @@ into those five shapes.
Two incidental facts fall out: **4 banks run at 44 100 Hz** where everything else
is 48 000, and the `BGM_*` tracks are the stereo ones.
## The 36 shared banks, decoded end-to-end (2026-08-28)
The census above is arithmetic — durations from the `seek` table, no decoding.
This section is the decode itself, for the 36 language-independent banks
(32 `BGM_*` + 3 `JNGL_*` + `Static.slb`), because they were the ones no viewer
had ever played: the library enumerator kept only names containing `VOICE` or
`\Briefing\`, so every one of them was filtered out before it could be tried.
**35 of 36 decode to plausible audio**, and the shapes agree with the census:
the 32 `BGM_*` come out 75555 s and **stereo**, matching "the `BGM_*` tracks
are the stereo ones"; `JNGL_002` decodes to 33.89 s against the manifest's
predicted 33.97 s.
Two things the census could not have caught, both found by decoding:
### `Static.slb`'s TOC entry over-declares its size
The SFX bank sits at the **highest offset in the archive** and claims
8 970 240 bytes — **616 768 past the end of `sound.p04`**. It is not our
extraction: `sound.p04` is byte-for-byte the size the ISO's own directory record
gives. A sweep of **every `.pak` on the disc** finds this one entry over-running
and no other, so the last entry's `comp_size` is an allocation size rather than a
stored size.
`PakArchive::stored_bytes` therefore allows a short read **only** for the
highest-offset entry. Any other overrun is still an error — that would be real
damage, and clamping it would hide the damage behind a half-decoded asset. With
the short read, `Static.slb` decodes to **514 s of mono**; before it, the SFX
bank could not be read at all.
### `JNGL_001.slb` does not decode — and that is a real gap, not a filter
It is one of the headerless banks (no `RIFF`), so it was already outside the
4 114-bank manifest above. Decoding it yields **0.01 s** — one frame, the
signature this corpus already records for a wrong channel count. But the usual
fixes do not apply:
* it is not a channel-count error the `RIFF+49` rule can repair, because there is
no `RIFF` to read the count from;
* its payload is **not a whole number of 2048-byte XMA1 packets** from any of the
four `DATA_OFFSET_CANDIDATES`, which a headerless XMA1 stream must be.
So `JNGL_001` is probably not a plain headerless XMA1 stream at all. The other
headerless root bank, `Static.slb`, decodes fine at 514 s, so the headerless path
is not broken in general — this is one bank in 9 519. It is listed in the viewer
and reports that it did not decode, rather than being hidden.
⚠️ Note on the offsets: `to_xma_riffs` still *scans* for the headerless data
offset, while [slb-data-offset.md](slb-data-offset.md) establishes the exact rule
(the cumulative `.pNN` segment start mod 2048, 8 783/8 783). Wiring the exact
rule into the decoder is open, and is the first thing to try on `JNGL_001`.
### Downmix is a per-category decision, not a constant
The decoder took the left channel unconditionally. That is right for **voice**,
whose content is mono however it is stored (some clips put the signal in the left
channel alone, others duplicate L=R) — and wrong for **music**, where the two
channels are a real stereo mix and taking one throws half of it away. The caller
now decides from the bank's category.

View File

@@ -0,0 +1,131 @@
# What a keyframe time is worth, and what shape the ramp has
**Status:**`CONFIRMED` for the two things the port is blocked on — the ramp is
**linear**, and the animation clock advances **2 keyframe time units per frame the
game submits**. 🟡 the conversion to *seconds* rests on one further step: the game
was measured presenting **27.6 frames/second**, which reads as a 30 Hz title
running at ~92 % under the emulator, giving **1 unit = 1/60 s**. That last step is
reasoning over a measurement, not a measurement — see *the reach of the negative*
below.
This answers `docs/port/MISSION.md` **Q1**. Everything animated downstream — the
title wordmark zoom, the main menu's staggered buttons, every fade — is scaled by
this number.
## The measurement
The question cannot be asked of a screen that is sitting still, and
`log_ui_draws` armed *at* a screen only ever sees the steady state. So arm it
repeatedly through the boot and keep every log: each F10 opens a new numbered
file and **closes** the previous one, which stays on disk complete. Re-arming
every 3 s tiles the whole approach to a screen, and whichever file straddles the
build contains it. `tools/re-capture/screen_build_capture.sh` does this; it sends
no pad input at all, because Ⓐ during the boot has ended a run on a permanent
black screen (`canary-scripted-input-traps.md`).
The developer-logo splash is the cheap target: it is the first thing the guest
draws, roughly ten seconds in, and its bundle (`GP_TITLE.pak`, `--all --build 11`
/ `14` — the `palogo` group) declares short, unambiguous ramps.
The capture is committed: [`captures/ui-timing/splash-build-draws.log`](captures/ui-timing/splash-build-draws.log)
(the raw draw stream) and [`captures/ui-timing/splash-build-quads.csv`](captures/ui-timing/splash-build-quads.csv)
(`tools/re-capture/kf_time_probe.py`, one row per quad per frame: pixel rect and
the per-vertex colour whose high byte is the element's fade).
Frame numbers are the emulator's **VdSwap count** — frames the guest submitted —
so an emulator running at 80 % of real time does not move them. That is the whole
reason to measure in this unit rather than with a stopwatch.
### The quads are the splash, by position and size
Every sprite in the capture lands on its declared placement:
| capture quad | declared element (build 11) | declared placement |
|---|---|---|
| `666x65 @ (307,331)` | `palogo_sqex.t32` 666×68 | (309,330) |
| `525x90 @ (378,155)` | `palogo_gamearts_eff.t32` 521×91 | (379,154) |
| `262x108 @ (512,306)` | `palogo_seta_eff.t32` 261×110 | (511,305) |
| `499x72 @ (390,162)` | `palogo_gamearts.t32` 500×71 | (390,164) |
| `243x86 @ (518,317)` | `palogo_seta.t32` 240×89 | (521,316) |
(A quad runs a few pixels under its sprite; that offset is already recorded in
`ui-title-paint-order-capture.md` and is not what is being measured here.)
## Result 1 — the ramp is linear, exactly
`palogo_gamearts_eff` and `palogo_seta_eff` declare a fade-in of
`t=15 a=0 → t=30 a=255`: a **15-unit** ramp. Their fade alpha, frame by frame,
straight out of the capture:
| frame | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
|---|---|---|---|---|---|---|---|---|
| alpha | `0x22` 34 | `0x44` 68 | `0x66` 102 | `0x88` 136 | `0xAA` 170 | `0xCC` 204 | `0xEE` 238 | `0xFF` 255 |
| ⇒ units into the ramp *k* | 2 | 4 | 6 | 8 | 10 | 12 | 14 | ≥15 |
`round(255·k/15) = 17k` reproduces **all seven samples with zero error**. An eased
ramp cannot do that: any ease-in/ease-out would bend the first and last steps, and
these are a constant 34 throughout. **Linear interpolation, refuted-nothing-left.**
The same element's fade-*out* gives the same law with a one-count offset —
`255·(1k/15) 1` = 254, 220, 186, 152, 118, 84, 67, 33 for
*k* = 0, 2, 4, 6, 8, 10, 11, 13, which is exactly what the capture holds on frames
108115.
## Result 2 — 2 time units per submitted frame
Read *k* off the row above: 2, 4, 6, 8, 10, 12, 14 on seven consecutive submitted
frames. **The clock advances 2.000 units per frame, over six consecutive
intervals, with no residual.** The fade-out shows the same 2-per-frame step with
one single-unit frame (*k* goes 10 → 11 → 13), so the quantum underneath is 1 unit
and the normal step is two of them.
Cross-checks in the same capture, all consistent:
* the declared hold `t=30 → t=45` (15 units) is held for frames 101107, **7
frames** ≈ 14 units;
* `palogo_sqex`'s fade-out steps in multiples of 17 per frame (34 with the odd
17), the same 255/15 quantum;
* the `gamearts`/`seta` pair holds full alpha for frames 116198 and fades out
over frames 199211.
## Result 3 — the conversion to seconds, and how far it reaches
300 submitted frames took **10.87 s** wall-clock, measured by arming one bounded
capture over the splash and timing it to its own `[UI-CAP] done` line:
**27.6 frames/second**.
With 2 units per frame that is **55 units/second measured**. The two readings that
fit are:
* **30 Hz present, 60-unit-per-second timeline** — the emulator running at 92 % of
real time. A 30 Hz renderer stepping a 60 Hz timeline is exactly the 2-units-
per-frame quantum that was measured, and 92 % is an ordinary number for this
container.
* 60 Hz present at 46 % of real time, giving a 120-unit-per-second timeline. This
requires the emulator to be running at *less than half speed* while drawing four
quads over a black screen, and it requires the game's timeline to tick at 120 Hz.
**Taking the first**: `1 unit = 1/60 s`. The title build (`GP_TITLE.pak` build 4,
t = 16 … 269) is then **4.2 s**, and the main menu build (build 5, t = 12 … 80)
**1.1 s**.
**The reach of this negative:** the present rate was measured *during the boot
splash*, where the guest is also streaming from the ISO, so it is a lower bound on
the emulator's speed and cannot by itself exclude the 60 Hz reading. What would
settle it is the same 300-frame timing taken on the **idle title screen**, where
nothing is loading — if that also comes out near 28 fps the game is 30 Hz and the
unit is 1/60 s; if it doubles to ~55 fps the game is 60 Hz and every duration on
this page halves. Until then the port should treat **2 units per rendered frame**
as the measured fact and `1/60 s` as the working conversion.
## What this does not say
* Nothing here is a *decode*: no field on the disc says "sixtieths of a second".
The number is measured from the running game, and the port is transcribing a
measurement, not a disc value.
* The splash drew `palogo_sqex`, then `palogo_gamearts` + `palogo_seta` with their
`_eff` sprites — but **not** `palogo_anima` (declared at 388×136 @ (446,449)) and
not `palogo_sqex_eff`, in the ~7 s of capture after the pair faded out. The
`_sqex_eff` absence is explainable (the capture joined the SQUARE ENIX logo
mid-hold); the missing studio-anima logo is not, and is left as an open
observation for Q2 rather than a claim.

View File

@@ -0,0 +1,536 @@
#!/usr/bin/env python3
r"""Census -- and preimage attack -- for IDXD field keys that carry no name.
Pure static work: reads the extracted disc, runs no emulator.
python3 tools/re-capture/idxd_unnamed_keys.py census > docs/re/data/idxd-unnamed-field-keys.txt
python3 tools/re-capture/idxd_unnamed_keys.py crack # corpus + brute-force attack
python3 tools/re-capture/idxd_unnamed_keys.py idtbl # dump the 42 hash-shaped keys in context
An IDXD field entry is `(key, name_off, value_off)`. When `name_off` is
0xFFFFFFFF the field has no name in the pool, and the reader must know what the
`key` means from somewhere else. This tool answers "how many such keys are
there, what do they look like, and can the names be recovered from the hash?"
The headline measurement (see docs/re/structures/idxd-unnamed-keys.md):
2 757 039 field entries in 7 750 IDXD objects
1 271 462 named, 1 485 577 unnamed
7 094 distinct keys that are never named ANYWHERE on the disc
7 052 of them are < 0x10000 -- author-assigned ordinals / element ids
42 of them are >= 0x01000000 -- genuine tag_hash values
`SYLPHEED_DISC` (default /work/sylph_extract) points at the extracted disc.
"""
import argparse, collections, glob, os, re, struct, sys, zlib
DISC = os.environ.get('SYLPHEED_DISC', '/work/sylph_extract')
# --------------------------------------------------------------------------- hashes
TAG_MODULUS = 0x00FFFFDF # 2^24 - 33, prime
TAG_MAGIC = 0x2101 # floor(2^56/M)+1, the guest's divide magic
def tag_hash(s):
"""IDXD record key / field tag -- `sub_82447DF0`.
Transcribed in docs/re/structures/idxd-tag-hash.md and implemented three
times in this tree (here, tools/re-capture/unitgroup.py, and
sylpheed_formats::hash::tag_hash). Case-SENSITIVE, bytes sign-extended.
"""
a = b = 0
for byte in s.encode('latin-1', 'replace'):
c = (byte - 256) if byte > 127 else byte # extsb
a = ((a << 8) & 0xFFFFFFFF)
a = (a + c) & 0xFFFFFFFF
b = (b + c) & 0xFFFFFFFF
hi = ((a * TAG_MAGIC) >> 32) & 0xFFFFFFFF # mulhwu
q = ((hi + (((a - hi) & 0xFFFFFFFF) >> 1)) & 0xFFFFFFFF) >> 23
a = (a - q * TAG_MODULUS) & 0xFFFFFFFF
return (((b << 24) & 0xFF000000) | (a & 0x00FFFFFF)) & 0xFFFFFFFF
NAME_MODULUS, NAME_RECIP = 0x00FFF9D7, 0x80031493
def _rotl(v, n):
return ((v << n) | (v >> (32 - n))) & 0xFFFFFFFF
def name_hash(s):
"""IPFB TOC path hash -- lowercased, a different modulus. `sub_82455C78`."""
bs = bytearray(s.encode('latin-1', 'replace'))
for i, x in enumerate(bs):
if 65 <= x <= 90:
bs[i] = x + 0x20
a = b = 0
for byte in bs:
c = ((byte - 256) if byte > 127 else byte) & 0xFFFFFFFF
a = ((_rotl(a, 8) & 0xFFFFFF00) + c) & 0xFFFFFFFF
b = (b + c) & 0xFFFFFFFF
q = _rotl(((a * NAME_RECIP) >> 32) & 0xFFFFFFFF, 9) & 0x1FF
a = (a - (q * NAME_MODULUS)) & 0xFFFFFFFF
return (((b << 24) & 0xFF000000) | (a & 0x00FFFFFF)) & 0xFFFFFFFF
# --------------------------------------------------------------------------- pak / IDXD
def pak_entries(pak):
"""yield (toc_key, payload) for every entry of an IPFB pak + its .pNN segments."""
idx = open(pak, 'rb').read()
data = b''.join(open(p, 'rb').read()
for p in sorted(glob.glob(pak[:-4] + '.p[0-9][0-9]')))
for i in range(struct.unpack_from('>I', idx, 4)[0]):
k, off, csize = struct.unpack_from('>III', idx, 16 + i * 12)
s = data[off:off + csize]
if s[:2] == b'Z1':
try:
s = zlib.decompress(s[10:])
except zlib.error:
continue
yield k, s
def parse_idxd(b):
"""-> [(record_key, record_name, [(tag, name|None, value, field_index)])]"""
u = lambda o: struct.unpack_from('>I', b, o)[0]
nrec = u(0x04)
npool_off = 0x08 + nrec * 16
npool = u(npool_off)
pool = npool_off + 4
strsize_off = pool + npool * 12
STR = strsize_off + 4
strsize = u(strsize_off)
if STR + strsize != len(b):
raise ValueError('string-pool trailer mismatch')
def s(o):
if o >= strsize:
raise ValueError('string offset out of pool')
return b[STR + o:b.index(b'\x00', STR + o)].decode('latin-1')
out = []
for i in range(nrec):
key, nm, lo, hi = (u(0x08 + i * 16 + j * 4) for j in range(4))
fields = []
for j in range(lo, hi):
o = pool + j * 12
tag, noff, voff = u(o), u(o + 4), u(o + 8)
fields.append((tag, s(noff) if noff != 0xFFFFFFFF else None, s(voff), j - lo))
out.append((key, s(nm), fields))
return out
def all_paks():
return sorted(glob.glob(DISC + '/dat/*.pak') + glob.glob(DISC + '/hidden/*.pak'))
def walk(counter=None):
"""yield (pak_basename, obj_toc_key, record_key, record_name, fields)."""
for pak in all_paks():
base = os.path.basename(pak)
for h, payload in pak_entries(pak):
if payload[:4] != b'IDXD':
continue
try:
recs = parse_idxd(payload)
except Exception as exc: # noqa: BLE001
print('PARSE FAIL %s %08x %s' % (base, h, exc), file=sys.stderr)
continue
if counter is not None:
counter['objects'] += 1
schema = recs[0][1] if recs else ''
for key, rname, fields in recs:
yield base, h, schema, key, rname, fields
# --------------------------------------------------------------------------- value profile
_FLOAT = re.compile(r'^-?(\d+\.\d*|\.\d+|\d+)([eE][-+]?\d+)?$')
_INT = re.compile(r'^-?\d+$')
_HEX = re.compile(r'^(0[xX])?[0-9a-fA-F]+$')
_FILE = re.compile(r'^[\w\\/. -]+\.[A-Za-z0-9]{2,4}$')
def classify(values):
"""A coarse type label for a bag of field values."""
if not values:
return 'empty'
kinds = set()
for v in values:
if v == '':
kinds.add('empty')
elif _INT.match(v):
kinds.add('int')
elif _FLOAT.match(v):
kinds.add('float')
elif _FILE.match(v):
kinds.add('file')
elif v in ('True', 'False', 'Yes', 'No', 'ON', 'OFF'):
kinds.add('bool')
elif _HEX.match(v) and len(v) >= 6:
kinds.add('hex')
else:
kinds.add('id')
order = ['file', 'id', 'hex', 'bool', 'float', 'int', 'empty']
return '+'.join(k for k in order if k in kinds)
# --------------------------------------------------------------------------- census
def census():
named = collections.defaultdict(collections.Counter)
unnamed = collections.Counter()
key_objs = collections.defaultdict(set)
key_recs = collections.defaultdict(collections.Counter)
key_idx = collections.defaultdict(lambda: [1 << 30, -1])
key_vals = collections.defaultdict(list)
key_nval = collections.Counter()
# per (object-schema, record-name) family, for the ordinal population
fam = collections.defaultdict(lambda: {'keys': set(), 'n': 0, 'vals': [], 'recs': 0})
tot = collections.Counter()
schema_of = {}
for base, h, schema, rkey, rname, fields in walk(tot):
tot['records'] += 1
# the object's schema hash is the key of its first record; use record name
fam_id = None
for tag, name, val, fidx in fields:
tot['fields'] += 1
if name is not None:
tot['named'] += 1
named[tag][name] += 1
else:
tot['unnamed'] += 1
unnamed[tag] += 1
key_objs[tag].add((base, h))
key_recs[tag][rname] += 1
lohi = key_idx[tag]
lohi[0] = min(lohi[0], fidx)
lohi[1] = max(lohi[1], fidx)
key_nval[tag] += 1
if len(key_vals[tag]) < 40:
key_vals[tag].append(val)
if fam_id is None:
fam_id = (base, schema)
f = fam[fam_id]
f['keys'].add(tag)
f['n'] += 1
if len(f['vals']) < 60:
f['vals'].append(val)
if fam_id is not None:
fam[fam_id]['recs'] += 1
return dict(named=named, unnamed=unnamed, key_objs=key_objs, key_recs=key_recs,
key_idx=key_idx, key_vals=key_vals, key_nval=key_nval, fam=fam, tot=tot)
def report(c, out=sys.stdout):
named, unnamed = c['named'], c['unnamed']
never = sorted(set(unnamed) - set(named))
hashy = [k for k in never if k >= 0x01000000]
ordinal = [k for k in never if k < 0x01000000]
both = sorted(set(unnamed) & set(named))
w = out.write
w('# IDXD field keys that carry no name\n')
w('#\n')
w('# Generated by tools/re-capture/idxd_unnamed_keys.py census\n')
w('# Disc: %s (all dat/*.pak + hidden/*.pak, .pNN segments joined, Z1 inflated)\n' % DISC)
w('# Companion write-up: docs/re/structures/idxd-unnamed-keys.md\n')
w('#\n')
w('# A field entry is (key, name_off, value_off). name_off == 0xFFFFFFFF means the\n')
w('# pool holds no name for it, so the reader must already know what `key` means.\n')
w('\n')
w('== TOTALS ==\n')
w('IDXD objects walked : %d\n' % c['tot']['objects'])
w('IDXD records walked : %d\n' % c['tot']['records'])
w('field entries : %d\n' % c['tot']['fields'])
w(' named (name_off != 0xFFFFFFFF) : %d\n' % c['tot']['named'])
w(' unnamed (name_off == 0xFFFFFFFF) : %d\n' % c['tot']['unnamed'])
w('distinct keys seen named somewhere : %d\n' % len(named))
w('distinct keys seen unnamed somewhere : %d\n' % len(unnamed))
w('distinct keys NEVER named anywhere on disc : %d\n' % len(never))
w(' of those, < 0x00010000 (ordinal-shaped) : %d\n' % len([k for k in never if k < 0x10000]))
w(' of those, in [0x10000, 0x1000000) : %d\n' % len([k for k in never if 0x10000 <= k < 0x1000000]))
w(' of those, >= 0x01000000 (hash-shaped) : %d\n' % len(hashy))
w('keys that are named in one place and unnamed in another: %d %s\n'
% (len(both), ['%08x' % k for k in both]))
w('unnamed FIELD ENTRIES whose key is hash-shaped : %d\n'
% sum(c['key_nval'][k] for k in hashy))
w('unnamed FIELD ENTRIES whose key is an ordinal : %d\n'
% sum(c['key_nval'][k] for k in ordinal))
w('unnamed FIELD ENTRIES with key 0x00000000 : %d (ordinal 0; the key is\n'
% sum(c['key_nval'][k] for k in both))
w('# "named" elsewhere only because tag_hash("") == 0, so it is counted apart)\n')
w('# The first of those two is the "504 hash-keyed fields disc-wide" already in\n')
w('# INDEX.md. It is 504 ENTRIES, not 504 distinct keys: %d keys x 6 language\n' % len(hashy))
w('# copies of the same object x 2 records (FILE and OFFSET) = %d.\n'
% sum(c['key_nval'][k] for k in hashy))
w('\n')
w('# Why the split at 0x01000000 is not arbitrary: a tag_hash puts the byte-sum\n')
w('# checksum in the top byte, so a hash lands below 0x01000000 only when the name\n')
w('# byte-sum is 0 mod 256 (1/256 of names) AND its low 24 bits are also tiny. Of\n')
zerotop = sorted(k for k in named if (k >> 24) == 0 and k != 0)
w('# the %d keys that DO carry a name, %d have a zero top byte, and the smallest of\n'
% (len(named), len(zerotop)))
w('# those is 0x%06x -- far above the ordinal band, which tops out at 0x%x.\n'
% (zerotop[0], max(ordinal)))
w('\n')
w('== PART 1: the %d HASH-SHAPED never-named keys ==\n' % len(hashy))
w('# key n objects record field value\n')
for k in hashy:
recs = ','.join(sorted(c['key_recs'][k]))
lo, hi = c['key_idx'][k]
vals = sorted(set(c['key_vals'][k]))
w('%08x %3d %2d %-13s %2d-%-2d %s\n'
% (k, c['key_nval'][k], len(c['key_objs'][k]), recs, lo, hi, '|'.join(vals)))
w('\n')
w('# All %d live in the six copies of <lang>\\script\\ID.tbl inside\n' % len(hashy))
w('# dat/GP_READY_ROOM.pak (eng jpn fra deu ita esp). Each copy has two records,\n')
w('# FILE and OFFSET, with the SAME 42 keys in the same order -- i.e. the object is\n')
w('# a column store: key -> (which .isb file, what offset inside it).\n')
w('\n')
w('== PART 2: the ordinal population, by (pak, object schema) ==\n')
w('# The schema is the name of the object\'s FIRST record -- the string whose\n')
w('# tag_hash is the object header word. `recs` counts records with >=1 unnamed field.\n')
w('# pak schema recs fields keys keymin-keymax valuetype sample\n')
rows = []
for (base, rname), f in c['fam'].items():
ks = f['keys']
rows.append((base, rname, f['recs'], f['n'], len(ks), min(ks), max(ks),
classify(f['vals']), f['vals'][0] if f['vals'] else ''))
for r in sorted(rows, key=lambda r: (-r[3], r[0], r[1])):
w('%-28s %-17s %5d %7d %5d %6x-%-6x %-10s %s\n'
% (r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8][:40]))
w('\n')
w('== PART 3: every never-named ordinal key ==\n')
w('# key occurrences distinct-records fieldidx-lo-hi valuetype sample-value\n')
for k in ordinal:
lo, hi = c['key_idx'][k]
w('%08x %8d %6d %4d-%-4d %-10s %s\n'
% (k, c['key_nval'][k], len(c['key_recs'][k]), lo, hi,
classify(c['key_vals'][k]), (c['key_vals'][k] or [''])[0][:40]))
w('\n')
w('== PART 4: the ID.tbl script-symbol call graph ==\n')
try:
idtbl(out)
except Exception as exc: # noqa: BLE001
w('# unavailable: %s\n' % exc)
# --------------------------------------------------------------------------- ID.tbl
READY_ROOM = DISC + '/dat/GP_READY_ROOM.pak'
LANGS = ('eng', 'jpn', 'fra', 'deu', 'ita', 'esp')
_RR_CACHE = {}
def readyroom_file(path):
"""Read one file out of GP_READY_ROOM.pak by its (lowercased) path."""
if not _RR_CACHE:
_RR_CACHE.update(dict(pak_entries(READY_ROOM)))
return _RR_CACHE.get(name_hash(path))
def idtbl(out=sys.stdout):
"""The 42 keys with their FILE/OFFSET columns, plus the .isb cross-check."""
payload = readyroom_file('eng\\script\\ID.tbl')
recs = {r[1]: r[2] for r in parse_idxd(payload)}
files = recs['FILE']
offs = recs['OFFSET']
out.write("# eng\\script\\ID.tbl -- key -> (defining .isb file, offset), plus every\n")
out.write('# .isb whose bytecode contains the key as a little-endian word (= a call site).\n')
out.write('# idx key defined-in offset referenced-from\n')
isb = {}
for name in sorted({v for _, _, v, _ in files}):
b = readyroom_file('eng\\script\\' + name)
if b:
isb[name] = {struct.unpack_from('<I', b, o)[0] for o in range(0, len(b) - 3)}
for i, (tag, _n, val, _f) in enumerate(files):
seen = sorted(f for f, s in isb.items() if tag in s)
out.write('%3d %08x %-24s %-6s %s\n'
% (i, tag, val, offs[i][2], ','.join(seen) or '-'))
# --------------------------------------------------------------------------- attacks
def _poly(s):
a = 0
for ch in s.encode('latin-1', 'replace'):
a = (a * 256 + ch) % TAG_MODULUS
return a
def corpus_attack(targets, words):
"""Straight dictionary attack. Returns {tag: [names]} and the corpus size."""
hits = collections.defaultdict(list)
n = 0
for w in words:
n += 1
h = tag_hash(w)
if h in targets:
hits[h].append(w)
return hits, n
def compose_attack(targets, tokens, suffixes=('',), seps=('', '_', '-')):
"""Two-token composition attack: name = t1 + sep + t2 + suffix.
Returns (hits, effective search space). The space is what matters: with a
32-bit tag, expected FALSE hits per target is space / 2**32.
"""
right = collections.defaultdict(list)
for t in tokens:
for sep in seps:
for suf in suffixes:
r = sep + t + suf
right[(len(r), _poly(r))].append(r)
lens = sorted({k[0] for k in right})
p256 = {l: pow(256, l, TAG_MODULUS) for l in lens}
hits = collections.defaultdict(list)
for left in tokens:
pl = _poly(left)
for l in lens:
for tag in targets:
want = ((tag & 0xFFFFFF) - pl * p256[l]) % TAG_MODULUS
for r in right.get((l, want), ()):
cand = left + r
if tag_hash(cand) == tag:
hits[tag].append(cand)
space = len(tokens) * sum(len(v) for v in right.values())
return hits, space
def brute_force(target, suffix='', maxlen=6, alphabet=None):
"""Meet-in-the-middle preimage search: all P over `alphabet` with
tag_hash(P + suffix) == target and len(P) <= maxlen.
Needs numpy. The honest limit is maxlen 6: the expected number of FALSE
preimages is |alphabet|**maxlen / 2**32, which is ~15 at 63**6 and ~58 000
at 63**8, so anything past 6 returns noise, not names.
"""
import numpy as np
if alphabet is None:
alphabet = ([chr(c) for c in range(48, 58)] + [chr(c) for c in range(65, 91)]
+ [chr(c) for c in range(97, 123)] + ['_'])
A = len(alphabet)
codes = np.array([ord(c) for c in alphabet], dtype=np.int64)
tabs = []
polys = np.zeros(1, dtype=np.int64)
sums = np.zeros(1, dtype=np.int64)
tabs.append((polys, sums))
for _ in range(maxlen // 2 + maxlen % 2):
polys = ((polys[:, None] * 256 + codes[None, :]) % TAG_MODULUS).ravel()
sums = (sums[:, None] + codes[None, :]).ravel()
tabs.append((polys, sums))
def decode(idx, k):
s = ''
for _ in range(k):
s = alphabet[idx % A] + s
idx //= A
return s
ps = _poly(suffix)
inv = pow(pow(256, len(suffix), TAG_MODULUS), -1, TAG_MODULUS)
need_sum = ((target >> 24) - sum(suffix.encode('latin-1'))) & 0xFF
out = []
for L in range(maxlen + 1):
v = L // 2
u = L - v
if u >= len(tabs) or v >= len(tabs):
continue
pu, su = tabs[u]
pv, sv = tabs[v]
order = np.argsort(pv, kind='stable')
pv_s = pv[order]
need = (((target & 0xFFFFFF) - ps) * inv) % TAG_MODULUS
want = (need - (pu * pow(256, v, TAG_MODULUS)) % TAG_MODULUS) % TAG_MODULUS
lo = np.searchsorted(pv_s, want, side='left')
hi = np.searchsorted(pv_s, want, side='right')
for i in np.nonzero(hi > lo)[0]:
for j in range(lo[i], hi[i]):
vi = order[j]
if ((su[i] + sv[vi]) & 0xFF) != need_sum:
continue
cand = decode(int(i), u) + decode(int(vi), v) + suffix
if tag_hash(cand) == target:
out.append(cand)
return out
# --------------------------------------------------------------------------- main
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('mode', choices=('census', 'idtbl', 'crack', 'selftest'))
ap.add_argument('--maxlen', type=int, default=6)
ap.add_argument('--pe', default=os.environ.get('SYLPHEED_PE'),
help='flat VA dump of default.xex; its strings widen the corpus')
args = ap.parse_args()
if args.mode == 'census':
report(census())
elif args.mode == 'idtbl':
idtbl()
elif args.mode == 'selftest':
# tag_hash must reproduce the key of every NAMED field on the disc.
ok = bad = 0
for _b, _h, _sc, _rk, _rn, fields in walk():
for tag, name, _v, _i in fields:
if name is None:
continue
if tag_hash(name) == tag:
ok += 1
else:
bad += 1
if bad < 5:
print('MISMATCH %r %08x != %08x' % (name, tag, tag_hash(name)))
print('tag_hash reproduces %d / %d named field keys (%d wrong)' % (ok, ok + bad, bad))
else:
c = census()
targets = {k for k in set(c['unnamed']) - set(c['named']) if k >= 0x01000000}
print('targets: %d hash-shaped never-named keys' % len(targets))
words = set()
for b, h, _sc, _rk, rname, fields in walk():
words.add(rname)
for _t, name, val, _i in fields:
if name:
words.add(name)
words.add(val)
if args.pe and os.path.exists(args.pe):
blob = open(args.pe, 'rb').read()
words |= {m.decode('latin-1') for m in re.findall(rb'[ -~]{3,}', blob)}
hits, n = corpus_attack(targets, words)
print('disc-string corpus : %d strings, %d hits, E[false]=%.4f'
% (n, len(hits), n * len(targets) / 2 ** 32))
for t, v in hits.items():
print(' %08x %s' % (t, v[:6]))
toks = collections.Counter()
for w in words:
for part in re.split(r'[^A-Za-z0-9]+', w):
for p in re.findall(r'[A-Z]+(?![a-z])|[A-Z][a-z]+|[a-z]+', part):
if 2 <= len(p) <= 14:
toks[p] += 1
tokens = [t for t, _ in toks.most_common()]
hits, space = compose_attack(targets, tokens)
print('2-token composition: space=%d, %d hits, E[false]=%.3f'
% (space, len(hits), space * len(targets) / 2 ** 32))
for t, v in hits.items():
print(' %08x %s' % (t, v[:6]))
for t in sorted(targets)[:3]:
r = brute_force(t, '', args.maxlen)
print('brute force <=%d chars %08x: %d candidates %s'
% (args.maxlen, t, len(r), r[:6]))
if __name__ == '__main__':
main()

View File

@@ -92,7 +92,11 @@ BUILTIN = {
13: 'play_se', 14: 'play_bgm',
15: 'set_group_speed',
17: 'wait_frames', 18: 'dist_lt', 20: 'hp_pct_test', 24: 'squad_survival_pct',
26: 'damage_unit', 30: 'objective_marker', 31: 'objective_marker_at_route',
26: 'set_unit_hp_pct',
# 28's field and default (1.0) are certain; the LABEL rests on a single
# consumer, so it is PROBABLE, not confirmed. See isl-builtins.md.
28: 'set_unit_damage_dealt_pct', 29: 'set_unit_damage_taken_pct',
30: 'objective_marker', 31: 'objective_marker_at_route',
33: 'global_counter0', 34: 'global_counter1', 36: 'screen_fade',
39: 'MARK_LAST_PHASE', 40: 'mark_not_last', 43: 'play_voice',
45: 'play_voice_vol', 46: 'squadron_trace', 47: 'squadron_attack',
@@ -109,8 +113,13 @@ BUILTIN = {
77: 'banner_mission_start', 78: 'banner_mission_complete',
81: 'banner_objective_update', 82: 'banner_mission_failed',
135: 'banner_mission_restart',
# 93 is `stopwatch_stop`, not the older `clear_flag`: 8/9/93 were renamed to
# stopwatch_start/_elapsed/_stop once the reading landed (isl-timers.md says
# of the old trio "the names predate the reading"). The branch that still
# called it `clear_flag` simply forked before that rename.
93: 'stopwatch_stop', 94: 'is_engaged', 95: 'unit_hp_pct', 100: 'reset_phase_threads',
102: 'prompt_yes_no', 104: 'request_next', 108: 'deploy_squadron_ex',
101: 'all_units_invulnerable', 102: 'prompt_yes_no', 104: 'request_next',
108: 'deploy_squadron_ex',
109: 'set_unit_flags', 115: 'named_event',
120: 'wait_cmds_drained', 123: 'timer_resume', 124: 'timer_stop',
125: 'timer_reset', 126: 'timer_elapsed', 127: 'timer_set',

View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Every call site of one or more ISL built-ins, across all `StageNN.ssb`.
Pure static work — reads the extracted scripts, runs no emulator.
python3 tools/re-capture/isl_builtin_sites.py 26,28,29 --ssb <dir>
python3 tools/re-capture/isl_builtin_sites.py 101 --ctx 3 --ssb <dir>
python3 tools/re-capture/isl_builtin_sites.py 28 --craft <unitgroup-dump>
Why this exists rather than `isl.py --calls`: naming a built-in needs the
OPERANDS and the NEIGHBOURS, not just a histogram of ids. `isl.py --calls`
scans on the encoding and so cannot see the `local[]` staging that carries a
call's arguments, and `isl.py dis` has to be re-synchronised by hand for each
site.
The walk here is a straight linear decode of the whole code region, from the
header's code offset (`+0x08`) to the symbol-table-1 offset (`+0x0C`, which is
where the code ends). That is safe: **measured on all 28 stages, the linear walk
lands on every call site the independent encoding scan finds** (Stage02:
2846/2846), so no call is skipped and no false instruction boundary is invented.
Columns are tab-separated:
stage offset builtin slot8 slot4-name slot4-raw symtype before after
`slot4` is the unit operand (a symbol-table-2 index; see isl.py for why the unit
sits at slot 4 and slot 0 is a tag word) and `slot8` is the second operand,
which is a bare double for the built-ins this was written for. `before`/`after`
are the ids of the neighbouring calls, which is what makes a fixed idiom —
`116 -> 101 -> 100 -> 124 -> 93` — visible.
With `--craft <file>` (the output of `unitgroup.py --all`) it also cross-tabs
value against the squadron's craft type, which is the test that separated
built-in 15's classes and separates these three.
"""
import argparse
import collections
import glob
import os
import re
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import isl # noqa: E402
def walk(b):
"""Yield `(offset, builtin_id, staged)` for every call in the code region.
`staged` is the `local[]` slot -> value map as it stood when the call
executed, i.e. the call's arguments. Both staging forms matter:
`local[i] = special[0]` after an immediate, and the far commoner
`local[i] = immediate` written straight in.
"""
code_base = struct.unpack_from('>I', b, 0x08)[0]
code_end = struct.unpack_from('>I', b, 0x0C)[0]
off, staged, pending = code_base, {}, None
while off + 4 <= code_end:
w = struct.unpack_from('>I', b, off)[0]
op, ln = w & 0xFF, (w >> 8) & 0xFF
k1, k0 = (w >> 24) & 0xFF, (w >> 16) & 0xFF
if ln == 0 or ln % 4:
break
words = [struct.unpack_from('>I', b, off + i)[0]
for i in range(4, max(ln, 4), 4) if off + i + 4 <= len(b)]
if op in (0, 1) and len(words) >= 2:
if k0 == 2 and k1 == 1:
pending = _imm(op, words)
elif k0 == 3 and k1 == 2 and pending is not None:
staged[words[0]] = pending
elif k0 == 3 and k1 == 1:
staged[words[0]] = _imm(op, words)
if op == 19 and words:
yield off, words[0], dict(staged)
staged = {}
off += ln
def _imm(op, words):
"""An `op 1` immediate is a DOUBLE carried as two words, not a float."""
if op == 1:
lo = words[2] if len(words) > 2 else 0
return struct.unpack('>d', struct.pack('>II', words[1], lo))[0]
return words[1]
def _fmt(v):
if v is None:
return '-'
return ('%g' % v) if isinstance(v, float) else '0x%X' % v
def load_craft(path):
"""{stage: {squadron: [craft, ...]}} from `unitgroup.py --all` output."""
out, stage, squad = collections.defaultdict(dict), None, None
for line in open(path):
m = re.match(r'^(S\d\d):', line)
if m:
stage = m.group(1)
continue
m = re.match(r'^ (\S+)\s+\S+\s+Count=', line)
if m:
squad = m.group(1)
out[stage][squad] = []
continue
m = re.match(r'^ unit=(\S+)', line)
if m and squad:
out[stage][squad].append(m.group(1))
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument('ids', help='comma-separated built-in ids')
ap.add_argument('--ssb', default='.', help='directory holding StageNN.ssb')
ap.add_argument('--ctx', type=int, default=2, help='neighbour calls to show')
ap.add_argument('--craft', help='unitgroup.py --all output, for the cross-tab')
a = ap.parse_args()
ids = {int(x) for x in a.ids.split(',')}
craft = load_craft(a.craft) if a.craft else None
vals = collections.defaultdict(collections.Counter)
units = collections.defaultdict(collections.Counter)
cross = collections.defaultdict(lambda: collections.defaultdict(collections.Counter))
unresolved = collections.Counter()
for path in sorted(glob.glob(os.path.join(a.ssb, 'Stage*.ssb'))):
stage = os.path.basename(path)[:-4]
b = isl.load(path)
sym2 = isl.symbols(b, 2)
calls = list(walk(b))
for i, (off, bid, staged) in enumerate(calls):
if bid not in ids:
continue
uidx = staged.get(4)
typ, name = sym2.get(uidx, (None, '?')) if isinstance(uidx, int) \
else (None, '?')
val = _fmt(staged.get(8))
print('%s\t0x%06X\t%d\t%s\t%s\t%s\ttype%s\t[%s]\t[%s]' % (
stage, off, bid, val, name,
('0x%X' % uidx) if isinstance(uidx, int) else uidx, typ,
','.join(str(calls[j][1]) for j in range(max(0, i - a.ctx), i)),
','.join(str(calls[j][1])
for j in range(i + 1, min(len(calls), i + 1 + a.ctx)))))
vals[bid][val] += 1
units[bid][name] += 1
if craft is not None:
cs = craft.get('S' + stage[-2:], {}).get(name)
if cs:
for c in set(cs):
cross[bid][c][val] += 1
else:
unresolved[bid] += 1
for bid in sorted(ids):
n = sum(vals[bid].values())
print('\n# builtin %d: %d sites, %d distinct values' % (bid, n, len(vals[bid])))
print('# values: ' + ', '.join('%s x%d' % kv for kv in vals[bid].most_common()))
print('# units: %d distinct, top: %s' % (
len(units[bid]), ', '.join('%s x%d' % kv for kv in units[bid].most_common(10))))
if craft is not None:
print('# craft cross-tab (%d unresolved):' % unresolved[bid])
for c in sorted(cross[bid], key=lambda c: -sum(cross[bid][c].values())):
print('# %-42s %4d %s' % (
c, sum(cross[bid][c].values()),
', '.join('%sx%d' % kv for kv in cross[bid][c].most_common())))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Per-frame quad geometry and vertex colour from a `log_ui_draws` capture.
`ui_draw_order.py` answers "in what order were these painted"; this answers
"what did this quad look like on frame N, and on N+1". That is what a keyframe
TIME unit has to be measured against: the bundle says an element ramps from
t=31 to t=34, and the only way to learn what a `t` is worth is to count the
rendered frames the same ramp takes in the running game.
kf_time_probe.py <capture.log> [--csv out.csv]
Emits one row per quad per frame: frame, draw index, pixel rect, whether the
quad is axis-aligned, and the per-vertex colour word (whose alpha byte is the
element's fade). Frame numbers are the emulator's VdSwap count, so they are
SUBMITTED FRAMES, not wall-clock — which is the point: an emulator that runs
at half speed does not move them.
"""
import re
import sys
W, H = 1280, 720
VERT = re.compile(r"\[(-?\d+\.\d+),(-?\d+\.\d+),z=(-?\d+\.\d+)(?:,col=([0-9A-F]{8}))?\]")
def quads(log):
"""Yield (frame, draw, x0, y0, w, h, rot, col) for every quad in the log."""
frame, pending = None, None
for line in open(log):
if line.startswith("# every draw"):
frame = int(line.rstrip().split()[-1].split("..")[0])
continue
if line.startswith("--- frame"):
frame = int(line.split()[2])
continue
m = re.match(r"\s*(\d+) (prim=.*)", line)
if m:
pending = (int(m.group(1)), m.group(2))
continue
if "vb=0x" in line and pending:
hits = VERT.findall(line)
verts = [(float(a), float(b), c) for a, b, _z, c in hits]
if verts:
# Two conventions in one log: the UI sprite shader emits NDC,
# the full-screen pass emits pixels already.
if max(abs(v) for x, y, _c in verts for v in (x, y)) > 4.0:
pts = [(x, y, c) for x, y, c in verts]
else:
pts = [((x + 1) / 2 * W, (1 - y) / 2 * H, c) for x, y, c in verts]
per = 4 if "prim=13" in pending[1] else len(pts)
for q in range(0, len(pts), per):
chunk = pts[q:q + per]
if not chunk:
continue
xs = [p[0] for p in chunk]
ys = [p[1] for p in chunk]
x0, x1, y0, y1 = min(xs), max(xs), min(ys), max(ys)
rot = all(
(abs(x - x0) < 1.0 or abs(x - x1) < 1.0)
and (abs(y - y0) < 1.0 or abs(y - y1) < 1.0)
for x, y in zip(xs, ys)
)
cols = {p[2] for p in chunk if p[2]}
col = sorted(cols)[0] if len(cols) == 1 else (
"/".join(sorted(cols)) if cols else "")
yield (frame, pending[0], round(x0), round(y0),
round(x1 - x0), round(y1 - y0), "" if rot else "ROT",
col)
pending = None
def main():
log = sys.argv[1]
out = None
if "--csv" in sys.argv:
out = open(sys.argv[sys.argv.index("--csv") + 1], "w")
out.write("frame,draw,x,y,w,h,rot,col\n")
for row in quads(log):
line = "%d,%d,%d,%d,%d,%d,%s,%s" % row
if out:
out.write(line + "\n")
else:
print(line)
if out:
out.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())

0
tools/re-capture/regn_decode.py Normal file → Executable file
View File

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Catch a screen being BUILT, not sitting still.
#
# `log_ui_draws` armed at the title only ever sees the steady state, and the
# steady state is the one thing an animation question cannot be asked of. The
# build — the wordmarks zooming in, the effect sprites wiping across, the black
# fade lifting — happens in the seconds BEFORE the screen classifier can call it
# a title.
#
# So arm repeatedly and keep every log: each F10 opens a new numbered file and
# CLOSES the previous one (which stays on disk, complete). Re-arming every few
# seconds through the boot therefore tiles the whole approach to the title, and
# whichever file happens to straddle the build contains it. Stop re-arming the
# instant the title is classified, so a press cannot land mid-build and abandon
# the one file that matters.
#
# NO pad input at all. Ⓐ during the boot has ended a run on a permanent black
# screen (docs/re/canary-scripted-input-traps.md), and nothing here needs it.
#
# Usage: screen_build_capture.sh [out_dir] [timeout_s]
# FRAMES= frames per capture window (default 1200 ~ 20 s at 60 Hz)
# MAXDRAWS= hard stop per capture (default 80000)
# REARM= seconds between F10 presses (default 10)
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
SD="$(cd "$(dirname "$0")" && pwd)"
OUT="${1:-/sylph-home/re/buildcap}"
TIMEOUT="${2:-600}"
mkdir -p "$OUT"; rm -f "$OUT"/xenia_re_ui_draws_*.log
alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; }
shot(){ screenshot "$1" >/dev/null 2>&1; }
wide(){ [ "$(identify -format '%w' "$1" 2>/dev/null || echo 0)" -gt 1000 ]; }
( cd "$OUT" && nohup run-canary \
--ui_draw_capture_frames="${FRAMES:-1200}" \
--ui_draw_capture_max="${MAXDRAWS:-80000}" \
--create_profile_if_none="${SYLPH_TAG:-SylphRE}" \
--logged_profile_slot_0_xuid="${SYLPH_XUID:-B13EBABEBABEBABE}" \
>"$OUT/canary.stdout" 2>"$OUT/canary.stderr" & )
sleep "${LAUNCH_WAIT:-8}"
until xdotool search --name "Xenia-canary" >/dev/null 2>&1; do
[ -n "$(alive)" ] || { echo "EMULATOR GONE before the window appeared"; exit 4; }
sleep 1
done
win="$(xdotool search --name "Xenia-canary" | tail -1)"
echo "WINDOW=$win"
arm(){
xdotool windowactivate "$win" 2>/dev/null
xdotool key --window "$win" F10 2>/dev/null
xdotool key F10 2>/dev/null
}
last_arm=-999
deadline=$(( SECONDS + TIMEOUT ))
s=none
while [ $SECONDS -lt $deadline ]; do
[ -n "$(alive)" ] || { echo "EMULATOR GONE at ${SECONDS}s"; exit 4; }
shot /tmp/sbc.png
if [ -s /tmp/sbc.png ]; then
wide /tmp/sbc.png || { echo "GRAB IS NOT THE GAME SURFACE at ${SECONDS}s"; exit 6; }
s="$(python3 "$SD/screen_id.py" /tmp/sbc.png | awk '{print $1}')"
fi
if [ "$s" = "${WANT:-title}" ]; then
echo "t=${SECONDS}s $s <- STOP re-arming"
cp /tmp/sbc.png "$OUT/reached.png"
break
fi
if [ $(( SECONDS - last_arm )) -ge "${REARM:-10}" ]; then
arm; last_arm=$SECONDS
echo "t=${SECONDS}s $s (armed)"
else
echo "t=${SECONDS}s $s"
fi
sleep 1
done
# Let the straddling window run itself out rather than cutting it short.
echo "waiting for the in-flight capture to close..."
for _ in $(seq 1 40); do
grep -q "UI-CAP. done" "$OUT/canary.stdout" 2>/dev/null && break
sleep 1
done
grep -i "UI-CAP" "$OUT/canary.stdout" | tail -6
ls -l "$OUT"/xenia_re_ui_draws_*.log 2>/dev/null || echo "NO CAPTURE LOG"
echo "SCREEN BUILD CAPTURE DONE (screen=$s, emulator left running)"

View File

@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""Why a `.slb` bank's XMA data starts at 1392 / 1468 / 1600 / 1728.
Static, disc-only. No emulator, no decoder, no run-generated input: everything
here is read straight out of `dat/sound.pak` + `dat/sound.p00..p04`.
The answer (see docs/re/structures/slb-data-offset.md): the XMA packet grid is
2048-byte aligned *inside each individual `.pNN` segment file*, but the `.pak`
TOC addresses entries in the flat CONCATENATION of those files, at offsets that
are themselves multiples of 2048. The segment files are not multiples of 2048
long, so the grid phase seen inside an entry is
X = (cumulative start of the .pNN segment holding the wave) mod 2048
and the four disc-wide values are just the running sums of the segment sizes.
Subcommands
phases the segment table and where 1392/1468/1600/1728 come from
verify predicted vs measured X over every bank on the disc
bank <name> full structural dump of one bank entry
chain <name> [n] the tiling arithmetic across n consecutive entries
Usage
SYLPHEED_DISC=/work/sylph_extract python3 slb_segment_phase.py verify
"""
import bisect
import os
import re
import struct
import sys
import zlib
BLOCK = 2048 # XMA1 packet size and the archive's data-offset alignment unit
WAVE_HDR = 4096 # RIFF + 32-byte `fmt ` + `Dmmy` pad, always exactly two blocks
# --- IPFB name hash (crate `sylpheed_formats::hash`, from `sub_82455C78`) -----
MODULUS = 0x00FF_F9D7
RECIP = 0x8003_1493
def _rotl32(x, n):
x &= 0xFFFFFFFF
return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF
def name_hash(name):
a = b = 0
for byte in name.lower().encode("latin-1"):
c = (byte - 256) & 0xFFFFFFFF if byte >= 0x80 else byte
a = ((_rotl32(a, 8) & 0xFFFFFF00) + c) & 0xFFFFFFFF
b = (b + c) & 0xFFFFFFFF
q = _rotl32(((a * RECIP) >> 32) & 0xFFFFFFFF, 9) & 0x1FF
a = (a - q * MODULUS) & 0xFFFFFFFF
return ((b & 0xFF) << 24) | (a & 0xFFFFFF)
class Pak:
"""Minimal IPFB reader: TOC plus raw access to the concatenated segments."""
def __init__(self, path):
data = open(path, "rb").read()
assert data[:4] == b"IPFB", data[:4]
count, self.block, self.flags = struct.unpack_from(">III", data, 4)
self.toc = [struct.unpack_from(">III", data, 0x10 + 12 * i) for i in range(count)]
base, pos, self.segs = path[:-4], 0, []
i = 0
while os.path.exists("%s.p%02d" % (base, i)):
seg = "%s.p%02d" % (base, i)
n = os.path.getsize(seg)
self.segs.append((pos, pos + n, seg))
pos, i = pos + n, i + 1
self.seg_starts = [s for s, _, _ in self.segs]
def raw(self, off, size):
"""Bytes [off, off+size) of the flat concatenation of the .pNN files."""
out = b""
for s, e, path in self.segs:
if off < e and off + size > s:
lo, hi = max(off, s), min(off + size, e)
with open(path, "rb") as f:
f.seek(lo - s)
out += f.read(hi - lo)
return out
def read(self, idx):
_, off, csz = self.toc[idx]
raw = self.raw(off, csz)
return zlib.decompress(raw[10:]) if raw[:2] == b"Z1" else raw
def find(self, name):
h = name_hash(name)
for i, (hh, _, _) in enumerate(self.toc):
if hh == h:
return i
return None
def phase_at(self, concat_off):
"""The 2048-grid phase in force at a flat-concatenation offset."""
k = bisect.bisect_right(self.seg_starts, concat_off) - 1
return self.seg_starts[k] % BLOCK
def entry_names(disc):
"""Every `.slb` path named in any `<lang>\\sounds.tbl` inside tables.pak."""
tbl = Pak(os.path.join(disc, "dat", "tables.pak"))
names = set()
for lang in ("eng", "jpn", "deu", "fra", "esp", "ita"):
i = tbl.find(lang + "\\sounds.tbl")
if i is None:
continue
blob = tbl.read(i)
for run in re.finditer(rb"[\x20-\x7e]{6,}", blob):
for m in re.finditer(r"[A-Za-z0-9_\\.]+\.slb", run.group().decode("latin-1")):
names.add(m.group())
return names
# --- structure walkers --------------------------------------------------------
def bank_headers(data, phase):
"""Bank headers inside an entry. They sit on the grid, so only `phase` is scanned."""
out = []
for off in range(phase, max(0, len(data) - 56), BLOCK):
if data[off + 0x18 : off + 0x1C] != b"\x00\x00\x08\x00":
continue
if data[off : off + 4] != data[off + 0x20 : off + 0x24]:
continue
f = struct.unpack(">14I", data[off : off + 56])
out.append(
dict(at=off, id=f[0], data_size=f[7], hdr_blocks=f[9],
bits=f[10] >> 16, channels=f[10] & 0xFFFF)
)
return out
def waves(data):
out = []
for m in re.finditer(b"RIFF", data):
r = m.start()
if data[r + 8 : r + 12] != b"WAVE":
continue
dp = data.find(b"data", r)
if dp < 0:
continue
out.append(dict(at=r, data_at=dp + 8,
data_size=struct.unpack_from("<I", data, dp + 4)[0]))
return out
def seeks(data):
"""`seek` (XMA dpds) chunks: one u32 LE per emitted packet, so the entry count
is the wave's packet count and the chunk sits immediately after its data."""
out = []
for m in re.finditer(b"seek", data):
s = m.start()
if s + 16 > len(data):
continue
size, streams, n = struct.unpack_from("<III", data, s + 4)
if streams == 1 and size == 8 + 4 * n and n:
out.append(dict(at=s, packets=n, data_start=s - n * BLOCK))
return out
# --- subcommands --------------------------------------------------------------
def cmd_phases(pak):
print("segment size size%2048 cum_start PHASE (= cum_start%2048)")
running = 0
for s, e, path in pak.segs:
n = e - s
print(" %-10s %11d %8d %12d %d" % (os.path.basename(path), n, n % BLOCK, s, s % BLOCK))
running = s % BLOCK
print()
print("The four disc-wide values are the running sums of `size % 2048`:")
acc = 0
for s, e, path in pak.segs:
print(" %-10s phase %4d" % (os.path.basename(path), s % BLOCK))
acc = (acc + (e - s)) % BLOCK
_ = running
def cmd_verify(pak, disc):
names = entry_names(disc)
ok = bad = 0
seek_ok = seek_bad = 0
misses = []
for name in sorted(names):
i = pak.find(name)
if i is None:
continue
_, off, _ = pak.toc[i]
data = pak.read(i)
w = waves(data)
if w:
pred = pak.phase_at(off + w[0]["at"])
meas = w[0]["at"] % BLOCK
if pred == meas:
ok += 1
else:
bad += 1
if len(misses) < 10:
misses.append((name, pred, meas))
else:
# RIFF-less entry: the prediction is still exact, cross-check on `seek`.
sk = seeks(data)
if sk:
pred = pak.phase_at(off + sk[0]["at"])
if sk[0]["at"] % BLOCK == pred:
seek_ok += 1
else:
seek_bad += 1
print("banks with a RIFF : predicted == measured %d, mismatches %d" % (ok, bad))
print("RIFF-less, via seek : agree %d, disagree %d" % (seek_ok, seek_bad))
for m in misses:
print(" MISMATCH", m)
return 1 if (bad or seek_bad) else 0
def cmd_bank(pak, name):
i = pak.find(name)
if i is None:
sys.exit("no such entry: " + name)
_, off, csz = pak.toc[i]
data = pak.read(i)
phase = pak.phase_at(off)
print("%s\n pak offset %d (%%2048=%d) stored %d bytes phase %d"
% (name, off, off % BLOCK, csz, phase))
events = []
for b in bank_headers(data, phase):
events.append((b["at"], "BANK id=%d data_size=%d hdr=%d blocks (%d B) %dbit/%dch -> wave at %d"
% (b["id"], b["data_size"], b["hdr_blocks"], b["hdr_blocks"] * BLOCK,
b["bits"], b["channels"], b["at"] + b["hdr_blocks"] * BLOCK)))
for w in waves(data):
over = w["data_at"] + w["data_size"] - len(data)
events.append((w["at"], "RIFF data@%d size=%d (%d packets)%s"
% (w["data_at"], w["data_size"], w["data_size"] // BLOCK,
" OVERRUNS window by %d" % over if over > 0 else "")))
for s in seeks(data):
events.append((s["at"], "seek %d packets -> its data began at %d%s"
% (s["packets"], s["data_start"],
" (BEFORE this window)" if s["data_start"] < 0 else "")))
for at, text in sorted(events):
print(" %8d %%2048=%-5d %s" % (at, at % BLOCK, text))
def cmd_chain(pak, name, count):
"""Show that entry K's overrunning wave lands exactly on entry K+1's leading `seek`."""
i = pak.find(name)
if i is None:
sys.exit("no such entry: " + name)
order = sorted(range(len(pak.toc)), key=lambda k: pak.toc[k][1])
start = order.index(i)
prev = None
for k in order[start : start + count]:
_, off, csz = pak.toc[k]
data = pak.read(k)
padded = (len(data) + BLOCK - 1) // BLOCK * BLOCK
sk = seeks(data)
lead = sk[0] if sk and sk[0]["data_start"] < 0 else None
w = waves(data)
tail = w[-1] if w and w[-1]["data_at"] + w[-1]["data_size"] > len(data) else None
line = "off=%-11d len=%-7d padded=%-7d" % (off, len(data), padded)
if lead:
line += " leading seek@%-6d (%d packets)" % (lead["at"], lead["packets"])
if tail:
line += " tail wave ends at %d" % (tail["data_at"] + tail["data_size"])
print(line)
if prev and lead:
predicted = prev[0] - prev[1] # tail end minus previous padded length
mark = "OK " if predicted == lead["at"] else "!! "
print(" %s predecessor's wave ends %d past its padded window; leading seek is at %d"
% (mark, predicted, lead["at"]))
prev = ((tail["data_at"] + tail["data_size"]) if tail else None, padded)
if prev[0] is None:
prev = None
def main():
disc = os.environ.get("SYLPHEED_DISC")
if not disc:
sys.exit("set SYLPHEED_DISC to the extracted disc root")
pak = Pak(os.path.join(disc, "dat", "sound.pak"))
argv = sys.argv[1:] or ["phases"]
cmd = argv[0]
if cmd == "phases":
cmd_phases(pak)
elif cmd == "verify":
sys.exit(cmd_verify(pak, disc))
elif cmd == "bank":
cmd_bank(pak, argv[1])
elif cmd == "chain":
cmd_chain(pak, argv[1], int(argv[2]) if len(argv) > 2 else 4)
else:
sys.exit(__doc__)
if __name__ == "__main__":
main()