Commit Graph

19 Commits

Author SHA1 Message Date
Sylpheed RE agent
7460d71e6e re: RATC +0x10 is a u16 pair too, and no bit is a clean predicate
The u16 at +0x10 is 0 in all 2985 bundles; the content is a 16-bit flag word at
+0x12 with 83 distinct values. Reading it as a u32 inflates the field and hides
that the header is built from u16 pairs -- the same shape +0x0c turned out to
have. All 16 bits are used, from 1.4% to 91.5%.

Cross-tabulated every bit against four properties measurable from the bundle:
multi-element, animated, window-starts-at-zero, 30fps. No bit is close to a
clean predicate. The strongest is bit 10 against window-at-zero, 0.79 vs 0.21 --
a real association but not a rule, and exactly the kind of moderate split that
invites over-reading.

Bit meanings stay open, but four candidate readings are excluded rather than
untried and the field is correctly sized. Every property visible in the file has
now been tried, so assigning meanings likely needs the game observed with
individual bundles loaded.
2026-08-26 06:59:08 +00:00
Sylpheed RE agent
92526469d9 re: the RATC +0x0c interval is authored, not a keyframe extent
Tested the alternative I recorded last iteration. Parsing every bundle's
keyframe times (2985/2985 parse), the derived-summary reading -- (high,low) ==
(min,max) keyframe time -- holds in 6 of 2985 (0.2%).

The apparent 34.2% match on 'high == min' is a coincidence of zeros: the minimum
keyframe time is 0 in 96% of bundles and high is 0 in 34.9%, so the 1022
'matches' are exactly the both-zero cases. Worth noting that last iteration I
declined to treat the high==0 share as support -- it turned out to be the
confound rather than the signal.

The interval is also narrow: (low-high)/(max-min) has a median of 0.019, about
2% of the keyframe span. It lies inside the keyframe range in 88.6%, entirely
after in 174 and entirely before in 68.

A short authored window is not the shape of a playback range or a whole-animation
loop region, so those readings weaken too. What it is stays open.
2026-08-26 06:52:44 +00:00
Sylpheed RE agent
49cd4996f2 re: RATC +0x0c is two u16s forming an ordered interval, not one number
Read as a u32 it looks meaningless -- 179 distinct values up to 248581842. The
raw values give it away (0x0007000F, 0x000F001A, 0x003C0064): two big-endian
u16s. Over all 2985 bundles, high < low in 2985/2985 with no equal and no
inverted cases, and both are bounded by the animation length at +0x08. Span
runs 1-1200, clustering on 1/10/30/8/20; low equals the animation length in 4%.

A strict ordering holding 2985 times rules out flags or a packed count. Which
interval it is stays yellow -- playback range, loop region and active window all
fit equally.

Recorded the alternative I did NOT test: that (high, low) is simply the min and
max keyframe time, making it a derived summary rather than an authored range.
First step written down. The 34.9% of bundles with high == 0 leans against it
but is not evidence on its own.
2026-08-26 06:46:50 +00:00
Sylpheed RE agent
a40a0b7b01 docs: looking closer at the frame-rate word weakened it, and that is the result
The 16.16 reading of +0x04 rested on twelve bundles at 30.0. They are not twelve
witnesses: they are TWO bundles - pghud_range_main_em/emeff and
pghud_range_nose_em/emeff, both dur 30 - repeated across six language PAKs. A
30-frame flash is equally consistent with 0.5 s at 60 and 1 s at 30, so the
observation that "30.0 bundles are shorter" says almost nothing.

Worse for the clean fixed-point reading: the four bundles at 0x3C0001 would be
60.0000152 fps, which nobody authors. They are py_ranking_jump/py_ranking_next
dialogs, all dur 60. So the better reading is <rate:16>.<flag:16> - a rate-like
number in the high half and a small low field that is 0 on 2843 bundles and 1 on
four, meaning unknown.

Also stated: nothing in this container can settle it by timing, because the
emulator runs on software Vulkan far from real time - a stopwatch would measure
lavapipe rather than the game.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 07:34:56 +00:00
Sylpheed RE agent
192a23a481 formats: +0x08 is the animation length - checked against the keyframe times
The rate/duration reading of the bundle header came from the values alone, so it
was checked against something the file states independently: the keyframe `time`
fields in the placement region.

