975d24def26f16cb005d7548ae2ff69bd5e45fe9
241 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
70799488fe |
port: delete the authored blend map for the decoded field, and find a counter-example doing it
PORT-MISSION §3: "When the RE agent later decodes something you had authored, delete the authored entry and let the exporter emit it. That deletion is the measure of progress." This is that deletion. authored/rendering.json's `additive_elements` -- a per-screen list transcribed from the Decoder's per-draw RB_BLENDCONTROL0 log -- is gone. The exporter emits `blend_additive` per element and per nested focus/leaf element from `T8aD +0x04` bit 0x02, and ScreenView reads it there. Both accessor spellings are needed: `ptbtn00f.t32` is in build.sprites while no element carries it as `sprite`, and it is the sharp case -- the plate alpha-over, its own glow additive, adjacent draws on one screen. CHECKED BEFORE THE SWAP, and the map turned out to be a SUBSET, not the answer: 15 elements it called additive the disc agrees with, ZERO contradictions, and 17 MORE the disc marks that it did not. Those include the sweep LEAVES (draw_leaf_for means pteff03/pteff03a are what reach the screen while the map listed their parents) and TWELVE on `title`, where the map was deliberately empty -- so the port has been drawing every title effect with the wrong blend. H6 closes with no capture at all: the JP asymmetry was an artefact of a NAME-KEYED map, and the bit is on the disc for every screen at once. 🔴 AND IT INTRODUCED A REGRESSION, WHICH IS REPORTED, NOT HIDDEN. Against the oracle captures on the GPU: main_menu 10.88 -> 13.02, main_menu_options 11.56 -> 13.57. Deterministic to the digit over three runs, so not sampling noise. Isolated to ONE element, with a control: - main_menu's only newly-additive top-level element is pteff10; - extras gained none and did not move -- the same change on a screen with nothing new moves nothing; - the leaf rule was disabled separately and main_menu stayed at 13.02, so pteff03/pteff03a are NOT the cause. That prediction of mine failed; the rule is restored, being provably neutral here; - title did not move despite twelve newly-additive elements, consistent with verify-capture posing at settle t=198 where those quads are transparent. That is a potential COUNTER-EXAMPLE to a ✅ DECODED claim, and it is a sharp question rather than a guess: their own map lists pteff10 additive on `extras` and not on `main_menu`, and they logged BOTH screens. Asked in BLOCKED.md H6. Shipped anyway, for reasons stated rather than assumed: +2.14 is inside the harness's own ±3.78 capture-phase term for that screen and cannot adjudicate a disc fact; the decoded source is far better evidenced (35 elements, zero errors, out-of-sample prediction 3 of 16); and fitting an exception for one element would put an authored entry back to make one number smaller, which is the move this project keeps having to undo. It is a KNOWN regression, not an unnoticed one. Also settled this iteration, for the Decoder's open question: the port FADES the plate, it does not pop it. Frozen sweep of the plate region -- 210u 0.1457, 216u 0.1573, 222u 0.1727, 228u 0.1900, 236u 0.2142 -- a clean monotone ramp across the declared 214->236. So t=236 is the port's COMPLETION, not its onset, and the 0.367 s "late plus a pop" reading does not apply. Not settled: whether pteff10 has a counter-example; H1's repeat half; the four red verify-screen rows; and finding 3, which still has no cause now that units/s is settled at 60. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AHUQvXGyNcKonSEWsgWcX |
||
|
|
7dd754f8e4 |
port: bump the formats pin to formats-pin-2026-09-01 for the blend accessors
What I wanted from the new state: `ui_layout::sprite_blend_additive` and its three siblings, which expose `T8aD +0x04` bit 0x02 -- the decoded blend mode. Until this tag, `Element` surfaced `kind` (+40), `parent`, pivot, keyframes and `focus_link`, all from the RATC record, and nothing reached the sprite's own header. That is why the port has been carrying an authored additive map keyed by SCREEN NAME, transcribed from the Decoder's per-draw RB_BLENDCONTROL0 log, which cannot answer for a screen nobody drove the game to -- the Japanese menus being the case that raised it. Both spellings are needed, not one: `sprite_blend_additive` takes an &Element, and `blend_additive_by_name` takes a sprite name, because a button's focused variant is reached through focus_link and `ptbtn00f.t32` is in build.sprites while no element carries it as `sprite`. That is exactly the sharp case in my own map -- press_start's only additive entry IS ptbtn00f. Pin bumped as its own commit, per PORT-MISSION §2. Nothing else in this commit; the exporter does not read the new accessors yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AHUQvXGyNcKonSEWsgWcX |
||
|
|
2ce3e188a2 |
port: the one load-bearing thing in the denominator thread, checked against the port
Their substantive point was not about counting: a static record still declares a cycle length, and a nonzero +0x08 against a largest keyframe time of 0 is a real disagreement. That is a rendering question for this port and it had not been asked. Scoped to GP_TITLE, the archive the port exports: 65 nested records, 20 declaring a cycle while every pose sits at t=0, and 0 of those with any element carrying more than one pose. So the declared cycle is visually inert on every one of them. A record whose elements each hold a single pose renders identically whether looped or held, since there is nothing to move between. The port holds nothing still that the disc says moves, and that is now measured rather than assumed. It includes ptbtn11, ptbtn12 and ptbtn13 -- EXTRAS' own buttons -- declaring 120-unit cycles. Had any carried two poses, the port would have been holding a menu button the disc says animates, on the one submenu P5's gate walks. The check cost one scan and the answer could have gone the other way. That is the thread's yield stated honestly. Three rounds of correction ran over an interpretation that was never load-bearing -- the offset stood on both scans throughout, so the cost of being wrong at each step was a paragraph. What came out of it worth having: the population distinction, and this check, which exists because they pushed on what the 1530 MEAN rather than on how they are counted. Their framing of why it was safe is the caveat I would attach to repeating it: nothing the port depends on moved at any point. That made three rounds cheap. It does not make three rounds a good default, and I would not have spent them if a shipped value had been waiting on the outcome. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
479a01fb82 |
port: correcting my own correction -- none of the 1530 is a question without content
I told the Decoder their denominator held 1530 questions that were never asked: records with no timed keyframe, where 'does +0x08 equal the largest keyframe time' has no meaning. I did not check that and it is wrong. Of the 1530 excluded, ZERO have no timed keyframe at all and all 1530 are timed with every pose at t=0. Every one has a largest keyframe time; it is 0. So the question is well-formed there and the answer is 'not exact', because a static record still declares a cycle length and a nonzero +0x08 against a largest time of 0 is a real disagreement rather than an absent one. That makes their 49.6% defensible rather than mistaken. Two statistics over two populations: 92.3% of records whose largest keyframe time is > 0, and 49.6% of all nested records including static ones. Neither is the corrected version of the other. I framed mine as correct and theirs as an artefact; the truthful statement is that they answer different questions and both need their population attached -- which was my own point one message earlier, applied to their number and not to my reading of it. Their cause diagnosis is still right about the mechanism, max() returning Some(0) rather than None, but 'records with no timed keyframe' describes zero records on this disc. The mechanism is real and the population they attributed it to does not exist. Third-order and worth naming: they corrected an argument, I corrected their denominator, and this corrects my characterisation of what was in it. Each step was checkable in one scan, and each of us stated the interpretation confidently while only the number had been measured. The numbers have agreed throughout; every disagreement has been about what they were counting. What survives untouched, and is the only part the port depends on: +0x08 equals the largest keyframe time exactly where that time is nonzero, +0x04 does so 0% of the time under either denominator, and the offset identification stands on both scans. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
6048c70bee |
port: the 92.3-vs-49.6 gap is entirely the denominator, and my number lacked its population
Reproducing my offset result, the Decoder reported the same discrimination over 3311 records against my 1781, with exactness 49.6% against my 92.3%, attributing the difference to a scan that 'takes every pak and requires a timed keyframe'. Both scans are described identically, so at least one was narrower than its own description. Counting my survivors per filter: 3311 records declared by parse_build, 3311 within bounds, 3311 carrying the RATC magic, 3311 parsing as nested builds, and 1781 with at least one timed keyframe. So 3311 is the count BEFORE the timed filter. The arithmetic closes it: 1643/1781 is 92.3% and 1643/3311 is 49.6%, their figure exactly. Same numerator. Their denominator includes the 1530 records with no timed keyframe, where 'does +0x08 equal the largest keyframe time' has no meaning -- max t is 0 and every one counts as not-exact by construction. So their stated filter is not applied, and 49.6% is not a weaker version of 92.3% but 1643 successes over a denominator containing 1530 questions that were never asked. The discrimination is untouched: +0x04 gives 0% under either denominator, so the offset conclusion stands on both scans. And my own number needed a qualifier it did not carry. 92.3% is 'of the records where the question is meaningful', not 'of nested records', and I have quoted it bare since 2026-08-30 including into screen.rs's doc comment -- a population-scoped statistic reported without its population, the same shape as a negative reported without its reach. Qualified in place. Two agents, one number, and the disagreement was entirely in the denominator; neither of us was wrong about the disc. A cheaper failure than the offset one and a more common one: the numerator agreed to the unit, which is what makes a denominator mismatch invisible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
b0bb57c0e6 |
port: my falsifier never identified the offset -- the half I called a formality did
Their struct-layout control found that a homogeneous repeated table type-checks at every field boundary, so an interior test carries no information about phase: 69 of 70 records passed under both shifted alignments. Their rule is that the evidence for a field order lives at the first and last record and nowhere else. That aimed at my +0x08 loop-length control, an interior test of exactly that kind which I re-ran as confirmation. Re-run at the neighbours: +0x04 gives 0 violations and PASSES the falsifier, +0x08 gives 0, +0x0c gives 1287 violations at 72%. The falsifier rejects +0x0c and accepts +0x04, whose word is >= max keyframe time in 100% of records. So the falsifier does not identify +0x08. I published it as the load-bearing half -- an animation cannot restart before its own last pose, so a wrong reading should produce violations, and none exist in 1781 records -- and a wrong reading one word to the left produces none either. What identifies the offset is the half I described as merely guarding against triviality: +0x08 equals the largest keyframe time EXACTLY in 92.3% of records and +0x04 does so in 0%. No unrelated word reproduces that coincidence. The value is right and my argument for it was wrong. Second time this week the weight was on the wrong leg: last time a count was taking credit for an exclusion argument, this time the falsifier was taking credit for the exactness statistic. Both were cases where the impressive-sounding control carried nothing. Their boundary rule does not transfer literally -- a per-record header has no first-and-last-record phase question -- but the underlying point does: an interior consistency check is satisfied by any reading that is internally consistent, and 'internally consistent' is what a wrong offset into a regular structure usually is. Their observation about when I found my extractor inflating my own backlog is worth keeping: while clearing it, not while building the tool. Clearing put me in contact with the individual items; building had only put me in contact with the rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
2759f3e719 |
port: their docstring point found three stale claims in my code
Their sharpening of my harness-note finding: a why in an authored file has a convention demanding a citation; a docstring has nothing, travels with the code, and reads as authoritative. Their instance was ring_row.py's calibration, wrong, sitting under every focus finding they had sent me, found by accident. Swept mine for numbers I had corrected in DECISIONS.md. Three live instances, each contradicting my own log. video.rs asserted '28 % of S00A's frames presented and 47 % of ADV's' as measured; boot.gd asserted that the same numbers 'refuted the claim outright'; dialog_rows.rs said 'by three routes'. All three were retracted days ago in the log and never in the code -- the percentages came from contended runs and the counter is an upper bound that goes vacuous once the engine outruns the stream, and three routes became two, one compound. verify-transcode-fidelity was the only one already correct. Third time this pattern has bitten me, and it is the one audio.json's own why warns about: a correction that does not reach the artifact a consumer reads has not been made. First was loop_why shipping a refuted story into manifest.json, second a BLOCKED row, this is code comments -- the worst of the three because they sit beside the thing they describe. So the class is now checked rather than swept: the retracted numbers are register rows carrying the propositions they asserted, and check-claims immediately failed on my own corrections quoting them unmarked. The next stale number of this kind fails a run instead of waiting for a sweep. What it does not cover is a docstring number that was never corrected anywhere. The register holds only what I have already retracted, so it catches propagation failures rather than wrong numbers -- their ring_row.py case would still have gone undetected here, because nothing had retracted that calibration. Their closing observation is the honest limit: the only thing that has actually caught these is one of us reading the other's sentence for its own sake, which is not a filter and does not scale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
7416445e90 |
port: audit my own multi-leg claims -- the load-bearing one holds, and now says why
The Decoder's sharpest addition: a conclusion with two supports reads as better evidenced than one with a single support, so if one is decorative the appearance of redundancy is itself the misinformation -- a reason to strip a weak second argument rather than leave it as colour. Unlike the domain-crossing sweep, this pattern has a tell: claims that announce their own leg count. Six in my authored data. The load-bearing one is audio.json's 'Static code, disc census and runtime all agree'. Read literally, two of those three could be one comparison. The sentence beneath says BGM_103.slb's declared wave sizes are byte-for-byte what the XMA probe saw at the menu -- a disc-to-runtime match, not two independent confirmations. It is a genuine third leg only if the census excludes alternatives: were another bank to carry the same two sizes, the byte match would not distinguish BGM_103. Measured with this port's own reader: of 32 readable BGM_* banks on the disc, exactly one carries waves of that size. The census does exclude, the static-code leg names the cue independently, and the three legs stand. The why now records that reasoning instead of the count -- it said 'all agree', and it now says why agreement from those three is not one fact stated three times. The audit did not find a defect. It found an assertion of independence that had never been checked, in the entry carrying P6's most load-bearing value. Reach: I checked one of the six. The other five -- 'two derivations', 'three routes', 'both agents independently', and two bare uses of 'independently' -- are unaudited, and saying so beats letting one verified case stand for the set. Same convenient-bound shape I named two iterations ago, and naming it is apparently the only thing that has ever got one closed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
e25f5e2a6f |
port: verify their closing of the 37 -- conclusion holds, one supporting leg does not
I wrote that nothing rewards closing the 37 pairs that differ without a button-count mismatch, and that a reader could not tell whether the bound was respected or merely convenient. They treated that as a prompt and closed it. The decisive evidence reproduces exactly from this port's reader: adjacent entries carry two different stages -- 10/11 is stage 10 against 02, 12/13 is 11 against 03, 14/15 is 12 against 13. Those are DLG_STAGE_TITLE01..16 from their table, and a translation of one dialog cannot be a different stage. So the language reading is refuted for the 37 as well, and the whole 63 reduce to one fact with no residue: adjacent GP_DIALOG entries are unrelated dialogs. Their second argument does not reproduce. They offered sprite counts differing 20 against 16 as evidence of a different amount of text. Counting .t32 elements here gives 42 vs 34, 28 vs 28, and 30 vs 22 -- entries 12/13 are EQUAL, so that leg does not hold uniformly, and my absolute numbers do not match theirs at all, which means we are counting different things. Neither discrepancy touches the conclusion, since the stage numbers settle it without help. Reported because a conclusion resting on two legs, one of which does not reproduce, is worth knowing about even when the other leg is sufficient. It is the same shape as the EN/JP pair withdrawal one step out: the leg carrying no weight is the one that went unchecked, by them when offering it and by me if I had taken the conclusion without re-running it. Process note recorded: we had both agreed in writing that the bound would stay open, and that agreement was the last thing protecting it. What broke it was saying out loud that nothing rewarded closing it. Not a mechanism to rely on -- it worked once because the other agent read it as a challenge rather than an excuse. Their statement of the limit stands sharper than mine: both sweeps find asides that cross domains, and an aside correctly about its own domain and still wrong has no tell in either corpus. Recorded as a limit rather than a backlog item, because filing it as work implies a route. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
896166caaf |
port: refute the language-sprite reading of the GP_DIALOG residual
The Decoder recorded a residual as odd rather than understood, with a plausible untested reading: GP_DIALOG has 140 entries against a 70-record table, adjacent pairing gives identical element-name sets on only 2 of 65 pairs, and the proposed explanation was that dialog text is baked into language-specific sprites so EN/JP entries differ by construction. They flagged its hole themselves -- it would explain the 63 that differ and leave the 2 that match needing their own explanation. It is refuted, and by a count rather than an impression: 26 of 65 adjacent pairs differ in BUTTON COUNT. Two languages of one dialog cannot, since a locale changes the glyphs on a button and not how many there are. At least 26 adjacent pairs are two different dialogs, so the language reading cannot be what explains the 63. The names agree once looked at rather than the ratio: entries 6/7 are py_ranking_NEXT_btn1/btn2/msg/win against py_ranking_JUMP_btn1/btn2/btn3/msg; 8/9 are py_ranking_* against pzeff*, a different subsystem; 10/11 are pzstg10_* against pzstg02_*, a different stage. It inverts the puzzle rather than solving it. The 2 that match do not need a special explanation; the 63 never needed the language reading. Adjacent entries here are unrelated dialogs, so the 2:1 ratio against the table is a coincidence of counting rather than a pairing -- consistent with their own finding that halves-pairing matched 0. Not claimed: that entries 0/1 and 2/3 ARE EN/JP pairs. Identical element sets is the signature in GP_TITLE and here is equally consistent with a duplicate. And 37 of the 63 differ without a button-count mismatch, so for those the language reading is unsupported rather than refuted. What is refuted is the reading as an explanation of the 63, which is what it was offered as. Their scoping answer closes the other half: their rival filter was btn, the same as mine, so the two disc-wide scans have identical reach and the zero is a real zero from two readers. Their note that a disc-wide negative should report its filter scope is the right generalisation of the known-positive point -- the whole content of the claim is an absence, so both the reader's liveness and its reach have to travel with the number. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
7f8d7128cf |
port: the reach we both recorded is closed, re-run with a broader filter
Yesterday both agents wrote down the same limit: another four-button dialog with the same rows would be indistinguishable by this evidence. The Decoder searched for one and found zero rivals disc-wide. Re-run here with this port's reader: 2859 builds across 33 paks, exactly 2 matches within 6 px of 259/329/399/469 -- the EN/JP pair -- and no rivals. My filter was deliberately broader than the claim needed: any element whose name contains 'btn', not only 'pcbtn', so a rival under a different naming convention would still have been caught. Narrowing by name would have answered a smaller question than the one asked, which is the method-versus-subject trap in its cheapest form. The run carries its own known positive: fewer than 2 matches would mean the reader cannot see the incumbents and its zero would mean nothing. That is the liveness discipline applied to a disc-wide NEGATIVE, where it matters most, since the entire content of the claim is an absence. The name is now backed by a table entry rather than an inference from a string list: every DLG_ name in the image sits in a 12-byte record spanning 0x820A0A2C to 0x820A0D68, 70 names and 70 records with none unmatched, and DLG_SELECT_DIFFICULTY is id 2000. Still unbound, and it is the load-bearing gap: nothing connects id 2000 to a pak entry. The table gives name-to-id, the disc gives a unique build, and no pointer joins them. The tie is uniqueness plus the oracle capture, not a binding, so if a rival build ever appeared the identification would go with it. flow.json records it in those terms rather than as a decode. Their closing observation is about method rather than result and is worth keeping: confirming the part I could check and refusing the part I could not is what produced the scan. Agreement would have ended it and so would a challenge to the whole claim; the useful move was taking it apart and handing back the half that was still open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
19987daafe |
port: the register records propositions now, and DIFFICULTY is a dialog
The register held twelve bare phrases, and that shape had two demonstrated costs. A phrase is not a claim: '1 of 3 streams' is dead here and a live warning in the Decoder's corpus, so a bare row cannot say which proposition it killed and a peer hit was unadjudicable in principle. And the bareness made THEIR parser lie -- a reader looking for a quoted string in each row found none, built an empty claim list and reported a clean table. My data shape made their instrument fail silently, which is not something they could have fixed from their side. Every row now reads 'phrase :: what it asserted', recovered from the corrections themselves. The phrase stays the search key; the proposition is for whoever has to judge a hit. Two failures while making the change, both from the data shape moving. The register began reporting itself as twelve unmarked assertions, because the rows used to sit inside the file header's marker window by accident and a proposition pushed them out; widening the window would have been tuning a constant until a failure went away, so the heredoc and only the heredoc is excised before scanning. And the control harness broke on its own colon-delimited cases, since rows now contain ' :: ' -- a data-shape change breaking the harness that guards the data, the same coupling in miniature. Then back to the disc. DIFFICULTY is a DIALOG, DLG_SELECT_DIFFICULTY, GP_DIALOG entries 2/3 -- re-derived with this port's own reader rather than taken on their word: entries 2 and 3 are the only builds in that archive carrying pcbtn00-pcbtn03, rows 259/329/399/469, spacing exactly 70. So the four external destinations are NOT uniform: three open GameParts and one opens a dialog. Q6's count-match holds as a count, and a rule read off it would be reading across two categories. They sent that count with disc support yesterday and weakened it themselves today; flow.json records it at the weaker strength and goto_name is now DLG_SELECT_DIFFICULTY. Their reach is carried: entries 2/3 are identified by geometry, not by a name-to-entry binding, so another four-button dialog with the same rows would be indistinguishable. My re-derivation confirms the geometry and does not name the screen. Also recorded, because it is truer of this port than of them: their note that recent exchanges were almost entirely about instruments. My last several iterations produced a harness self-test, a liveness sweep, peer-head, a peer-scan, a known positive for it, and register propositions. Every one was a real defect and several were in checks I had shipped days earlier -- but they kept catching things in each other, and a tool that fixes a tool that guards a tool is still not a screen the port draws correctly. Not resolved by declaring a ratio; this iteration ends on the disc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
1e8c457a8b |
port: make the frame count permanent, then correct what I read from it twice
The Decoder's closing point -- the inference is cheap and the measurement looks expensive right up until someone does it -- is actionable, so the probe I reverted is now permanent. The exporter records each transcode's duration and frame rate in the manifest (probed from the file it wrote, not the source), and every video run prints what it showed against what the media holds. An instrument that has to be added before the question can be asked will not be there the next time somebody reasons instead. Then the instrument corrected me twice more. It is an UPPER BOUND, not a count. It counts engine frames, and the engine renders the UI at its own rate: on a quiet box ADV drew 6480 frames across a 4123-frame video, 44 fps against the media's 30. Above that crossover it constrains nothing, and '157% presented' is the counter used outside its range. The report now says so instead of printing a percentage. So 'the player skips, heavily' is not supported. At 8.3 engine fps under contention S00A could not have shown more than 28% -- a valid bound under contention and nothing more. Quiet, the bound is 88-90%, permitting anything from no drops to a tenth. And the 720p-versus-432p contrast is refuted -- the finding I sent them twice. I reported ADV +6.7% against S00A -0.5% and built 'heavy decode falls behind, light keeps up' on it. Quiet, both run +6.7...+6.9%. The -0.5% was a contended run in which the player dropped frames to hold schedule. I was measuring which run happened to share the box and reading it as a property of the resolution. What survives is sturdier than either: playback runs +6.7%...+6.9% long on this container, five runs, both videos, quiet, resolution-independent. Three corrections in three iterations, all mine, all the same shape: argued from an absence; measured and over-read; then found the measurement was taken under a confound I introduced myself by running the suite alongside it. Their rule needs a companion -- ask what the quantity can be skipped by, and ask what else was running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
0e41f5381d |
port: take formats-pin-2026-08-30b and stop owning the +0x08 read
The tag was cut within the iteration, so screen.rs now calls ui_layout::loop_length_units and its local RATC guard and byte read are deleted. One line, as predicted -- and the doc comment promising that deletion is the only reason a temporary reading did not quietly become permanent. A pin bump moves the whole crate, not one function, and this pin is recorded load-bearing, so both commits between the tags were read before taking it: |
||
|
|
6a8b80faaa |
port: the contract I read is 3185 lines shorter than the contract
docs/port/HANDOFF.md on main is 926 lines, last touched |
||
|
|
8da453478d |
port: a refuted-claim register, enforced by check-all
The Decoder's audit of their own corpus found four refuted claims standing -- including one they had corrected to me, agreed with, and written a METHOD entry about, without landing it for a full iteration. A hand audit finds what is there on the day it runs; it does not stop the next one. check-claims is a register: every occurrence of a refuted claim must carry an explicit [refuted] sentinel within 400 characters. It found four more unmarked occurrences than my manual pass had, including one in authored/audio.json. The marker is a sentinel rather than a keyword because the first version's every failure was a quotation inside a correction whose wording lacked the keyword. The temptation was to widen the window until they passed -- tuning a threshold until the answer comes out right, in the tool built to catch that. 21 quotations marked by hand; proved it fails by removing one. Also fixes the Decoder's other finding in my corpus: BLOCKED's voice row had a struck heading with three sentences below still asserting in the present tense. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
2cd94aca4c |
port: grep the corpus for the claim -- one refuted sentence was still shipping
Applying the Decoder's rule to my own corpus, on four claims refuted this fortnight. Two were properly marked. Two were not. The exported why still told readers the dropped chunk 'IS understood: the TAIL of the kept stream' -- an interpretation refuted when the resolver turned out to start 238 packets inside the first stream, so what matched end-flush was a start-truncated simultaneous stream. It was wrong twice over: S00A's dropped chunk is the SILENT one, so the sentence described a case that was not present -- the second template-message-true-only-for-one-case defect in this file, which suggests the first fix taught me nothing general. 'The port's boot is known too fast on both' was still standing unmarked. I wrote the withdrawal as a new section and left the original untouched, so a reader arriving there first got the dead answer. Annotated in place. One false positive: BLOCKED matched inside my own correction, which quotes the refuted claim to name it. Naming a refuted sentence keeps it greppable, which is the price of not deleting it, so the audit needs a read of every hit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
328c8a6f96 |
port: duration confirmed at 0.2 percent, window refuted, loop start now an explicit field
They timed the wraps instead of converting them -- a probe stamping read_offset on arrival, three wraps observed, each from its own loop_end to its own loop_start, both contexts wrapping at the same instant. Cycle 61.81 s against the 61.93 authored here: 0.2 percent, from a wall clock between decoder events versus an autocorrelation that never touched the wave. The window is wrong: loop_start is 11.6 percent into the stream, about ten seconds, so this export has the right duration over the wrong window -- replaying the intro every cycle and omitting the tail the game plays. Not re-cut, on their instruction: the exact start is unmeasured and linearity is refuted by a 4.4 percent rate variation within one stream. But loop_end_s alone silently asserted a start of zero, so the entry gains loop_start_s, authored as 0.0 and flagged wrong, with -ss applied before -t so the pair is (start, duration). Proved before it is needed: loop_start_s=10 yields -ss 10 -t 61.93 and a 61.930 s output. Restored to 0.0, export byte-unchanged. My smooth-join check gains a use I could not have anticipated: it explains why a wrong ten-second window went unheard, because a cut near a zero crossing is smooth wherever it falls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
039ba725a4 |
port: the menu bed loops at 61.93 s -- my 3.4 s seam was ours, not the game's
Measured on 240 s parked on the real menu: no seam at all (zero runs >=0.3 s below median-18 dB in 232 s), and the loop is at 61.93 s, not the summed wave's 87.744 s length. Two instruments agree -- top correlation lag 61.909 s with r=-0.009 at 87.750, and slice location showing playback wrap at 61.93 three times. The final ~25.8 s, the fade-out and trailing silence, is never played. This port had authored loop: restart, measured the resulting 3.4 s of near-silence off its own Master bus, and reported it as the price of the missing loop point -- in BLOCKED and in a message to the Decoder. The measurement was correct and the attribution was wrong. Second time this fortnight I have measured something real and assigned it to the wrong side. Godot loops whole files, so the loop region has to BE the file: BgmSpec gains loop_end_s and the bed is trimmed to 61.930 s. Verified over 131 s on the port's own bus -- 5 near-silent windows, no run >=0.2 s, against 36 windows and 3.4 s. Also records their withdrawal of '8 of 10 three-chunk regions': that audit file was truncated mid-list with no summary line, so the multichannel-specificity claim is unsupported and my 25 stands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
b7c2e9b2de |
port: apply the measured positional weights, keyed by byte size
ADV's three chunks map onto the Decoder's contexts exactly, each a declared byte_size plus the 60-byte RIFF header. authored/audio.json gains voice.stream_weights keyed by SIZE, and the exporter weights only when every kept stream matches, falling back to the divisor otherwise. The key is the check: two weeks ago these same sizes did not fit the region the resolver returned, which is how the 238-packet late start was found. Applied positionally the weights would have gone onto the wrong streams silently. S00A matches nothing and keeps the divisor. ADV mixes at 0.4142/0.2929/0.2929, -2.87. Unlooked-for confirmation: the filter folds chunks 0 and 2 from two live channels and chunk 1 from one, found independently by live_channels, matching their claim that ctx1 is the only stream with a silent channel. Also files an UNEXPLAINED regression: verify-menu-audio's dead-press check now reports DIFFER across three runs, diverging at 0.085 s with different durations. Not diagnosed, not the voice change, and left failing rather than silenced. The check's premise is cross-run bit-determinism, which is what made it strong and also what makes it brittle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
a821cdbffd |
port: move to formats-pin-2026-08-30 -- the voice export is complete
The late start was a second condition on the start filter, end - s < 1_500_000, only within one bank. ADV's predecessor trailer sits 3618816 B before end, so it was rejected and start fell back to anchor, a TOC offset rather than a stream boundary -- which is why it hit regions over 1.5 MB and never single-stream ones. ADV region 3114352 -> 3618816 B, streams 2 of 3 -> 3 of 3, complete, dropping zero chunks. S00A 1 of 3 -> 2 of 3 with the third digitally silent. The runtime no longer prints an incomplete line for either. Also fixes the warning crying wolf: S00A still read KNOWN INCOMPLETE over a 93.694 s stream of exact zeroes. Exported gains content_waves and the warning, console line and manifest field all key on kept < content rather than kept < present. Second time in two iterations this warning was wrong in the cautious direction -- over-warning is what makes the next real warning unreadable. S00A is a second movie in the predicted direction: kept went 1 -> 2 because a chunk that was a different duration now matches at 93.694 s. Not independent ground truth, but a different asset and the outcome was predicted first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
64b76b0892 |
port: the runtime now says what the voice export is missing, at the moment it plays it
The manifest has carried the gap for weeks and the runtime printed '+ voice ADV' and nothing else. A reader of manifest.json gets a paragraph; a person LISTENING gets clean dialogue and no way to learn a stream is absent. NEW GAME already announces the screens it jumps over; audio had no equivalent. ManifestAudio gains -- one line naming what is KNOWN missing, absent meaning nothing is known rather than nothing is wrong -- and MenuAudio carries it so _play_video can print it. Verified on the boot's ADV and P7's S00A. The first version of the message was FALSE for one of the two assets: it said 'one is a start-truncated stream', which is ADV's story, where S00A's dropped chunks are digitally silent. Caught by reading the output for both, which I nearly skipped because the ADV line was obviously right. Now states the counts and points at the entry's why. A message generated once from a template but true only for the case it was written against is harder to see than a wrong number -- the sentence is well-formed and confident in both places. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
e2d889bf97 |
port: voice export carries every qualifying stream; a unity sum was refused by check
#4 answered: ADV.wmv carries ONE audio stream and it is WMA Pro 5.1, not XMA, so the movie's own track is the bed and the three streams are additional. Solving capture = 0.600 x movie + residual gives three residual signals at three positions, with LFE reproducing to -115.73 dBFS -- where nothing is added the decoders agree exactly, so the rest is added content. presentation: all keeps every equal-length non-silent survivor -- ADV 2 of 3, S00A 1 of 3 -- and the warning now keys on kept < present rather than on more than one existing. A unity sum was tried first and check refused it at +2.62 dBFS. The BGM stems precedent did not transfer: those are stems of one signal, these are positions in a field whose downmix weights sum to one whatever the assignment. Dividing by the count preserves the total and claims nothing about placement; ADV lands at -3.1. That is the OPPOSITE of the two divisor bugs already in this file, where a silent input sat in the divisor. Divide-by-N is not right or wrong in itself. Also carries their census correction: the ALSA permutation does not apply, the map is the identity, and the '82% silent' channel was LFE. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
e647368ddd |
port: FORMAT.md declared the port's own export invalid; assert audio.json's stems
Audits the open format spec against the validator by breaking each documented requirement. Four of five caught. The fifth is the doc's error: FORMAT.md said check refuses any peak >= 0 dBFS, where the implementation is kind-dependent -- a bgm is a sum we produced and is refused at full scale, an se/voice is a disc wave whose lossy decode overshoots and is allowed to +1.0. The doc was wrong about our own export: confirm ships at +0.18 and the ADV voice at +0.31. A consumer implementing a validator from FORMAT.md would have rejected a valid tree -- the file that exists so someone can check our work without trusting us. Corrected, with the +1.0 marked as a judgement. Also closes the last unread authored value: audio.json's was carried as stems_why only, so serde ignored the value. Now deserialised and asserted in the exporter (only sum is implemented); the assertion is proved to fire. Files, not fixes: a failed export leaves a tree with no manifest, and every tool then says 'is that an export tree?' -- which nearly made me conclude the validator was checking nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
606eee8f23 |
port: check the five MODDING rules, and label the generated files in the asset tree
MODDING.md calls modding a constraint on the exporter TODAY and nothing verified it -- the same shape as the black hold, skipped[], stop_bed and --focus. All five rules pass, so check-modding is a guard rather than a fix, and it is proved able to fail: a stripped .cmd header, a bogus.bmp, and one orphaned PNG each exit 1. It found one thing: the .cmd encode-cache sidecars sat in the modder-facing tree with nothing saying what they were. They now carry a header. The header is excluded from the cache key so rewording it does not re-encode four minutes of video, and the sidecar is refreshed whenever its text differs rather than only on re-encode -- otherwise a header change could never reach an existing export. Also partly answers my own question to the Decoder: there is no general capture-path floor, because the port matches live-title-press-a at 0.00093% full-frame and 0.000% across the band. The 0.301% is specific to that pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
3c71962698 |
port: the PRESS (A) plate could not be drawn at any instant -- four faults, and a misquoted number
1. --time= was silently ignored on any screen with a settle window >= 30 units: pose_at overwrote the requested instant with settle_instant. ScreenView.frozen now marks an explicit instant and skips both clamps. 2. press_start's settle window was [0,214] -- the dead stretch BEFORE the plate exists -- so its settle instant was t=107, where the element is alpha 0. The exporter now rejects intervals in which nothing is visible. title keeps [160,236], the interval the Decoder's draw stream confirmed. 3. My authored looping_focus_records entry for press_start/ptbtn00 drew a dim focus record INSTEAD of the plate's own sprite: max 0 vs max 252.5. Deleted -- an authored guess that overrides a decode with a worse answer is removed. 4. verify-capture passed --time=5.9617 for the title and it was never applied. Every title figure it has printed, including the 0.26% quoted to the Decoder, was measured at the settle instant under a note claiming t=357.7. Both rows now pose by omission and the note matches. title is 0.21% honestly; splashes unchanged at 0.01%. The boot's end artifact now contains the plate (region mean 95.7 vs 33.6). Corrects last iteration's BLOCKED row, which had the entry's effect backwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
5393c0be02 |
port: test backdrop coverage per instant -- scale animates, and the disc proves it
The Decoder found forced_backdrop judged coverage from declared size alone. pbafc.prm declares 844x600 at alpha ff and draws ~17x18 px at 2%x3% scale -- a rule reading declared size would call it screen-covering. scale_at interpolates on the same ramp as the fade, and coverage is folded into the opaque-instant test: an instant counts only where the element is BOTH alpha 255 and covering. The previous code tested the two halves at different times. The size prefilter deliberately no longer rejects, since an element scaled above 100 could cover from a smaller declared size. No verdict moves: 6 forced before and after, 16 screens validate, oracle figures identical to the digit. Nothing in GP_TITLE has a non-100 scale on any keyframe. It is in because the data that would break it exists, not because it failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
7ffcf743a2 |
port: P6 gate verified with sound on the bus; tighten the backdrop guard to a positive primitive test
verify-menu-audio records the Master bus over the P5 walk under the Dummy driver. A dead press is bit-identical to the bed alone; all three cues match their exported wave in the recording with margin over a bed-only control; the cue order matches the script order, which the correlator was never told. The first version of this tool counted envelope bursts above a multiple of the bed and gave 4 cues on one run and 0 on the next from the same script. Replaced with template matching, which has no tuned constant. Cue LENGTH is deliberately not asserted -- the bed masks the tail and I nearly filed that as a defect. Also acts on the Decoder's .tbm self-refutation. No port verdict is affected -- all six forced elements are .prm solid black, and GP_TITLE has no full-screen .tbm at all -- but the guard was sprite.is_none(), a symptom test of the same shape as the one they say fixed their symptom not their cause. Now role == primitive. Six verdicts identical, 16 screens validate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
0653230983 |
port: implement the Decoder's forced-backdrop rule; two screens were black for their whole life
build_12 and build_15 rendered mean 0 at every instant of a PLAYING timeline, and verify-screen scored both OK -- two renderers sharing implied_layer_key, comparing nothing against nothing. Implements the constraint as a post-pass over ui_layout::derived_paint_order, with both of the Decoder's limits copied verbatim: layerless elements only (a sprite's element alpha says nothing about its texture's coverage) and NOT a name heuristic (palogo_eff0 is named like an overlay and paints first). Both controls reproduce: palogo_eff0 forced first, pteff00 still last on all four title screens at exactly 2 opaque instants. Splashes unmoved against the oracle at 0.01%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
4226505222 |
port: pose a settled screen at ONE instant -- title 1.81% -> 0.26%, splashes to 0.01%
The Decoder's finding, applied. `rest()` returns each element's last hold keyframe chosen independently of every other element: right for anything that ends the screen settled, exactly wrong for a transient. The title's ptlogo_back2eff1 is a two-frame flash (0 until t52, 255 at t54-56, 0 by t58), so its last hold IS the flash peak and rest() left it burning -- five of them, drawn at once. The settled instant is the longest interval containing no keyframe time, over a bundle's TOP-LEVEL elements. Reproduced here before adopting: title [160, 236], midpoint 198, the Decoder's number to the unit. The top-level restriction is what makes it match -- including the ptloop leaves gives [269, 540]. AGAINST THE ORACLE: title 20.92 RMSE 1.81% -> 14.61 RMSE 0.26% publisher_logo 9.05 0.75% -> 2.17 0.01% developer_logos 8.86 0.33% -> 3.05 0.01% main_menu 0.08% -> 0.08% window too narrow extras 0.19% -> 0.19% window too narrow Seven times fewer differing pixels on the title, seventy-five times fewer on the publisher splash, whose differing region is now a 13x18 box. The largest correctness gain this port has had, and none of it is mine -- it is a decode computed from the keyframe table with no reference to any capture. APPLIED ONLY WHERE THE WINDOW IS WIDE, and the bar is not invented. This export's widths split with nothing in between: 214, 190, 145, 76, then 12, 12, 8, 4. The bar is 30 units -- the Decoder's disc-wide census puts the knee there (30% of bundles >= 30, 42% under 10, the latter mostly loop* fragments meant to be in motion) and this export's screens sit 4x either side with nothing between 12 and 46. Two independent populations agreeing on where to cut. Checked unbroken: boot pacing unmoved, scripted walk runs end to end with focus restored. Also recorded: my "34 focus-record elements, only 2 varying" is right for GP_TITLE and reads as a fact about the format -- disc-wide it is 210 varying, 202 with rest() at the peak, concentrated in the paks a wider port reaches next. And their sharper framing, which I have adopted: a pulsing element has no resting pose at all, so rest() is MALFORMED rather than mis-answered on one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
5dbc9aeac0 |
port: delete exit_ramp_units, invert the format's own rule, and guard a scale-0 leaf
FOUR THINGS, and the first is what MISSION section 3 calls the measure of
progress.
DELETED `exit_ramp_units` AND `exit_ramp_seconds`. They were authored because the
disc had no time slot on a group's final keyframe, so the ramp into it was the
one unknown duration per screen. Under the corrected record layout that keyframe
does not exist -- a group is an 8-byte header then frames x {u32 time; 36-byte
pose} and every pose is timed. VERIFIED DEAD BEFORE DELETING: setting it to 9999
(166 s) moved the boot's transitions by 0.04 s, which is wall-clock jitter, and
both uses in ScreenView are gated on a condition that no longer fires on any of
the export's 866 keyframes.
INVERTED THE FORMAT'S OWN RULE. `check.rs` enforced "the final keyframe has no
`t`; the disc has no time slot there" and FORMAT.md stated it. Both are now
backwards, and the validator fired 150 times on a re-export. I had not run
`check` between pinning the tag and measuring against the oracle -- the pixel
harness was green while the format validator was failing on every screen with a
multi-keyframe group. A correctness harness does not replace a format one; they
fail at different layers.
GUARDED A SCALE-0 LEAF, which the Decoder hit in its own renderer: its leaf
branch marked the element drawn unconditionally while the blit returned early on
zero scale, so a scale-0 leaf suppressed its parent and blanked the element --
live on all four loading screens. This port did not have the bug only because
authored/rendering.json happens not to list pgloading_loop5. That is an accident
of a gate written for another reason, not a defence, so `_draw_leaf` now reports
whether it drew and `_draw` falls back to the parent.
ISOLATED THE PACING QUESTION rather than leaving it as a suspected regression.
Legacy association: publisher 4.70 agrees, developer 3.92 DIFFERS. Corrected:
publisher 4.26 DIFFERS, developer 3.62 agrees. Both misses are ~0.03 s outside a
composite bound. The association traded which screen is marginally out; it did
not regress the pacing.
Bumped the pin c -> d for the parser and audio changes. Its headline renderer
change does not reach this port: sylpheed-cli builds from the workspace crate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
|
||
|
|
42474642e0 |
port: pin formats-pin-2026-08-29c -- the knob I tested last iteration was retired
I tested the wrong switch. SYLPHEED_KF_TIME_SHIFT is a superseded partial fix: it
got the association right but LEFT POSE 0 UNTIMED, which is exactly why the
untimed keyframe appeared to move from last to first. The real correction is the
DEFAULT in the tagged crate, with the old reading behind SYLPHEED_KF_TIME_LEGACY.
So last iteration's five rows measured a mismatch against a knob nobody should
use -- I suspected they were not decisive, I did not suspect the knob was retired.
THE CONSEQUENCE IS MUCH SMALLER THAN I BUDGETED. A placement group is an 8-byte
header then frames x {u32 time; 36-byte pose}, so pose 0's time is the group's
lead-in word and every pose is timed. Measured on the re-export: 866 keyframes,
0 untimed. `pose_at`'s "the final keyframe carries no t, so give it a synthetic
time" premise does not invert, it DISAPPEARS -- dead code rather than wrong code,
which is why nothing needed re-deriving. And the leaf now reads t=0 x=-639,
t=150 x=-39, t=540 x=1521, giving x=781 at t=355: the Decoder's predicted
top-left, and the 1300 px discrepancy is gone.
Pinned by tag, which is what MISSION section 2's tagging rule is for. BLOCKED was
wrong in both directions -- "cannot be taken yet" AND "only when that branch lands
on main". It arrives when the tag is pinned.
COST STATED: sylpheed-cli builds from the workspace crate, so until this reaches
main the exporter and the reference renderer read different decoders and
verify-screen compares two eras. verify-capture is unaffected -- it compares
against oracle captures and never touches the CLI. Revert to the path dependency
when the tag is an ancestor of main.
Oracle: publisher_logo 1.00% -> 0.75%, developer_logos 0.39% -> 0.33%, and
extras' differing region COLLAPSING from 736x525 to 398x295 at the sweep position
-- the residual localised onto the one element still in question. title unchanged
at 1.82%, now posed at t=355, the Decoder's FITTED sweep time. t=390 measures
1.65% and picking it would be fitting the pose to the score.
REFUTED, MINE: "ptlogo_eff2 is the single drawn element at a scale that is not a
whole multiple of 100%". That census was parents-only; the 45 leaves hold
thirteen distinct non-whole-multiple scales and 125% is among the rarest at two.
The claim's real content was "the only one the port draws" -- about my element
set, not the disc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
|
||
|
|
9f6959c7ca |
port: implement the decoded leaf composition -- and it does not close the 1.82%
The Decoder decoded the rule I refused to guess: draw the leaf on its own timeline, do NOT multiply the parent's alpha in. Multiplying is refuted rather than unsupported -- at the fitted time the parent has expired, so leaf x parent predicts zero for both quads and the sweeps would be invisible. They are drawn. Implemented: `_draw_leaf` runs the leaf unclamped, like the spinning ring and for the same reason -- held at its own rest.t the leaf sits at x=1521, entirely off the right edge, so `holding` would delete the sweeps rather than settle them. AND IT CHANGES NOTHING MEASURABLE. The title is still 1.82% against the oracle: 1.82 at t=261, 1.81 at t=355, 1.79 at t=420. At t=355 my interpolation puts the leaf's top-left at x ~ -324, off-screen left, where the Decoder's model puts the quad's CENTRE at 981. Those cannot both be right, and it is not something to tune away -- it is a disagreement about how the leaf's keyframes become a placed quad, most likely in the pivot and the rotation about it. Handed back with both numbers. So: the exporter no longer drops the data, the composition rule is implemented as decoded, and the port's largest oracle gap is exactly where it was. Fixing the export was necessary and not sufficient. TWO FLAGGED ELEMENTS DELIBERATELY NOT DRAWN, in authored/rendering.json with reasons. title_jp/ptlogo_eff2 (parent 125%, leaf 100%) is the same shape and is the element DECISIONS has recorded since P1 as the largest render disagreement -- but the Decoder said plainly "I have not tested it", and drawing it would extend a decode past the case it was fitted on. pgloading_loop5's leaf is scale (0,0), and scale-0 is one of the three historical failures this corpus names. Neither can be adjudicated here: title_jp has no oracle capture, and verify-screen compares against a renderer that draws no leaves at all, so ANY leaf drawing increases that divergence whether right or wrong. Its max went 155 -> 232 when they were drawn, and that number is not evidence in either direction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
a298a95eb8 |
port: the exporter never opened an element's own .rat leaf -- 45 elements, and the title's 1.82%
The Decoder overturned the elimination I was most confident about. I ruled out
the ptloop sweeps because "399x180 at (441,270), keyframes hold position
constant". That is the PARENT's record. The geometry is in the leaf.
ptloop01 parent: scale (100,100) rot 0, fixed at (441,270)
LEAF: scale (100,600) rot +30, x sweeping -639 -> -39 -> 1521
ptloop02 parent: scale (100,100) rot 0, fixed at (441,270)
LEAF: scale (100,800) rot -45, x sweeping 1721 -> 1111 -> -839
Two ~1080 and ~1440 px quads leaning opposite ways and sweeping across the frame,
against two 400 px sprites drawn upright and static in the middle. That is
exactly the signature I measured -- darker centre-left, brighter right, nearly
cancelling -- and the GPU capture puts their centres at x ~ 467 and 992, the two
cells where my signed difference peaked.
`ui_layout`'s own doc comment said it: "the rotated quads come from its two
nested .rat leaf records, which the census never opened". Neither did this
exporter -- it opened a leaf in exactly one place, `highlight_name`, for focus
records.
IT IS NOT TWO ELEMENTS, IT IS 45: every button on every menu (the benign case,
where screen.rs already knew the leaf duplicates the parent and the parent wins),
the four loading screens' pgloading_loop*, and title_jp's ptlogo_eff2 -- which is
the element DECISIONS has recorded since P1 as the largest render disagreement in
the export, and which has a TWO-element leaf. A lead, not a conclusion.
EMITTED, DELIBERATELY NOT DRAWN. One `read_leaf` closure serves both the new path
and the focus path, because a second copy is how this would go missing again.
ScreenView ignores the data: parent and leaf each carry their own alpha ramp over
a different span (parent 0->255 over t=70..238, leaf 255->0x80->255 over
t=150..600), so how they compose is a decoding question, and drawing on a guess
would replace a visible 1.82% gap with an invisible wrong one. verify-screen
confirms nothing moved.
Additive blending is refuted -- the Decoder tested T8aD +0x04 bit 0x02 and "every
measure worsens", and the export carries no blend field because none has been
found (no RB_BLENDCONTROL in the per-draw capture). My hypothesis from last
iteration is dead.
This makes the port's biggest oracle gap the same item as the rotation question
already standing with the human: sylpheed-cli deliberately does not rotate, which
is why both renderers show it, and MISSION's "Needs a human decision -- rotation"
now has a number: 1.82% of the title's pixels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
|
||
|
|
c1113b8cd6 |
port: the menu music was 3.52 dB quiet -- a bank header was being summed as a stem
Checking my export against the Decoder's declared XMA1 durations turned up a
defect of mine that has been shipping since P6.
`export_bgm` summed every sub-wave `media` returned and scaled by 1/n. Decoded
and timed, all three banks have the same shape:
BGM_103 sub-wave 0: 10300 B -> 0.009 s, peak -inf 1: 87.744 s 2: 87.744 s
BGM_102 sub-wave 0: 10300 B -> 0.009 s, peak -inf 1: 37.482 s 2: 37.482 s
BGM_001 sub-wave 0: 10300 B -> 0.009 s, peak -inf 1: 173.809 s 2: 173.809 s
Sub-wave 0 is DIGITALLY SILENT in all three, and 10300 B is 10240 plus a 60-byte
RIFF wrapper -- 10240 being exactly the bank header the Decoder's census
identifies. Counting it in the divisor put every real stem at 1/3 instead of 1/2:
3.52 dB on all the menu music since P6. Dropping a silent input is arithmetic,
not a decoding decision. Measured after: main_menu.ogg -7.69 -> -4.20 dBFS,
+3.49 dB against 3.52 predicted.
THIRD INSTANCE OF ONE DEFECT: a silent chunk in the voice sum, a silent channel
in the mono fold, now a silent sub-wave in the music sum. Each invisible to every
check except a level, and each time the divisor was computed from how many inputs
there are rather than how many carry signal. That is the shape, not the bug.
Closes a red row open since P6 -- "sound_bank_riffs returns three sub-waves where
Q10's census says two". The census was right, and this corroborates the Decoder's
|
||
|
|
bb9626defe |
port: the game decodes all three voice streams at once, and two baseline rows were comparing blank frames
TWO FINDINGS, one mine and one handed to me, and the second retires a premise I built on twice. THE P1 BASELINE HAD ROWS THAT PROVED NOTHING. `build_12` and `build_15` render pure black in BOTH renderers -- mean 0, max 0 -- so the difference is zero and `verify-screen` scored them `max 0 over3 0 OK`, the strongest verdict it has. Two of sixteen rows were comparing nothing against nothing. Worse than a missing test, because a missing test is visible in the count. Cause isolated by a control, not by reading: `build_00`/`build_01` are the same loading screen minus three elements and render fine (mean 1.913, max 214.5). The dressed variants add `pgloading_eff00`, a 1280x720 primitive resting OPAQUE BLACK at t=38 inside its own opening black hold, with no layer key so paint order puts it last. The rule I was about to write -- "rest.t before the last timed keyframe is the pathology" -- was killed by running the census first: 152 of 212 elements in this export have rest.t earlier than their last timed keyframe. It is the norm. What is actually unusual is the CONTENT, and its reach is one: `pgloading_eff00` is the only element in the export whose resting pose is a fully opaque full-frame quad. One instance is not a rule, so the renderer is unchanged and the HARNESS is fixed: a blank pair now reports BLANK -- both renderers drew nothing; this row proves nothing. `status` is untouched, so an unrelated DIFFERS still fails. THE VOICE EXPORT IS KNOWN INCOMPLETE. The Decoder booted Canary with --xma_param_probe and the game decodes ALL THREE streams CONCURRENTLY, in three XMA contexts whose byte sizes match the disc payloads exactly. So "three presentations of one take, pick one" is refuted by the running game and the question I had been arguing -- WHICH presentation -- has no answer. This one no census could have caught. Every measurement was right: the streams are equal-duration, one is silence, one is 0.60x another with the residual 26.8 dB down. The frame around them was wrong, and the file says ChannelMask 0x0002 on all three. It took the running game -- which is the mission's own sentence arriving in practice. BEHAVIOUR HELD DELIBERATELY. An equal-gain 1/n sum of channel pairs is not a downmix either -- MISSION section 6 pins an explicit matrix for exactly that reason -- and summing cost S00A 6.02 dB when one stream was silence. Swapping one guess for another on a message is what produced this entry twice. What changed is that the wrongness is now LOUD, because this failure sounds like success: one stream is clean audible dialogue. A top-level manifest warning per movie, the console line, and the authored entry all say `1 of 3 streams`. "They are 5.1" is recorded as the Decoder's HYPOTHESIS with its own counter-evidence attached, and nothing builds on it. What settles it is asked: a recording of the game's own output over ADV through the null sink, which turns channel roles into a fit against an oracle. Refutation attempt, survived: the Decoder's loading-screen variant map. Entries 0/1 carry 7 elements and 12/15 carry those seven plus baseeff, eff00 and loop5 -- exact in count and identity, and it is what made build_00 a control. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
92f1436836 |
port: withdraw the dual-mono generalisation -- the measurement stands, the rule does not
I argued `highest_rate` had no case because ADV's higher-rate presentation is dual-mono while its louder one is mono-in-stereo, so the extra bytes buy a duplicated channel rather than fidelity. The Decoder tested that disc-wide over the 28 three-stream cues: the stream-3/stream-2 size ratio runs min 0.0778, median 1.2565, max 2.9163, sd 0.5057, with only 12 of 28 within 15% of 1.0, and declared rates scatter with them. A 37x spread is not a duplicated channel. The CHANNEL MEASUREMENT STANDS -- ADV chunk 1 is mono-in-stereo and chunk 2 is dual-mono at -8.318574, this port's own decode, which the Decoder could not re-run and did not dispute. What fails is the step from one asset to the format. NOTHING IN THE EXPORT CHANGES. `loudest` is a per-asset content rule -- it reads the peak of the streams in front of it -- so a scattering structural ratio cannot undermine it. What changes is the REASON, in four places: authored/audio.json's presentation_why, the selector comment in audio.rs, BLOCKED.md's row, and DECISIONS.md. The honest statement is narrower: `highest_rate` was never refuted, it was never argued for, and neither is `loudest`. That is why the entry is marked CHOSEN rather than measured, and why one capture deletes it. Recorded on the pattern rather than just the instance: this is the third claim of mine in two iterations that generalised a single-asset observation, after "the chunks are two stems" and "everything the sequencer paces off rest.t is late". All three were true of the thing I looked at. The failure is reaching for the rule a measurement would imply if it held everywhere and writing that down in the same breath as the measurement. Also noted, not mine and not affecting export_voice: S12B's three streams are byte-size identical, and BIRD_224 is three-stream while not being a movie cue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
81ea5cb324 |
port: settle times measured -- the pacing was already right, and my own red flag was half wrong
THE TRANSCODE CACHE HAD NEVER HIT. `video::transcode` has carried one since P4 -- a `.cmd` sidecar with the command, the source size and the channel count -- and `main.rs` clears the output tree wholesale immediately before the check, deleting the sidecar and the file it stamps. Six exports in this session paid the full Theora encode and produced five byte-identical files, roughly 48 minutes. Nothing reported it, and nothing could: a cache is silent either way and the only symptom is a wall clock that looks like the job being slow. The wipe now spares `video/` and `prune_videos` deletes anything in it this run did not claim, so the wholesale guarantee is kept rather than traded. A re-export is 20 s. SETTLE TIMES, MEASURED, and they refute more of my row than they confirm. The principle holds -- the title's rest.t is 251 units = 4.183 s where its art finishes at ~2 s -- but "everything the sequencer paces off that landmark is therefore late" does not. Measured the port the way the game was measured, by VISIBLE SPAN rather than arrival-to-arrival: publisher wordmark port 4.25 s game 4.297 / 4.604 / 4.370 developer logos port 3.50 s game 3.508 / 3.503 / 3.366 black hold port ~0.25 s game 0.2 - 0.3 title -> plate port 2.000 s (declared 120 units) game 2.247 s Dead on. My earlier reading compared the port's transition timestamps against the game's visible spans, which differ by the exit ramp plus the black hold -- the whole of the discrepancy I was about to chase, and the same definitional trap that cost this corpus 0.48 s on the plate delay. Nothing in the sequencer is changed. `dwell_seconds` stays null, now for a measured reason rather than an absent one: `timing.json` said "if a capture ever times the real boot, this is where that number goes", and the answer is that nothing goes there. Not authored, deliberately: an (A)->menu dwell, which measures 3.763 s and contains a 1.53 s guest load stall on a cold cache; and the menu build-in and (B)->title, which rest on one run where the port is already within ~0.1 s. THE VOICE PRESENTATION MOVED TO authored/, because the recommendation behind it was withdrawn as self-contradictory and the choice is now unambiguously mine. `voice.presentation` = `loudest`, and the reason to switch is a measurement, not a preference: ADV chunk 1 is MONO-IN-STEREO and chunk 2 is DUAL-MONO, so chunk 2's extra bytes encode a duplicate channel rather than fidelity. That explains the byte-rate difference and removes the only argument for `highest_rate`. ADV's dialogue now exports at +0.3 dBFS instead of -8.7, which is the SE bound's documented decode overshoot on a wave mastered at full scale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
d3b37b2a1e |
port: a voice region is three presentations of one take -- keep one stream, sum nothing
Third reading of the same bytes in one session, and the first one that is
decoded rather than inferred.
1. concatenate -> 359 s of dialogue for a 137 s movie. Dead on measurement.
2. sum as Q10's two stems -> refuted here: S00A's second full-length chunk is
DIGITAL SILENCE and ADV's is 0.60x the first with 26.8 dB of residual. That
claim was mine, and the Decoder had already adopted it before I tested it;
it is withdrawn in both places.
3. keep ONE stream. Decoded disc-wide by the Decoder, counting stream starts
inside every inter-descriptor span: 258 spans hold one stream, 28 hold
three, nothing holds two. So 359 = 84.55 + 137.32 + 137.32.
Summing was therefore wrong a third time, and for a third reason: a take plus a
0.60x copy of itself is ~4 dB louder and coloured, not a mix of parts. The filter
is now `[0:a]anull` plus the mono fold -- no gain applied at all.
`check` moves `voice` off the strict peak bound as a consequence. It sat with
`bgm` because it was a sum this exporter produced; it is now a single wave off
the disc, mastered near full scale -- ADV's louder presentation measures +0.0003
dBFS at source -- so refusing that would be refusing the disc's own mastering.
The leading chunk is dropped as a DUPLICATE, not a truncation. It is this
movie's own dialogue (the Decoder, 17 of 17) and I measured it to be the TAIL of
the kept stream: sliding envelope correlation r=0.998 / 0.932 with the lag flush
against that stream's end, controls 1.000 self and 0.289 for a different movie,
confirmed in the sample domain at 16.7 / 23.2 dB of residual.
STILL OPEN, and flagged rather than absorbed: WHICH presentation to keep. Highest
byte rate is the Decoder's recommendation and not a decoded field, and on ADV it
selects the QUIETER of two (-8.3 dBFS against 0.0). Said in the manifest with the
consequence, so it is visible and reversible. A capture of the intro with
dialogue audible settles it.
Not converted, and the Decoder has since withdrawn the ask with a better reason
than mine: its 504464 B anchor constant is structural, not proportional --
identical on all 17 regions, and a proportional prediction lands within 8 bytes
on ADV while being 4305 B out on S00A.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
|
||
|
|
4817e5ea9e |
port: fold only the channels that carry signal, and measure what the leading chunk actually is
TWO DEFECTS AND ONE MEASUREMENT, all from verifying the previous commit rather than from reading it. Channel 2 of both voice streams is DIGITALLY SILENT -- peak -inf over the whole file. The voice is a mono recording carried in a nominally stereo stream, and averaging it with silence cost 5.94 dB. The doc comment directly above the code that did it warned that "a stereo matrix applied to a mono voice track is not an error, it is a -6 dB attenuation that nothing reports", and then the code checked the DECLARED channel count instead of the content. `live_channels` now measures which channels carry signal and averages only those. Three defects this iteration were the same shape: a silent chunk in a sum, a silent channel in a fold, and a pan matrix naming channels that do not exist. Each is an input contributing nothing while still counting in a divisor, and none is visible in anything but a level. THE LEADING CHUNK IS THE TAIL OF THE FULL-LENGTH ONE. The Decoder settled by byte-span analysis that it is the movie's own dialogue, 17 of 17 -- killing its own hypothesis that it was an in-mission line -- and asked whether dropping it is a truncation, having no XMA1 decoder. Sliding envelope correlation with overhang allowed and normalised over the overlap: ADV r=0.998 at +52.8 s, S00A r=0.932 at +25.6 s, against controls of 1.000 (self) and 0.289 (a different movie). Both lags put chunk 0 flush against the END of chunk 1. Sample domain, lag refined to one sample then a scalar best-fit: residuals 16.70 dB and 23.15 dB below target. So dropping it removes a DUPLICATE and the exporter was right for a worse reason than it gave. The manifest note is NOT rewritten to claim that -- the structural conclusion belongs on the Decoder's page, not in my manifest -- but it no longer equates the chunk with BGM_103's third sub-wave, which a disc-wide census showed is a different structure, and it now says in words that the omission must not be read as junk removal. Not converted, deliberately: the Decoder's 504464 B anchor constant. Bytes per second is not constant even inside one region -- chunk 1 is 1118268 B and chunk 2 is 1171516 B for the SAME 137.324 s -- so any figure in seconds off it would be invented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
cb8d77febc |
port: withdraw my own "two stems" reading of a voice region, and stop summing silence
The Decoder asked me to decode a voice region's leading chunk -- it has no XMA1
decoder in its container -- and the decoder run refuted a claim of mine that it
had already adopted into `docs/re/structures/voice-region-leading-chunk.md`.
I wrote that a region's two equal-length chunks are HANDOFF Q10's decoded
two-stem shape. Equal duration was a SHAPE match and I carried the music census
across on the strength of it. The content does not support it:
S00A chunk 2 is DIGITAL SILENCE -- 4497300 samples, peak -inf.
ADV chunk 2 is 0.60x chunk 1, best-fit scalar, residual 26.8 dB below the
target: about 95% of its energy is a -4.4 dB copy of the first chunk.
That cost real level. Summing chunk 1 with silence at 1/n put S00A's dialogue
6.02 dB down for nothing -- the exported file peaked at -16.2 dBFS against a
source chunk peaking at -4.2. `export_voice` now drops a digitally silent chunk
before the sum, which is arithmetic and not a judgement about content.
WHAT ADV'S NEAR-DUPLICATE SECOND CHUNK IS REMAINS OPEN AND IT IS STILL SUMMED.
Whether the game plays both is a decoding question, 26.8 dB of residual is not
nothing, and dropping a chunk because it correlates with another would be
answering it.
The leading chunk, answered as far as a measurement goes: ADV region + 1392, 394
packets, 84.553 s, stereo 48 kHz, peak -2.48 dBFS, 6 silent gaps over 0.4 s
totalling 45.3 s -- 54% silence, the same duty cycle as the full-length chunks.
Speech-structured, so not a header and not padding. "Cutscene or mission" is an
identification and this agent has no ears and no oracle; envelope correlation
peaks at 0.768 at the last lag in the search range, which is where a statistic
lands when it has found nothing, and it is not an answer.
Not taken yet, and said so in BLOCKED: the discriminator should be
`bank_header_len`, not a duration tie. This exporter never used `riffs.len()`, so
it already handles both of the Decoder's cases, but a tie is an observation and
`bank_header_len` is decoded. It switches when `c1f3608` reaches `main`;
`sylpheed-formats` is a path dependency and merging another agent's topic branch
is not the port's to do.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
|
||
|
|
53a93e2e9e |
port: the intro had no dialogue because the voice is a separate asset, and I concatenated it wrongly first
A human play-test heard music under the boot intro and no voices. The obvious reading -- the 5.1 fold dropped the centre channel -- is wrong. `ADV.wmv` carries music and effects only; a cutscene's voice is a separate continuous XMA stream in `sound.pak`, bound to the movie by the manifest in `tables.pak`. Nothing was dropped. The exporter had never been asked for it, so every fidelity measurement in AUDIO-VERIFICATION.md would have come back clean. `audio::export_voice` resolves it with `media::resolve_movie_voice_region` and never by filename: `RT01A`'s voice lives inside `VOICE_ADV.slb`, so a name match is correct on exactly the two movies this port would have spot-checked. Decoded, not authored -- so it runs outside the `authored/audio.json` block. THE FIRST VERSION CONCATENATED THE REGION'S CHUNKS AND WAS WRONG. It produced 359 s of dialogue for a 137 s movie. Decoding and timing each chunk shows two of them equal to six decimals and each spanning the whole movie -- HANDOFF Q10's decoded two-stem shape on a second asset kind -- so they are summed at 1/n. The error was visible only because the first version recorded the decoded length against the movie's instead of clamping to it; the clamp `media`'s own doc comment invites, and which `sylpheed-viewer` applies, would have produced a file of exactly the right duration containing the wrong audio. The dropped leading chunk matches no duration in its region and is NOT closed here. It is the same signature as `BGM_103`'s third sub-wave, already open in BLOCKED.md, now corroborated on an independent asset kind. Raised with the Decoder; the manifest names every chunk dropped and its length. Also in this commit, and separable: * `--skip-at=SECONDS` -- `--script` structurally cannot press during a movie, because `_script_settled` waits while `_player != null`. That is why "does (A) skip the intro" had been read out of the source rather than measured. * MISSION section 6 pins a 5.1->stereo matrix and this exporter has shipped a different one since P4 -- the same weighting, 7.65 dB quieter -- and said so nowhere. Re-measured with the right instrument (float decode, whole file, count the samples that would clamp, not a peak reading): the pinned matrix puts ADV at +4.26 dBFS on 4406 samples, while S00A never clips. So the pin overloads one movie and the constant is over-broad for the other. NOT changed -- the level of a mix is what section 6 reserves to a human. The export now carries a warning with the numbers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
3a4c6ac9df | Merge remote-tracking branch 'origin/main' into auto/port-p6-audio | ||
|
|
d1685d67c9 |
viewer: show where a cutscene's voice actually is, and let you hear it
Some checks failed
The Cutscenes window printed the voice token as text and offered no way to play it, which left the most confusing thing on the disc invisible. The movie voices are one continuous XMA stream chunked into VOICE_*.slb entries whose boundaries do NOT match the cutscene cues, so the bank named after a movie need not hold that movie's audio. Measured, on the retail disc: ADV region 433930240..437044592 inside VOICE_ADV.slb name honest S00A region 452798464..455499120 inside VOICE_S00A.slb name honest RT01A region 437044592..437345648 inside VOICE_ADV.slb NAME LIES RT01A's voice sits in bytes belonging to the entry named after the intro movie. A viewer that played the name-matched bank would be confidently wrong for exactly the cutscenes where it matters, and would look right on the two that are easiest to check. So the window now shows BOTH locations -- the named bank with its byte range, and the resolved region -- and states plainly whether the name is honest, highlighting it when it is not. Play routes through the movie form of RequestAudio, which resolves the region rather than reading the bank. Static data only: sound.pak and tables.pak, both on the disc. |
||
|
|
2ad839460c |
port: P6 -- the menu has sound, and the BGM I "chose" was decoded all along
The three Static.slb cues and the menu bed now export to Ogg Vorbis and play.
`sylpheed_formats::media` does the assembly; nothing in port/ has heard of XMA.
Three things this milestone got wrong before it got right, all recorded in
docs/port/DECISIONS.md because the corrections are the useful part:
1. The cue offsets were a Rust `const` in the exporter. They are MEASURED, not
decoded -- a measured value compiled into the exporter is a measurement
wearing the costume of a decoded field, and nobody deletes it because nobody
can see it. They are authored/audio.json now.
2. I picked BGM_001 and wrote a careful `why` calling the choice arbitrary. The
menu's music is BGM_103, and it is in HANDOFF at
|
||
|
|
65cefa74c3 |
monorepo: one repository for the decoders, the port and the corpus
Some checks failed
Merges the Godot port into the reverse-engineering repository, preserving both
histories -- 1019 commits of corpus plus the port's 31, brought in by subtree
merge and then moved into place so git can follow each file across the rename.
The reason is not tidiness. The two-repo split forced the exporter to depend on
the decoders by pinned revision, and that created a whole class of failure that
now disappears: a sha reachable only from a topic branch, orphaned by a
squash-merge, breaking a fresh checkout silently at build time. It also forced a
live read-only mount of one agent's working tree into another's container, which
is why a contract file could move mid-iteration. With a path dependency, a
decoder change and the exporter change it requires land in the same commit or
not at all.
Canary stays separate: it is a fork tracking upstream.
New structure for the long term:
docs/game/ how the game is NAVIGATED -- menus, modals, prompts, alerts,
and in-game flight. Written so nobody rediscovers it. Mostly
open questions on purpose; the in-game tutorials are the
resource for the flight half.
docs/port/MODDING.md
modding as a constraint on the exporter TODAY, not a later
feature: one logical asset in one file (the disc splits nearly
everything, and resolving that is the exporter's job), names a
person recognises, PNG/OGG/OGV/JSON only, base-and-overrides so
re-exporting is always safe, provenance in every file.
data/base + data/mods
generated tree and drop-in overrides, both gitignored
exchange/ transient inter-agent files, deliberately outside history
docs/agents/ the team protocol
Both the README and the navigation doc lead with the correction that cost the
most: the oracle is the real game under Xenia Canary. Reborn's renderer is a
hypothesis under test, it has been wrong, and treating it as ground truth
propagated into three documents and both agents before a human caught it.
Scripted modding stays possible without being built: no screen name is hardcoded
in GDScript and there is no native code in port/, which is what Godot Mod Loader
needs to be able to substitute behaviour later.
|
||
|
|
7eeae3006a |
re(ui): the focus ring SPINS, the game draws it, and the leaf owns the f record
Three things, all from parsing ptbtn0Nf.rat as a build. 1. THE RING SPINS. Its two keyframes differ in exactly one field: rotation_deg ramps 0 -> 360 with position, scale, alpha and tint all constant. A spin in place, the same shape as the GP_BUNK example already recorded. 2. THE ORACLE CONFIRMS THE GAME RENDERS IT. In the OPTIONS-focused capture the ring's bright head sits in a completely different angular position from the sprite's own -- caught mid-spin. This is a SECOND independent confirmation that rotation_deg is drawn, now on a different screen and a different element from the ptloop sweeps, and it raises rotation's priority: it is not a title-only concern that sits off-screen at rest, it is the main menu's focus marker. NO ANGLE IS QUOTED. A brightest-region centroid says ~250 deg, but the control refuses that precision -- rotating the sprite by a known 30/90/180/270 and re-measuring gives errors up to 19.8 deg. What survives the error bar is that a <=20 deg error cannot manufacture a ~250 deg displacement. 3. WHICH PLACEMENT WINS -- correcting this page's own earlier caveat, which said to use the leaf only for elements the parent does not declare. Right for a BASE record, wrong for an f record: the parent declares NO element for ptbtn0Nf.rat at all (zero of build 5's 16), so the f record's placement comes from its leaf for BOTH elements, label included. The label's (-7,-7) is load-bearing -- the f sprite is 13px larger per axis and -7 keeps them concentric (535+96/2 = 583 vs 542+83/2 = 583.5). Corroborated against the oracle: the focused-minus-unfocused region is x 505..703, and the leaf predicts a right edge near 707 where the parent reading predicts 714. Also exposes UiBuild::records (name -> (offset, size) of a nested .rat leaf). Nested records were parsed into a PRIVATE map, so a consumer holding a UiBuild could not locate a leaf's bytes at all -- which is exactly what blocked the port from reaching the ring. |
||
|
|
d110cf38c7 |
media: expose se_wave_riff -- the menu's SE cues, assembled where the format lives
The port is forbidden from reimplementing media assembly and Static.slb is exactly that case: no RIFF, no seek chunk, no XACT container, just a packed run of whole 2048-byte XMA1 packets, so a wave is defined only by (offset, packet count) and the header has to be synthesized. That step now happens once, in the crate that owns the format, instead of in each consumer. `slb::xma1_wave_riff` wraps raw packets; `media::se_wave_riff` looks the bank up and reads just the packets asked for. Both reuse the existing synth_xma1_fmt / build_riff, which are already byte-identical to what tools/re-capture/ slb_extract_wave.py writes -- so this is exposure, not a second implementation. It reads a TARGETED range rather than the whole bank, and that is load-bearing: Static.slb is the ONE entry of sound.pak's 9 519 whose declared extent runs past the end of the extracted segments -- by exactly 616 768 B -- so reading it whole fails outright on this extraction. Every cue we need is in the first few hundred KB. Recorded rather than worked around silently. Verified as an artifact, not a compile: all three cues decode through ffmpeg to mono 48 kHz PCM at 0.533 / 0.344 / 1.016 s, non-silent (rms 2085 / 2985 / 4327, peaks 29813 / 16973 / 32767). The refusal path is exercised in the same run -- an impossible packet count is rejected rather than returning a short stream, because a truncated XMA decodes to plausible-sounding garbage. Also adds docs/re/captures/ORACLE-CAPTURES.md: an index of the nine canary framebuffer captures already in this repo, and a plain statement that THEY are the reference and `screen render` is not. |
||
|
|
b21c8e4118 |
re(ui): the focus ring's position is decoded -- a .rat leaf parses as a build
The port needed ptbtneff01.t32's placement and was about to author it from an eyeballed PNG measurement. It does not have to: a `.rat` leaf needs no new reader. Its first 32 bytes have a bundle header's shape -- "RATC", 0x3c declaration-entry size at +4, element count at +20, design 1280x720 at +24/+28 -- so ui_layout::parse_build reads it unchanged. The control is the base record, whose position is known independently: the parent screen reports ptbtn01.rat resting at (542,162), and parsing the leaf alone returns ptbtn01.t32 at (542,162). It reproduces all five buttons. Positions are absolute design-space top-left. The ring rests at (500, 156/236/ 316/396/476) for buttons 1-5 -- a uniform (-42,-6) from each button's own rest, identical in the Japanese bundle. The bright label is a uniform (-7,-7). Two things recorded rather than smoothed over: a leaf's placement DUPLICATES the parent's rather than being relative to it, and the two copies are not always byte-equal (ptbtn04's parent says y=401, its leaf says 402) -- the parent is what compose honours, so the leaf is the source only for elements the parent does not declare, which is exactly the ring. And `screen render --focus` is blind to the ring for the same reason the port's exporter was: el.focused is name-based on top-level elements and neither walks into the leaf. |
||
|
|
f817dd5939 |
re(ui): the 60 nameless RATC children are frames, not children -- .tan decoded
Closes the reach caveat the `opt ` name fix left behind: 60 of 18 002 RATC children carry no `opt ` block, and it was not established whether they lack one or sit past our 128-byte window. Neither. They are not children. `examples/ratc_optless_children.rs` re-runs `ratc::parse`'s own guards over the disc and reports which one fired: all 60 are "tag beyond the window", none is rejected by length, gap or charset, none is child #0, and all 60 live in six bundles of one archive. Within a bundle the distances back to the nearest tag are an exact arithmetic progression, step 60 600 -- ten different records finding the SAME tag, because there is only one. Reading a bundle directly: children 1..10 are equal-size T8aD blocks under a single `opt ` name, `pb_f15_eg_anm.tan`. `.tan` is a FRAME SEQUENCE. One block declares the resource; its payload is a run of T8aD frames. Disc-wide, over all 18 718 `opt ` names in all 33 paks: a RATC bundle names exactly six kinds of resource -- `.t32` 14 756, `.rat` 3 311, `.prm` 367, `.tbm` 224, `.sbo` 54, `.tan` 6. Six `.tan`, ten frames each = 60, the entire population with nothing left over. The negative is closed, not narrowed. Consequence recorded but deliberately not fixed: `ratc::parse` over-reports there, listing a `.tan`'s frames as anonymous children. Nothing in the menu milestone reads a `.tan` -- it occurs only in GP_READY_ROOM, which S1 ruled out -- so no screen the port draws changes. Also a METHOD entry for this container OOM-killing `slb_leading_segment_disc` under default test parallelism (SIGKILL, no assertion; 8/8 pass with --test-threads=1). |