Over the 2313 bundles that have both, the largest keyframe time is <= +0x08 in
EVERY one, none exceeds it, and 444 reach it exactly. The refutation attempt is
in the same data: a large unrelated constant would bound every time too, but the
max_time/+0x08 ratio would then pile up near zero - instead it peaks at exactly
1.0 with 520 bundles. Asserted, including the shape of that histogram, so the
bound cannot later be waved through as vacuous.

Stated precisely because the units are a separate claim: what is proven is that
+0x08 is the animation length in the SAME UNIT as a keyframe's time. That the
unit is frames still rests on the values (30/60/120/1200) and on the 16.16
reading of +0x04, which stays amber - the only new evidence for it is that the
twelve 30.0 bundles cap at +0x08 = 30 while the 2843 60.0 ones reach 1440, and
twelve bundles is not a demonstration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 04:40:28 +00:00
Sylpheed RE agent
27fcb69fdf formats: sweep the bundle header - no screen flag, but three of its words decode
The backlog asked what makes a bundle a screen rather than a fragment, and the
obvious suspect was the 32-byte header. Swept over all 2859 composable bundles
with a real declaration table.

The answer to the question is NO, and it is asserted rather than argued: no bit
of the flags word at +0x10 labels a screen. The best any bit manages is bit 13 -
403 bundles, 179 of them carrying a full-screen element, a 44% hit rate against a
12.8% base - and the commonest bit is set on 91% of everything. Enrichment, not a
marker.

The sweep found more than it was asked for, though. The header is not dead space:
+0x18 is 1280 on 2829 bundles and +0x1c is 720 on 2823 - the design resolution at
bundle level, the same pair the parser already reads out of a .rat record, and
asserted here. And +0x04 takes only three values, 0x3C0000 on 2843 and 0x1E0000
on 12, which are exactly 60.0 and 30.0 in 16.16 fixed point, with +0x08 taking
30/1200/120/60 - a frame rate and a duration in frames would fit a format whose
records are keyframe lists. That reading is marked amber: it comes from the
values alone and is not verified against an animation.

Also recorded, since the file will not say: element counts are min 1, median 2,
p75 5, p95 23, max 56, and only 365 bundles carry a full-screen element. The
population is mostly fragments and the separation is shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 04:26:06 +00:00
Sylpheed RE agent
d9a2229217 formats: what opt links, measured - a record-to-record reference, focus is one use of it
Two readings were on record and both were wrong in different directions: the
structure doc called it "normal state -> focused state" from a single example,
and the backlog called it "refuted as focus; unexplained otherwise".

Classified every link reachable from a declaration table: 1467 links, and ALL
1467 resolve to a RATC child of their own bundle, ALL are .rat -> .rat, none
dangle and none self-link. 1076 (73%) match the <stem>f focus pattern; the other
391 are chains between effect records - px_bunk_eff01 -> pjex_eff -> pjex_eff07,
pveff01 -> pjeff02 -> pjeff21 - which also explains why only 227 targets are
themselves declared elements: the middle of a chain is, the end is not.

So `opt ` is a record-to-record reference within the bundle, and focus is its
commonest use rather than its meaning.

Coverage is stated rather than glossed: the bundles hold 18718 raw `opt ` tags
against the 1467 classified, because opt_link reads the first tag of a DECLARED
element's record. Roughly 92% of occurrences sit deeper in the chains (or are
byte coincidences - the scan is unaligned) and are untested. The numbers are
asserted so the answer cannot drift back into an anecdote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 04:09:18 +00:00
Sylpheed RE agent
2d28226b30 formats: the declaration entry does not mark a focused state - swept and asserted
The backlog's cheapest open question about the 60-byte entry: kind is a flags
word (0x10 untextured primitive, 0x4 repeated instance, 0x3002 button record), so
a focus bit would be the obvious answer and the name-pairing in
mark_focused_states would be a convention standing in for a real field.

It is not. Over every screen build on the disc: 54 name-paired focused/base
pairs, all 54 with IDENTICAL kind (all 0x0), NO bit ever set on the focused entry
and clear on its base, and the only words of the entry that ever differ are +48
and +52 - the pivot. The two entries differ in where the sprite sits and in
nothing else.

The test asserts all four numbers rather than just printing them, so the negative
is pinned instead of decaying back into a suspicion. Also noted: these buttons
carry kind = 0x0, so the documented 0x3002 "button record" belongs to the .rat
records and not to the .t32 sprites a menu draws.

Closes the declaration table, not the question - the focused state could still be
marked in the .rat record, the RATC child stream, or only in the game's code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 03:50:40 +00:00
Sylpheed RE agent
ee65631de8 docs/re: the title-screen oracle — what it settled, and what it refuted
`BACKLOG.md`'s "the declaration table is not a paint order on every screen" had
been reasoned about but never measured. It is now measured, and the entry says
so both ways.

Confirmed: a framebuffer capture of Canary on the title screen shows the
`PROJECT SYLPHEED` wordmarks over the full-screen background, so declaration
order is not the paint order there.

Refuted, and recorded rather than quietly dropped:

* the `0x10`-adjacency first step this entry proposed. The background *is* next
  to a `kind = 0x10` `.prm` on both screens that have one — but on opposite
  sides (`GP_TITLE` puts the background after it, `GP_MISSION_SELECT` before),
  so no rule keyed on the `.prm` orders both. `GP_READY_ROOM` and `GP_OPTIONS`
  cannot serve as the third and fourth witnesses: neither carries a `.prm` or a
  full-screen background at all.
* the placement-region group order and the RATC child order, as above.
* reverse declaration order, by the same capture — it would draw the background
  over the copyright line, which is visible.

And separated out: part of what the bad render showed was not ordering at all
but the pivot/scale defect fixed two commits back. `ui-rat-layout.md` gains that
rule, the correlation evidence, the unmeasured pivot-vs-centre gap, and the
disc-wide demotion of "pivot is exactly half the texture" (49 % agree, 37 % are
off by more than 16 px — `ptcopyright` is 694 px wide in the framebuffer and
`pivot*2` says 618).

Captures: `title-screen-oracle.png` (the game surface cropped out of the
emulator window at (1,45), 1:1, bottom 45 rows clipped by the display) and
`title-screen-composite-2026-08-18.png` beside it.
2026-08-18 16:32:52 +00:00
d9ffc223bc re: RATC "one level deep" is not a limitation -- measured, plus a second dangling asset
INDEX read as if the parser stopped at one level. Measurement says there is
nothing deeper: 2859 top-level bundles hold 18002 children at depth 1 and ZERO at
depth 2, with no blob failing to parse.

The children that are themselves RATC -- the .rat layout records -- are leaves.
They carry no child list and reference their siblings BY NAME (the sprite they
place, and via "opt " their focused variant): 3311 such leaves, every one
embedding sibling names, and 10144 of 10148 references resolve within the same
bundle. That is the same by-name convention used one level up (a screen config
naming .prt components) and one level up again (the movie table naming
<lang>.pak+<member>).

The 4 unresolved references are a disc defect, not a parse gap: pmbase.rat in
GP_STAGE_CLEAR.pak's four language builds places pmbase.t32, and that sprite
exists nowhere -- not a pak member, not a child of those bundles. Second such
dangling asset after SUBTITLE_S12B.tbl, so a reimplementation should skip a
missing sprite rather than call it a decode failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:44:19 +00:00
9228255bfa re: validate the screen layout against the running game -- eight rows, +/-2 px
Booted to the ARSENAL and detected the eight teal category chips by colour, then
compared their row centres against the eight prbtn1..8.rat placements parsed from
eng\prmain_scr.prt. All eight fit

    screen_Y = placement_Y + pivotY(14) + chrome(45)

with residuals -1,-1,-1,-2,0,-2,-1,-2 px. The spacings are the real signature:
predicted 54,56,55,54,56,55,55 against observed 54,56,54,56,54,56,54 -- an
irregular alternating pattern, not a round number that could match by luck. The
one free parameter is the 45 px emulator window chrome, which is not part of the
game, and the residual is centroid measurement noise.

Confirms three things together: the declaration table is the element set, the
placement region gives real screen coordinates, and the pivot composes additively
(pivotY 14 = half the 28 px chip, so placement is top-left as documented).

Explicitly NOT confirmed: the max-dwell rule for animated elements. These buttons
are static -- every keyframe identical -- which is exactly what makes them a clean
ruler. That rule needs an element captured mid-slide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:25:18 +00:00
35e2d1067f re: placement X/Y are SIGNED, the tail word is time, and the resting pose is max-dwell
Dumping the remaining Arsenal components turned up placements reading as
4294966780 and 4294966856 -- negative as i32 (-516, -440). X and Y are signed;
off-screen animation starts are negative, and read as unsigned a reimplementation
would draw them four billion pixels off.

The raw region then showed the rest of it. The trailing word is a TIME, and a
group is an in -> hold -> out animation: prselect_win1 runs
t=4:-516 -> 6:-71 -> 7:81 -> 8:127 -> 23:134 -> 24:134 -> 25:127 -> 27:81 ->
31:-71 -> 1:-516. So NEITHER the first nor the last keyframe is where the element
sits -- both are off-screen. The resting position is the max-dwell keyframe,
(127,155)..(134,155) here, which is where the weapon-list panel actually appears.

This corrects my own note from earlier today, which reported "final=" positions.
Static elements were unaffected (the pause menu's buttons have identical
keyframes) but animated ones were misread. screen_layout.rs now reports the
max-dwell pose plus the full timeline, and both captures are regenerated.

Also seen while dumping: kind 0x2, and a .sbo child (scrollbar) parented to the
list window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:14:36 +00:00
de357ffdc9 re: the screen parse generalises to the ARSENAL, plus kind 0x4 and the component model
examples/screen_layout.rs dumps a bundle's declaration table and placement region
together. It reproduces the tutorial pause menu exactly and reads the ARSENAL the
same way -- 23 elements that match the running game: eight buttons prbtn1..8.rat
at X=242 evenly spaced (the config declares WEAPON_CATEGORIES = 8, and eight
categories are what the Arsenal shows), prexp3.t32 declared SEVEN times at X=726
34px apart (the DATA SHEET rows), prexp1 sliding (726,143)->(1286,143) with
prexp1a parented to it, and prmsg at (151,645).

Two additions to the format:

  kind = 0x4 marks a REPEATED INSTANCE of a sprite -- prexp3.t32 appears once
  with 0x0 then six times with 0x4, each with its own placement. So the element
  name is not a key; the declaration index is.

  A screen composes from named .prt components. GP_HANGAR_ARSENAL.pak has 510
  RATC entries because its config names components (Menu = prmain_scr.prt, etc.)
  and they resolve under the config's own PATH prefix: prmain_scr.prt is absent,
  eng\prmain_scr.prt is present -- the same <lang>+<member> convention the movie
  table uses. A sub-component reads identically: psselect_win1 declares 4
  elements, three parented to element 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:06:00 +00:00
f65387143e re: the UI screen gap is closed -- the bundle also carries every element's placement
Immediately after the declaration table the RATC bundle holds a placement region,
one group per element: a header of (u32 element index, u32 keyframe count)
followed by that many 40-byte keyframes carrying scale/tint/X/Y. The element
index is stated in the header, so the correspondence is read, not inferred from
order.

Tutorial pause bundle: 11 groups for 11 elements, every header index and count
matching the blocks present. The values are self-evidently right -- three menu
buttons at X=546 exactly 70 px apart (288/358/428), title (540,119), message
(451,545), and the eff* sprites carrying multi-position fly-ins.

Cross-checked: pgp_ttrl_btn10.rat places its own sprite at (546,288), identical
to its inline group, so the inline region is the same placement data and it
covers the eff*/deli*/msg elements that have no record of their own.

So a screen is fully reconstructible from its bundle alone: element list and
order from the declaration table, placement and animation from this region,
plus the sprites, with "opt " supplying focused states. This retires the caveat
added earlier today that only the order was known.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:58:21 +00:00
8814df843b re: the RATC declaration table IS the screen's draw list, and +32 is a parent index
The doc's open gap was "where does the screen's draw list live" -- the eff/deli/msg
sprites have no .rat and loop1.rat turned out to be an animation. It is the
bundle's own declaration table, which is NOT the child listing: children are
grouped by type (all .t32, then all .rat) while the declaration table names
ELEMENTS in a plausible back-to-front order and contains exactly the missing set.
For the in-mission pause menu it lists 23 elements ending with loop1.rat, and it
omits the focused button variants -- those are reached via each base record's
"opt " link -- so it is the screen's element set, not an inventory.

Also corrects the entry layout: it is FIVE words after the name, not "u32 x4
flags", and word +32 is a PARENT ELEMENT INDEX (0xffffffff = none). pgpeff02a
carries 3 and element 3 is pgpeff02; pgpeff03a carries 5 and element 5 is
pgpeff03. Verified on two independent language builds, no out-of-range value in
any bundle, and the tutorial bundle (no "a" variants) is 0xffffffff throughout.

Bounded honestly: what is drawn and in what order is now answered; WHERE the
eff/deli/msg sprites land is not -- the declaration entry has a pivot but no X/Y.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:50:13 +00:00
e3f3d55d29 re: ui layout generalizes — the title main menu rebuilds too
Second screen, independent of the pause work: GP_TITLE.pak's ptbtn01..05.rat give
X=542 for all five items and Y=162/242/322/402/482, an 80px pitch where the pause
menu used 70. Measured against the main-menu screenshot the sprite tops sit at a
constant +46px for all five, which is exactly the 45px of Xenia window chrome plus
one — so the record's Y is the sprite's top edge in the guest framebuffer, to the
pixel, on a screen the format was not derived from.

GP_TITLE also splits by sub-screen the way the pause pak splits by context:
ptbtn00 (PRESS A BUTTON), ptbtn01..05 (main menu), ptbtn11..13 (EXTRAS submenu),
pgloading_* (loading screen).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:44:29 +00:00
9a86a09f8d re: ui — the RATC bundle header is the screen's element declaration table
u32 count at 0x14, then fixed 60-byte entries of [name | 4 flag words | pivotX |
pivotY | 0]. The table lists both .t32 sprites and .rat records, so it is the
screen's element list.

Verified: for all 7 .t32 entries in the tutorial pause bundle the declared pivot
is exactly half the decoded texture's dimensions, 7/7 with no mismatch. That also
settles what the pair at 0x50/0x54 of a .rat record is — a .rat simply opens with
one of these declarations.

The language split is visible inside one bundle: the English build's .t32
declarations carry correct English pivots while its .rat declarations carry
Japanese-derived ones, so the sprite table is regenerated per language and the
layout layer is inherited from the Japanese master.

Adds tools/re-capture/ratc_decls.py (parse the table, check pivots against the
decoded textures).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:40:57 +00:00
aaa4ea60b3 re: ui .rat — pivot field, language-independence, PRMD, and a tightened claim
Follow-up decoding on the layout record:

- 0x50/0x54 is the sprite PIVOT: exactly half the texture size on 4 of 5 plain
  sprites. It is a rotation/scale centre, not a draw offset — X/Y remains the
  top-left, which is what actually reproduces the screenshot.
- The records are byte-identical across the English and Japanese bundles: layout
  is authored once and only the sprites are swapped. That explains pgpbtn01's
  pivot implying a 226px texture that neither shipped language has.
- loop1.rat is NOT the screen composition — it is a looping sprite animation
  (3-name table + ~30 keyframes at one position). The eff*/deli*/msg draw list is
  therefore still unfound, and that gap is now stated plainly.
- The 380-byte top-level entries are PRMD primitives: a colour plus four explicit
  corner coordinates for a full-screen quad — the pause dim overlay. The format
  is tag-driven ('opt ', 'PRMD', 'end '), not a fixed struct.

Also demotes one piece of evidence. The screenshot is of the TUTORIAL pause menu,
so only pgp_ttrl_* can be checked against it; the in-mission records mapping onto
the same screenshot under a constant offset works only because both builds share
the 70px pitch and proves nothing. In-mission coordinates are now marked
unverified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:38:04 +00:00
621b9dd6e9 re: crack the UI layout record — the retail UI rebuilds from the disc
Each UI sprite <name>.t32 ships with a <name>.rat companion, and that companion
is the layout record: big-endian u32, a 1280x720 design space at 0x18, and a
placement block of scaleX/scaleY/tint/X/Y. Animated elements repeat the block
with a trailing time field (keyframes); an 'opt ' tag carries a length-prefixed
link to the focused-state record.

Verified in that order, records first: across the four pause buttons exactly one
field varies, stepping 268/338/408/478 at a constant 70px pitch with X fixed at
226; the three items visible in a screenshot of the running game measure
346.0/415.5/485.5, mapping onto those numbers under one constant offset to
within 0.5px. The tutorial build's records are outright framebuffer coordinates,
and compositing its sprites there reproduces the screenshot pixel-accurately.

Also records two mistakes the A/B caught, since a static-only reading would have
shipped both: texture width does not identify a language (Japanese fits English
widths), and the in-mission and tutorial pause menus are separate sprite sets
rather than one set re-packed into slots.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:05:21 +00:00