4817e5ea9e61909c4d33bebae0ca88f8854dbec4
1086 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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
|
||
|
|
9c55ca4801 |
port: the voice is in the mix, proved by a null control that needed no test code
Three things verified against the re-export, none of them by reading the source. THE STEM READING IS CORROBORATED BY A NUMBER IT WAS NOT FITTED TO. Chunks are selected as "the longest, and everything tying with it" -- the movie's own length is not an input to that rule. The result now matches the picture to -0.114 s (ADV) and -0.085 s (S00A), where the concatenated version was +221 s and +161 s. THE VOICE REACHES THE MASTER BUS. `+ voice ADV` in the log proves only that a stream was found and played. The control is MODDING rule 4: 140 s of silence at data/mods/audio/voice/ADV.ogg shadows the exported file and changes nothing else, so no test-only flag was added to the runtime. The muted run reproduces the movie's own bed to 0.01 dB peak and 0.19 dB RMS; the unmuted run measures -22.913 dBFS RMS against -23.184 predicted for two incoherent sources at -24.941 and -27.965. 0.27 dB out. (A) SKIPS THE INTRO IN THIS BUILD. `--skip-at=25` ends the movie at 25.02 s through `Input.parse_input_event` and `_unhandled_input`, so the press-to-skip wiring is live and the play-test's report is not that bug. Untested here: a real key from a focused window. Unsettled anywhere: whether the GAME permits skipping an attract movie -- Q9, still amber, and if the answer is no this path is deleted rather than debugged. `check` accepts the new `voice` kind and holds it to the strict peak bound, with `bgm`, because it is a sum this exporter produced rather than a wave off the disc. FORMAT.md says what a `voice` entry is, that it keys by MOVIE NAME rather than by a role, and that its `why` names every chunk dropped. 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 | ||
|
|
1b1a4dfcd3 |
containers: an expired token could never be replaced
Some checks failed
Credentials were seeded only when the container's copy was MISSING. So when a session expired, the file still existed, the copy was skipped, and restarting changed nothing -- the one recovery path a human has, re-logging in on the host, could not reach the containers at all. Now re-seeds whenever the host's copy is newer. Newer-wins rather than always-copy, because a container refreshes its own token mid-run and that copy may legitimately be the fresher of the two. Found when both sessions expired: host credentials at 16:30, containers holding 14:20 and 14:24. |
||
|
|
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. |
||
|
|
f33aeca8f5 |
port: file the keyframe-layout change and the loading-screen answer in BLOCKED
Neither is taken. The keyframe record layout (every pose timed, exit_ramp_units decodable) reaches me only when that branch lands on main -- sylpheed-formats is a path dependency and merging another agent's topic branch is not mine to do. The loading-screen names are answered but carry two of the RE agent's own amber flags plus the index-space problem I found, so no name is written yet. Recorded with what limits the risk: static composites are byte-identical across the keyframe change, so this iteration's verify-screen baseline is the control to re-run the day the pin lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC |
||
|
|
cdea236713 |
port: the P1 regression harness could not have run since the monorepo merge
verify-screen resolves its reference binary to a path build-reference-cli
stopped being able to produce: that script greps Cargo.toml for a
`Syplheed-Reborn.git", rev = "..."` pin, and
|
||
|
|
7132c4a326 |
port: implement MODDING rule 4, and withdraw a red flag that was my own bad measurement
MODDING.md calls base-and-overrides "a design constraint on the exporter today, not a milestone to add later". Nothing read `data/mods/` at all -- the directory has existed since the monorepo merge with a .gitkeep and no code path anywhere. Eight milestones shipped past it. ExportTree.resolve() now shadows by path, and every read goes through it: screens, sprites, cues, the music bed, movies. MenuAudio was reading tree.root directly and would otherwise have made audio the one asset kind a mod could not touch, for no reason a modder could have guessed. No manifest, no registration step -- the path IS the registration, which is the whole of the rule. One tree, not a stack: layering needs a load order and nobody has asked for one, so data/mods/README.md says that rather than inventing it. Every shadowed file is printed as it is read. The first version summarised in _ready, before any asset had been read, so it always said "nothing shadowed yet" -- a report structurally incapable of reporting anything, which is worse than none because it looks like an answer. data/mods/ was NOT gitignored, and that is a hole in a hard rule: a mod is usually an edited game asset, and this was the one directory a user is invited to put modified sprites in and git would have taken them. Now excluded except the README. Gate: a synthetic 203x43 magenta PNG (nothing disc-derived) at data/mods/sprites/title/main_menu/ptbtn01.png changes 8501 pixels in a bounding box of exactly 203x43 at the button's position, and `check` still passes. RAISED, NOT RESOLVED: MODDING.md says the tree is data/base/, PORT-MISSION.md §3 and the exporter and .gitignore say export/. Both are mission files and only the human changes a mission. REFUTATION on Q3's paint-order key: 2 of 16 screens did not match a stable sort by layer key -- but that was my test. pgloading_eff00.prm carries NO layer key (layer_source "none"): a primitive with no sprite header and no implied-name fallback. I sorted keyless first; the decoders put it last, which is right, since it is the full-screen black quad and HANDOFF's own sentence is that the fade quad paints last. Completing the rule to "keyless last" gives 16 of 16. SURVIVES. Recorded because the published claim does not say where a keyless element goes and there is one in the archive. Separately the tie-break's reach looks understated: 105 elements share a layer key across 12 of 16 screens, where HANDOFF characterises the cost as "one element's blend on one screen". WITHDRAWN, and it was mine: I filed "the runtime mix has no headroom" in red twice, off a peak reading. Measured properly it is 43 samples at full scale in 5.9 s and 24 in 98.5 s, longest run 0.25 ms -- the disc's own confirm cue on a transient, possibly only in the 16-bit save. Nothing changed, deliberately: attenuating would be an unmeasured level decision of the kind I refused for the loop point. A peak reading is not a clipping measurement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC |
||
|
|
d2100c227a |
port: say at the top of BLOCKED.md that it is the ask list, and that nobody reads it
The port's open asks have now been delivered by message three times and lost three times, because the decoder container was recreated each time and a message dies with it. The asks themselves were never lost -- they are in this file, with the HANDOFF sha each row came from -- but `docs/agents/decoder-loop.md`'s read list does not name this page, so a fresh decoder has no route to it. This does not fix that. It makes the page introduce itself, so that ONE pointer at it is enough for a session that has never seen it, and states the gap plainly where both agents and the human will read it. Changing the loop brief is the human's call and I have not touched it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC |
||
|
|
e490de1609 | Merge remote-tracking branch 'origin/main' into auto/port-p6-audio | ||
|
|
f1b87e47b6 |
decoder: tell it about the reference assets, and that the DB can be wrong
Some checks failed
The mounts landed but the agent could not learn of them: I documented them in CONTAINER-NOTES.md, which the decoder's prompt does not list, and then restarted the container -- so a fresh session with no memory of the exchange had a 586 MB database and a decompressed image sitting unmentioned in its filesystem. Now in the PROMPT itself, not only in a document, because the prompt is the one thing a new session is guaranteed to read. CONTAINER-NOTES.md is also added to its reading list. And the caveat that matters more than the asset. The .pe is PRIMARY -- the bytes the console executed. The database is somebody's ANALYSIS of them, produced by a disassembler that had to guess, and it is wrong in the ways disassemblers are wrong: misdecoded mnemonics where data was read as code, function boundaries short or long or merged or split, coverage missing entirely for code reached only by indirect dispatch, and names that are derived rather than symbols. So a finding resting on a database row is not established until the bytes agree: read the same address out of the .pe and check. Where they disagree the image wins, and the disagreement is itself worth recording, because it tells the next reader which parts of the database to distrust. A fast index into 9.2 MB of machine code, not a source of truth. |
||
|
|
48a2b91e5e | Merge remote-tracking branch 'origin/main' into auto/port-p6-audio | ||
|
|
72b10e7d03 |
decoder: mount the disassembly DB and the flat VA image
Some checks failed
The decoder had neither, and reported the gap precisely: four scripts in this repo READ /work/xenia-rs/sylpheed.db and nothing produces it, so the whole static PPC route was consumers with the producer missing. Both exist on the host and are now mounted read-only: the 586 MB database (25 481 functions, 851 classes with RTTI, EH tables, imports, 1.8M indirect-dispatch candidates) and the decompressed image. The image is the more useful of the two. It is a FLAT VA DUMP -- file offset = VA - 0x82000000 -- so reading a known address needs no XEX decrypt, no LZX, and no booted emulator. The decoder had independently recovered the same bytes by dumping /dev/shm/xenia_memory_* and validating against the GamePart table, which is good work and a sound method, but it noted itself that needing a running emulator is a bad dependency for something the entire static corpus rests on. It does not need one. Also recorded that an earlier claim the .pe was STALE was tested and refuted, so nobody re-litigates it, and that instructions.raw is an INT rather than hex. Written down as reference material, explicitly NOT a deliverable: they are read-only, they come from outside the repository, and a fresh checkout elsewhere has neither. Reimplementing the producer belongs in sylpheed-formats, and until it exists every static finding rests on an artefact this project cannot rebuild. |
||
|
|
d45da73bba |
port: P7 -- the new-game intro plays, and the two screens it skips are named out loud
S00A has been exported since P4; what P7 needed was something to play it and a
defined place to land. Both are here, and the interesting part is the gap.
The real chain is NEW GAME -> DIFFICULTY -> SELECT DATA -> (A) on a save slot ->
~4.5 s -> S00A. DIFFICULTY and SELECT DATA are MEASURED destinations that are not
GP_TITLE builds, so no screen file exists to go to. The port jumps from NEW GAME
to the one thing in that chain it has -- and the whole design is about not
letting that read as a sequence:
* MenuFlow.accept returns a new kind, `video`, rather than folding this into
`blocked`, because the caller has to announce the skip and a distinct kind is
what forces it to;
* the runtime prints the skipped screens by name on every run;
* flow.json carries `skipped_chain` as DATA, so what is missing lives beside
the decision instead of inside a GDScript string.
After the movie the port returns to the title. Authored, and it has to be: the
game goes into mission 1 and gameplay is out of scope. The ~4.5 s before the
movie is left EMPTY on purpose -- GP_TITLE does carry a loading screen and 4.5 s
is about the right shape for one, which is exactly why that belongs in BLOCKED.md
and not in flow.json.
`--script`'s 20 s per-step timeout would have killed every movie run at step 1.
Raising the constant would have been wrong the other way: a movie stuck at frame
0 would then hang the job, and a job that waits is worse than one that fails. The
test is now LIVENESS -- while get_stream_position() advances the deadline moves
with it, and a stalled movie still trips the same 20 s.
Found while looking: GP_TITLE's four unnamed builds (entries 0, 1, 12, 15) are
LOADING screens -- every element in all four is pgloading_*, and LOADING is one of
the three names the decoder read out of the title part's state function. NOT
renamed here: which member of each pair is which locale is an inference, and a
name stops being questioned once written. Handed over.
One of them is a second casualty of the rest.t problem, and a worse one:
pgloading_eff00.prm rests OPAQUE BLACK at t=38, so anything drawing that screen
at its declared rest paints a black rectangle over all of it. The title's case
only dimmed a frame.
REFUTATION, attempted and SURVIVED: HANDOFF says "exactly the six screen builds
carry the black .prm quad while the six overlays do not". Counting bundles with a
full-screen black primitive gives 8 and 4 -- build_12/15 carry one too. But
theirs runs black -> held -> clear where the transition quad runs black -> clear
-> black, so read strictly as "the quad whose group is the transition" the claim
holds. Recorded anyway: there are two kinds, and the naive census over-counts.
Gate: NEW GAME -> S00A plays 93.75 s against a declared 93.9 -> title, with
98.453 s recorded off the Master bus. What that does NOT show is that S00A's own
audio is in the mix -- bed and movie were not separated in this run, and the
write-up says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
|
||
|
|
59fba56a0f |
port: end the boot when BOTH builds have arrived, not when the plate lands
Moving the plate onto the shared clock moved the boot's exit with it: the run quit at the overlay's settle (t=238) while build 4's own fade-in from black runs to t=261. pteff00 is still ~7 % opaque there, so the capture came out visibly darker than the previous one -- with nothing failing, no warning, and no line in the log to say why. Caught only because there was an earlier capture beside it. The boot now ends at max(view.settle_time(), overlay.settle_time()) and prints the unit it is waiting for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC |
||
|
|
f8a8d17327 |
port: the ring spins, the plate needs no constant, and rest.t was never the settle
Two milestones' known-wrong bits, both now answered by the RE agent, both taken. P5 -- the focus ring. It was drawn at 0 with a comment saying so. The period is now measured (continuous spin, eight evenly spaced autocorrelation peaks over nine revolutions, no angle estimated anywhere) and it needs NO authored constant: the period is the element's own declared t=120, and what the measurement adds is only that the turn repeats rather than stopping -- which "groups hold" could not decide, because 0 and 360 are the same pose. `spin_period_units` is structural and narrow on purpose: two keyframes, differing in nothing but rotation_deg, by a full 360, first timed and second untimed. 16 of 212 elements in this export match and all 16 are focus rings, zero false positives. That check is the point -- the measurement was taken on ONE button of ONE screen, and a rule that caught anything else would be extrapolating it to elements nobody watched. Verified on the port's own render with the RE agent's own control: bit-identical one period apart across the whole frame, 3.6/255 inside the ring's box at quarter-period steps, and box luminance conserved to 0.027 % over eight phases -- which is the observable they used to separate rotation from a pulse. Not claimed: direction (no signed angle was ever measured) and phase across a focus change (their run held focus throughout). P3 -- the plate. Last iteration I refuted their authoring instruction and shipped it anyway rather than pick between two of their numbers. The refutation held and the answer came back better than either option I offered: AUTHOR NOTHING. Both builds run on one clock started together and the plate arrives at its own declared t=238. The 2.13 s constant is deleted. The premise that failed was mine: rest.t IS NOT WHEN A SCREEN SETTLES. It is the last hold keyframe before the exit. ptlogo1 stops MOVING at t=42 and then creeps 5 px and 31 alpha steps to t=251. Reading rest.t put build 4's arrival at 4.350 s instead of 1.967 s, and the "2.51 s, which is not a landmark of anything" I sent them is that error wearing a decimal point. 238 - 118 = 120 units = 2.000 s against a measured 2.135 s at 28.1 fps presentation. Checked against my own export before touching anything. `ScreenView.settle_time()` still uses rest.t, and so the boot sequencer paces every screen off the wrong landmark. NOT changed here: "visible arrival" is a heuristic and getting it wrong re-paces everything. Filed, and asked for a timed boot instead now that their oracle is live. REFUTATION: two of their pages measure the same declared 120 units of wall clock during a static hold and disagree by 2 % -- plate 2.135 s (28.10 fps implied), ring 2.177 s (27.56 fps). That is seven times the plate page's own 6 ms run-to-run agreement, and it lands on the argument that page uses to justify itself: "the build-in is where frames are dropped; the static hold is not". Also the ring page's band, 27.6-28.8 fps, does not contain its own measurement -- the mean needs 27.56 and four of seven spacings are outside. Filed, not worked around: my port uses the declared 120 units either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC |
||
|
|
4f767e72f6 |
port: P3 -- the boot title gets its PRESS (A) plate, and two of the RE agent's numbers do not agree
ScreenView now draws two builds at once, which it never had to before. It is a second ScreenView in the same SubViewport rather than a subordinate screen inside one: each build has its own timeline, its own textures and its own hold, which is the entire content of the finding, and Node2D siblings already paint in tree order. `paint_order` still means what it meant -- an ordering WITHIN a build. The delay is authored in flow.json on the BOOT STEP, not on the `title` screen. What was measured is the boot title; whether the plate is there when the title is reached again -- (B) from the menu, or after the attract movie -- is not, and hanging it on the screen would quietly claim that it is. REFUTATION, and it is the substance of this commit: the RE agent's authoring instruction does not reproduce the RE agent's own measurement, and the gap is 3.97 s. The instruction is "when build 4 has settled, wait 2.13 s, composite build 2". But build 2 has a group and this port plays groups -- ptbtn00 is alpha 0x00 at t=214, still 0x00 at t=236 while it slides 10 px up, and 0xff only at t=238, which is 3.967 s at 60 units/s. So the plate is first VISIBLE at settle+6.10 s, while what was measured -- the glyph counter leaving 154 -- is visibility at settle+2.13 s. Both groups starting together puts it 0.38 s BEFORE settle; build 2 starting at settle puts it at settle+3.97 s; landing on the measurement needs build 2's group to start 2.51 s after build 4's, which is not a landmark of anything. The measurement is untouched -- it is an observation of the running game and I have no standing to doubt it. What is refuted is the step that turns it into an authoring rule. So the port ships the instruction, prints the discrepancy on every boot, and files the row. Same call as the BGM sub-waves: a port that quietly picks the number that looks right destroys the evidence, because a corrected boot looks exactly like a correct one. Also refuted, and it was mine: BLOCKED.md has said since P2 that "no element's alpha reverses direction anywhere in this export, so nothing pulses". ptbtn00 reverses -- 0x00 -> 0xff -> 0x00 -- and it was in the export the whole time. The claim had been checked against the screens P2 happened to be animating. The port still draws no pulse, because no reading of this group yields the measured 2.24 s: the whole group is 4.47 s and from its first keyframe 0.90 s. Gate: `--boot --capture=` writes one frame of the composited end state, instead of the 600-PNG filmstrip that was previously the only boot artifact. `--screen=title --overlay=press_start` raises the same composite in two seconds for anyone who does not want to sit through 137 s of Theora. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC |
||
|
|
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
|
||
|
|
aebd79a3b9 | Merge remote-tracking branch 'origin/main' into auto/port-p6-audio | ||
|
|
2021eee47d |
agents: merge main at the start of every iteration
Some checks failed
Both agents read the protocol, their mission and the shared tooling from their OWN checkout, and both work on topic branches -- so without an explicit sync they follow whichever version of the rules existed when the branch started. Found concretely: tools/audio-capture and two protocol revisions were on main while the decoder worked for hours from a branch that had neither. The port had merged on its own initiative and did have them, which is exactly the kind of divergence nobody notices until the two disagree about what the rules say. |
||
|
|
3ddaa74df4 | Merge remote-tracking branch 'origin/main' into auto/port-p6-audio | ||
|
|
a8d2491366 |
audio: actually install the capture path I kept deferring
Some checks failed
The audio work was three parts and I shipped two. The transcode-fidelity method and the pinned 5.1 downmix landed; the null sink -- the only one that answers "what does the GAME play" -- I deferred to "the next natural rebuild window" and then rebuilt both images four times without doing it. pulseaudio-utils is now in both, with tools/audio-capture wrapping it: a null sink is a real device as far as an application is concerned, so Canary and Godot open it normally and parec records what they emit. This unblocks the decoder's Q8. The cue-to-event bindings are currently a name match against the authors' own identifiers -- a plausible guess, not a measurement -- and capturing what the game plays on a menu move converts them. `audio-capture run` reports the peak level and warns when the capture is silent, because silence is the failure that looks like success: a WAV of exactly the right duration, full of zeroes, because the application opened a different sink. A duration check alone passes it, which is how a confident wrong number gets made. |
||
|
|
9256722a11 |
port: P5 end to end -- and the port's title never says PRESS (A)
The P5 gate walk starts on a screen. This runs the whole objective instead, and
it is the only thing that would have found what it found:
xvfb-run -a godot --path port -- --boot --play \
--script=accept,down,down,down,down,accept,cancel,cancel --shots=/tmp/e2e
publisher wordmark -> developer logos -> ADV (151.9 s) -> title -> (A) -> main
menu -> navigate -> (A) -> EXTRAS -> (B) with focus restored to ptbtn05 -> (B)
-> title. 166.76 s, exit 0, nine frames. Shared as 1788003274-e68367e787d5.
THE PORT'S TITLE DOES NOT TELL THE PLAYER TO PRESS (A). The boot's last step is
`title` = GP_TITLE build 4, and build 4 has NO `PRESS (A) BUTTON` plate. P5 has
just made (A) the only way off that screen.
Not a guess about the art -- both states are captured off the running game and
differ by exactly that plate (live-title-build4-no-plate.png vs
live-title-press-a.png), and the plate is ALREADY EXPORTED as `press_start`,
build 2, sitting in export/screens/title/ unused by anything.
RECORDED, NOT FIXED, and the distinction is the point. This is P3's gate that
P5 exposed, and fixing it needs two things the port does not have:
* WHICH state an idle post-boot title shows -- build 4 alone, build 4 with the
plate over it, or build 4 THEN the plate after a delay -- is BEHAVIOURAL.
The game demonstrably has both states and nothing says which follows the
intro. The port has no oracle for a sequence; that is the Decoder's.
* showing it means DRAWING TWO BUILDS AT ONCE, which this port has never done
-- every mode loads exactly one screen. That is a change to ScreenView, not
a line in flow.json, and it is not being smuggled in under a navigation
milestone on the strength of "it looks more right".
Filed in BLOCKED.md. P5's gate is (A) into a submenu and (B) back; both work.
Two smaller things the same run found, both fixed:
* the boot step's `why` still said "nothing takes the title's place until P5
gives it somewhere to go". P5 has. Now says what is true: `--boot` STOPS on
the title (a boot that ends by fading to black looks like a crash) and
`--play` HANDS THE HELD TITLE OVER -- the stop is not a bug and the handover
is not another boot step.
* an empty focus printed as a line that trailed off, reading like a value had
gone missing rather than like there is none. The title is a screen with no
`buttons` that still takes (A), so it now prints
"(none -- this screen has no focusable item)".
Also confirmed: entering a submenu directly (`--menu=extras`) and pressing (B)
enters the parent at its AUTHORED initial focus, not a restored one. There is no
history to restore and MenuFlow.cancel only claims a restored focus when the
stack agrees about where it is going.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtmUw5N5LJaMW1Njb8Ziey
|
||
|
|
eef45ecfd6 |
port: P5 -- the menus navigate, and the focus ring is drawn wrong on purpose
P5's gate is "a human clicks through it". The artifact is a scripted walk that
proves the wiring rather than the intent -- up (wraps 01->05), five down, (A)
into EXTRAS, down, (B) back, landing on the main menu with focus RESTORED to
EXTRAS, ten PNGs one per settled step:
xvfb-run -a godot --path port -- --menu \
--script=up,down,down,down,down,down,accept,down,cancel --shots=/tmp/p5
--script posts InputEventAction through Input.parse_input_event so the presses
arrive at _unhandled_input exactly as a d-pad's would. Calling MenuFlow directly
would have been shorter and would have proved nothing: the wiring between a
press and the cursor is the part most likely to be broken, and a direct call is
exactly the part that skips it.
Derived vs authored, which P5 is the easiest place to blur:
* DERIVED -- the ORDER of the items, from each screen file's `buttons`, which
the exporter already fills from button-role elements sorted by resting Y.
* AUTHORED -- destinations, initial focus, what (B) does, and left/right being
a no-op. All measured off the running game (HANDOFF Q4/Q5) or chosen, none
on the disc, all in authored/flow.json with a why.
Four of five main-menu destinations are `goto: null` with a `blocked` note. That
is a MILESTONE BOUNDARY, not an unknown -- DIFFICULTY, the save list, the lesson
list and OPTIONS were all measured and live in archives this export does not
carry. `blocked` and `none` are kept apart so nobody later "discovers" the gap.
--headless CANNOT DRAW, and the port hung instead of saying so.
Measured, not assumed: under --headless Godot's dummy renderer never emits
RenderingServer.frame_post_draw, so every capture path awaited it forever --
--capture since P1, --film since P3, --shots as of now. With stdout block-
buffered the observable behaviour was SILENCE, FOREVER, which in a loop reads as
a job still working. Isolated by `--quit` (prints, exits 0) vs `--capture` (zero
bytes, killed at 40 s). Now those three flags refuse at STARTUP naming the
xvfb-run line that works, and --script no longer waits for a frame it is not
going to photograph -- so headless walks the menus in 4.5 s as a cheap
regression check needing no X server.
REFUTATION ATTEMPT, against the Decoder's
|
||
|
|
20b3c74b2c |
agents: they never spoke, the decoder lost the disc, and both shared one state dir
Some checks failed
Three defects, all mine, found by checking instead of assuming. **They never exchanged a word.** SendMessage=0, ListAgents=0 across both new sessions. PROTOCOL.md specified in detail what a message MAY and MAY NOT do and never said how to send one or that the other agent was addressable -- they knew that last time only because the human told them directly, and rebuilding with fresh volumes wiped it. Policy without mechanism is prose. Now documented with the two addresses, a worked example, and an instruction to introduce themselves on the first iteration rather than waiting to have a question. **The decoder lost the disc and the ISO.** They used to arrive inside the project mount and silently stopped when /work became a clone. Silently is the word: the disc-gated tests SELF-SKIP without SYLPHEED_DISC and report green, so a whole test suite would have passed while measuring nothing. Both are now mounted explicitly, the ISO at a stable path so run-canary does not depend on host directory names. **Both agents shared one Claude state directory.** They share the host's ~/.claude, and once both working directories became /work they resolved to the same projects/-work/ -- two supposedly independent agents writing to one place, which undoes the point of separate checkouts. Each now has its own volume, seeded once from the host with credentials only, so a token refresh writes locally and neither can corrupt the host's auth. Also widened the pacing rule. It banned ScheduleWakeup by name; the decoder then scheduled itself an hourly cron job -- not harmful, but the same instinct that ended a run yesterday, through a door I had left open. Now: no self-scheduling by any route. Mount audit after the changes: shared and intentional are the exchange volume and the read-only credential seed. Everything else -- repo, Claude state, cargo, target, canary, disc, ISO -- is per agent or one-sided. |
||
|
|
01a3505b1e |
containers: fix volume ownership and make the clone guard survive interruption
Some checks failed
Two bugs, both mine, both found by starting the thing.
**Volume mount points must exist AND be owned by the agent before USER agent.**
Docker seeds a named volume from whatever the image has at that path, ownership
included, and creates a ROOT-OWNED directory when the path is absent. Either way
the agent cannot write, and the failure surfaced far from its cause: "clone
FAILED", with no permission error anywhere in sight. The port's own Dockerfile
already carried a comment explaining this trap, which I then walked into for
/work and /exchange.
**The clone guard checked for a .git directory, not a usable HEAD.** A clone
interrupted partway -- the container was removed while one ran -- leaves a .git
with no commits, and a presence check then skips the retry forever and hands the
agent an empty repository that looks like a checkout. It now verifies HEAD, and
clones via a temp directory so a partial result never lands in /work at all.
Also: the port launcher's path defaults still assumed the old repo root, so it
mounted no disc; and the stale /reborn notice is gone now that there is one
repository.
Verified running: both agents cloned
|
||
|
|
60595d4062 |
port: P5 groundwork -- the focus record, measured and checked against a capture
P5 is the lowest unfinished milestone. This does not implement navigation; it
settles how a focused button is drawn, because three claims sat under that and
none had been checked from this side.
Refutation attempts, all three failed -- recorded either way, per PROTOCOL:
* HANDOFF ask 3's '(7,7)' focus offset SURVIVES, and more strongly than
stated: over a 15x14 scan of the whole offset space it is a UNIQUE
ISOLATED cell at 100% coverage on all five buttons, with (6,6) and (7,6)
both below 90%. The centre and pivot alignments reproduce the 78-84%
band the RE agent called misleading.
* ORACLE-CAPTURES' 'a crop, not a scale' SURVIVES. Its own evidence -- a
+/-6 px cross-correlation -- cannot tell a crop from a 0.9375 scale, so
it was re-tested with a scale-sensitive one: button text bands land at
design y + 23 for all three unoccluded buttons, an exactly 1:1 vertical
mapping. (The prose understates 45 missing rows as 'the missing row'.)
* My own suspicion that the declared geometry disagreed with the capture
by 6 px was MY ARITHMETIC ERROR, written up rather than quietly dropped:
I took the top-left as pos - pivot. pos IS the top-left; the pivot is the
anchor scale grows about and cancels at 100%, exactly the 'can be got
wrong invisibly' that screen_view.gd:120 warns about.
What is actually true, and what P5 does with it:
* The exporter ALREADY emits the focus record's second element, the 42x46
ring ptbtneff01, for all five buttons. That gap is in the renderer, not
the exporter -- nothing to change in crates/sylpheed-export.
* Base minus focus is (7,7) directly from the declared positions, so P5
draws each focus element at its own pos and authors no constant.
* ptbtn04 is 1 px off the 80 px grid ON THE DISC (base rows 162 242 322
401 482; focus rows a clean 155 235 315 395 475). So its base->focus
delta is (7,6) while its art aligns at (7,7). Do NOT derive focus
placement from the base by a constant: it would be wrong on exactly one
button and right on the other four.
Verified against captures rather than our other renderer: diffing
live-main-menu against live-main-menu-options-focused isolates one cluster at
x 506..702, y 398..445, and ptbtn04's focus record under pos-as-top-left spans
x 500..706, y 395..451. Under pos - pivot it predicts x 433..604, y 367..422,
which matches nothing in the capture and no other button either.
Also records an instrument that FAILED ITS CONTROL and was discarded: a masked
NCC template matcher returned NCC 0.096-0.206 with three of five results pinned
to the search boundary when asked to re-find the base sprites at their known
positions. None of its output is used. Filed so this is not rebuilt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Evuhbt8pxKJEUvwfniwYkU
|
||
|
|
cd1b6cfe72 |
port: reconcile BLOCKED.md against the in-repo HANDOFF at 9ca1eb5
The page cited '/reborn HEAD 9a0ca0d' and that address no longer resolves, for two reasons it now records: * the repositories were merged into one monorepo ( |
||
|
|
c2e32d3397 |
port: commit the Cargo.lock entry for sylpheed-export
The crate is in the workspace and builds, but its lock entry was never committed -- `build-export` regenerates it on every fresh container, so it only shows up as a dirty tree nobody caused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Evuhbt8pxKJEUvwfniwYkU |
||
|
|
e23c5717aa |
port: gitignore the export tree, which MISSION already said was ignored
`export/` is what `crates/sylpheed-export` writes and what `ExportTree.locate()` reads. It was **not** gitignored. `data/base/` was -- the name MODDING.md gives the same tree, and a directory nothing writes. So the live output directory was tracked while MISSION §4 states, as the hardest rule in this project, that it 'is generated from the user's own disc and is gitignored'. A `git add -A` in a container that had run the exporter would have committed the disc: 16 screens of sprite PNGs and two transcoded .ogv reels. Nothing was committed -- this repository's history is clean, and a fresh container reproduces the hazard rather than inheriting it, because the export tree is regenerated and never cloned. Both names are now ignored, so renaming the tree to match MODDING.md later cannot silently re-open this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Evuhbt8pxKJEUvwfniwYkU |
||
|
|
06676d3dc0 |
containers: each agent clones the monorepo into its own volume
Some checks failed
The last structural fix for the collision class that has bitten three times. Both containers now clone the repository into their OWN named volume instead of bind-mounting a human's working tree, so an agent's local git config cannot capture a human's commits, a credential helper cannot leak a container-only path onto the host, and a `git add -A` cannot sweep another party's in-flight files. Cloned once at startup and never auto-pulled: pulling under a running agent moves files out from under whatever it is mid-edit, which is the same bug again. Accepted knowingly: Claude Code keys per-project memory off the working directory, so moving off the host path starts that memory empty. The corpus in docs/ is the memory that matters and it travels with the clone. Other changes: * docker/agent -> docker/decoder; the launcher is sylph-decoder. Roles, not "the agent", now that there is more than one. * /reborn is gone -- one repository now, so the port reads HANDOFF from its own checkout rather than through a live read-only mount of someone else's tree. * Canary mounts separately at /canary; it stays a fork tracking upstream. * A shared `sylpheed-exchange` volume at /exchange, with tools/ on PATH so `share` is available in both. * The decoder's credential file gets the .host-copy treatment the port already had -- `credential.helper=store` rewrites by rename-over-target, which is EBUSY on a bind mount and reports a fatal that is not one. * Budget split deliberately: decoder 5 cpu / 6 GB, port 3 / 4, leaving room for the planned Referee. "Half the host" was right when there was one agent. Prompts move to docs/agents/ and are rewritten around the protocol: the oracle is the running game, dynamic RE stays with the decoder, each iteration must attempt to refute one claim of the other, and neither may verify its way out of its own role. |
||
|
|
a8815f2826 |
agents: the team protocol, the share tool, and a player's-eye navigation doc
Some checks failed
**navigation.md rewritten from the player's chair.** It was written from the inside out -- GamePart ids, pak names, sprite names -- which is how WE find things, not what the game shows anyone. Now it describes what is on screen, what you press and what happens, with internals as footnotes. Most rows are open on purpose: it exists to be filled in by playing, and the in-game tutorials are the resource for the flight half. **tools/share** gives transient files provenance without giving them history. Three kinds of thing were travelling down one channel with opposite needs: code and decoded knowledge want permanence, cited evidence wants permanence, and "look at this PNG" wants no history at all. The third kind bloats a repository forever; passing it by message is worse, because the receiver gets bytes with no idea which build produced them. `share put` records who, when, what, the sender's commit, and whether their tree was dirty -- because a capture taken from a modified tree cannot be reproduced from the sha, and the receiver deserves to know that before building an argument on it. **docs/agents/PROTOCOL.md** is the contract. The parts that matter: Dynamic RE stays with the Decoder -- most of what is open is behavioural and cannot be answered from the file. What the planned Referee adds is different: bias enters at what you CHOOSE to capture, so a corpus captured to a fixed protocol by someone with no hypothesis is worth more than one captured to settle an argument. A message may point, ask, prioritise and challenge. It may not change scope, redefine ground truth, or carry a finding instead of writing it down -- including a message claiming to relay the human, because a relayed instruction has no evidence attached and this project has watched a wrong belief travel further and faster than its correction. Adversarial duty is explicit: every iteration, try to refute one claim of another agent and record the attempt either way. Run your own instrument through a control first. Disagreements go to the human with both positions, not to whoever is more certain. And no agent may verify its way out of its own role: the Port has no oracle, the Decoder builds nothing, the Referee interprets nothing. |
||
|
|
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.
|
||
|
|
8c33f86a20 |
merge the Godot port's history into the monorepo
Brought in with a subtree merge rather than a copy, so the port's 31 commits survive as history rather than arriving as one anonymous import. Landed under godot-import/ and moved into the final layout in the next commit, which keeps git's rename detection able to follow each file across the move. |
||
|
|
ff0937fa8a |
port: pin the 5.1 downmix matrix explicitly -- human decision
The RE agent escalated this rather than picking, correctly: folding centre into L/R changes how dialogue sits against the music, which is an aesthetic judgement about the game and not a container detail. Decided: explicit ITU fold, centre at -3 dB, recorded in the manifest with the rest of the transcode command. Pinned rather than defaulted because a default is a decision nobody made -- invisible in the output and free to change between ffmpeg versions. |
||
|
|
8d7373b16c |
video: state the 5.1 downmix instead of inheriting it, and write atomically
A real defect in the P4 output, found by the human on ADV.wmv and widened by the RE agent to the whole disc: the disc ships 28 movies in 5.1 WMA Pro (every cutscene, INCLUDING both movies this port needs) and 69 already in stereo. A bare `-ac 2` therefore does two different things and records neither -- stereo passes through, and 5.1 is folded by ffmpeg's DEFAULT matrix. How loudly centre-channel dialogue sits against the music is a content decision, and it was being made by accident and could move under an ffmpeg upgrade. Now stated: ITU-R BS.775, LFE dropped, normalised by 1/(1+2*sqrt(1/2)) = 0.4142. It appears in the recorded command, so the manifest determines the output. MEASURED rather than chosen by taste, and the measurement is the interesting part: the explicit matrix and ffmpeg's inherited default differ by a residual of -91 dB -- about one LSB at 16-bit -- with peak and mean agreeing to 0.1 dB. So ffmpeg's default IS this matrix, and the audio does not change; what changes is that the manifest now says which matrix. The UNnormalised textbook form was measured too and clips at 0.0 dBFS, which is why the scaling is there. The filter is applied only to 6-channel sources, probed per file with ffprobe, so a stereo source is never run through a matrix referencing channels it lacks. Also: encode to a temp name and rename on success. ffprobe read a mid-write .ogv as 33 s against a 137 s source -- no error, no warning, the exact shape of catastrophic truncation. The filesystem is shared with the RE agent, so that is a race, not an edge case, and a half-written file must never be visible under its final name. tools/verify-video-audio answers the second of the three questions docs/AUDIO-VERIFICATION.md separates: does GODOT route the audio. An AudioEffectRecord on the Master bus writes Godot's own mixed output to a WAV from a headless run, so "no sound card" was never the obstacle I claimed. It deliberately checks non-silence and level only -- a difference-signal RMS against the source is inconclusive without cross-correlation alignment and an agreed downmix, and would produce a confident wrong number. |
||
|
|
700094bef1 |
FORMAT v3: rotation, and the focus record -- the ring the port could not reach
Pin bumped to the TAG formats-pin-2026-08-29 (
|
||
|
|
c0a5725934 |
port: pin by tag, and how to verify audio with no sound card
**The pin.** Pin a TAG, never a bare sha. A sha reachable only from an auto/* branch is orphaned when that branch is deleted or -- worse -- squash-merged, because squash creates new commits: main looks like it contains the work while the pin becomes unreachable and this project stops building for a fresh checkout, silently, at their build. formats-pin-2026-08-29 exists for the current state. Also says plainly why NOT to float to a branch, which was the tempting fix: Cargo resolves a git dependency once and writes the sha into Cargo.lock, so floating gives staleness you cannot see instead of staleness you can read. push-work now pushes --follow-tags so annotated tags travel with the branch. **Audio.** docs/AUDIO-VERIFICATION.md separates three questions that were being asked as one: is the transcode faithful (no engine, no device -- a file-vs-file difference measurement), does Godot route it (AudioEffectRecord on the Master bus writes a WAV from a headless run), and what does the GAME play (a PulseAudio null sink, which needs a rebuild). It leads with the three ways the fidelity measurement lies, because all three were hit on the first attempt and each produces a confident wrong number rather than an error: unaligned subtraction, mismatched channel layouts, and probing a file another process is still writing. The 5.1 disc fact deliberately is NOT copied here -- it lives in the RE corpus at docs/re/structures/movie-audio-channels.md and is linked, so there is one copy to keep true rather than two that drift. Same reason HANDOFF is a summary with links. The downmix itself stays flagged as an unmade decision, not quietly resolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
26b5afeaa3 |
agent: tag the decoder states the port pins, and stop leaking credential config
Two fixes to push-work and the policy that goes with them.
**Tags.** The port's exporter depends on sylpheed-formats BY REVISION, so a
commit of ours is part of its build -- and the commit it pinned lived on one
auto/* branch and nowhere else. Deleting that branch orphans it; squash-merging
it is worse, because squash creates NEW commits, so main appears to contain the
work while the pinned sha becomes unreachable and the port stops building for a
fresh checkout. Silently, at their build, long after the breakage.
push-work now pushes with --follow-tags, which publishes annotated tags
reachable from the pushed commits, and MISSION.md says to tag whatever the port
needs. formats-pin-2026-08-29 at
|
||
|
|
3491d30066 |
re(media): the disc ships movies in TWO audio profiles, and 28 of them are 5.1
Probed all 97 movies. 28 are wmapro 48 kHz 6-channel 5.1 -- ADV.wmv and every S*.wmv story cutscene; the other 69 are wmav2 48 kHz stereo, every RT*.wmv and hokyu_*.wmv. The split is cinematics vs in-mission radio chatter. Both movies the menu milestone needs, ADV.wmv (the boot/attract intro) and S00A.wmv (the new-game intro), are in the SURROUND group. Why it matters: one ffmpeg command over dat/movie/ produces two different kinds of result and records neither. The 69 stereo files pass through unchanged; the 28 surround files get downmixed 5.1 -> stereo by ffmpeg's DEFAULT matrix, folding centre-channel dialogue into L/R at a weighting nobody chose and which is not stable across ffmpeg versions. That is a content decision inherited by accident, so it should be stated explicitly and recorded beside the command. Credit where due: found by the human while checking a transcode, verified independently here and widened from one file to the whole disc. Two METHOD entries from the same episode, both about measurement rather than format: don't probe a file another process is still writing (a half-written transcode reported 33 s against a 137 s source, no error, nearly a filed bug), and a difference-signal RMS is meaningless before cross-correlation alignment (-34.2 dB against a -25.3 dB source looks like failure and is inconclusive). |
||
|
|
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. |
||
|
|
2fbcd59920 |
docs: retract "reference renderer" -- sylpheed-cli is not the oracle
A framing correction from the human, and it runs through everything I have written, so it is a retraction rather than a silent edit. Reborn "was/is just a GUI explorer and extraction CLI for verifying the decoding of the various files. It may very well be wrong." The oracle is the Xenia Canary capture and the game. So verify-screen is a CONSISTENCY check between two decoders that share their assumptions, plus a regression detector -- not a correctness check, and agreement in it is not evidence of correctness. Its header now says so, it calls the CLI the COMPARISON renderer, and DIFFERS means "we moved apart, find out which of us moved". The uncomfortable part, recorded because it is the actual failure mode: this file already contained the sentence "two renderers reading one field through one decoder agreeing is not evidence that the field is right", written after the ptframe1 case -- and I then went on quoting 3/255 against sylpheed-cli as though it meant the port was right. Having the principle written down did not stop me leaning on the agreement. Three times both renderers agreed and both were wrong, each caught only by a capture: pteff05 (menu screens had no background), scale-0 (drawn full size instead of collapsed), rest() (the menu bracket missing). Correctness moves to the captures -- nine of them, indexed at docs/re/captures/ORACLE-CAPTURES.md, covering all five screens in scope. Three cautions travel with them: not gamma-neutral (there is a floor, don't chase it), geometry IS sound (a positional disagreement is real), and each is one moment of a still-animating screen. verify-screen keeps running over all 16 screens every iteration. It is still worth having -- total, cheap, and it catches a divergence introduced on the RE side. It is just not a grade. |
||
|
|
579c8096c1 |
port: P4 -- the intro video plays inside the boot sequence
The exporter transcodes ADV.wmv and S00A.wmv to Ogg Theora and records the exact ffmpeg command in the manifest, per MISSION §6, so a modder who dislikes the quality re-runs one line rather than reverse-engineering what was done. Quality was MEASURED, not judged: SSIM against the decoded source over a 10 s sample is 0.9863 / 0.9896 / 0.9924 at -q:v 6 / 8 / 10, and at 200 % zoom on the reel's hardest case -- fine serif text and soft gradients over near-black, where Theora breaks first -- q8 is indistinguishable. So MISSION §6's permitted FFmpeg-GDExtension fallback is NOT needed and is NOT being proposed. No new runtime dependency. -ac 2 because the source is 6-channel WMA Pro; that downmix is a decision, so it lives in the recorded command rather than in prose. Encoding is cached on a .cmd sidecar holding the command and the source size -- any change to either re-encodes. export/ is still regenerated wholesale; this is derived state validating derived state, not a hand-edit, and without it every re-export pays ~4 minutes to produce a byte-identical file. The player renders INTO the design SubViewport. Parenting it to the Boot node played the movie to the window instead, and every captured frame came out black -- which is worth more than a capture-bug note: a movie outside the 1280x720 design space is outside the coordinate system every screen is expressed in. (A) skips a movie, because Q9 measured that (title at 57 s vs a 193 s baseline). NOT VERIFIED, and stated as such: audible playback. This container has no audio device and Godot falls back to the dummy driver. The Vorbis stream exists, is 2-channel and decodes; whether Godot emits it is unconfirmed. |
||
|
|
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. |
||
|
|
b632602517 |
docs: P3 -- the boot runs unattended, and four answers taken as given
Gate: `godot --path port -- --boot --film=/tmp/boot` runs publisher_logo -> developer_logos (4.65 s) -> title (8.57 s), holding on the title after 13.05 s, each screen fading in, holding, and fading through black into the next. verify-screen now covers all 16 screens and passes `--all` to the reference renderer: the exporter addresses by pak entry index, which is the numbering `--all` uses, and without it `--build 10` would land on entry 12. The four new splash bundles come in at max 1-2/255. The three known differences are unchanged. Also recorded, taken from the RE agent rather than re-derived: * Focus stays "replace" -- over-vs-instead is unobservable (the focused sprite covers the base at 100 % of base-visible pixels; the two compositions differ by RMSE 1.1, under the gamma floor). The port guessed right for the wrong reason. The real gap is that ptbtn0Nf.rat declares TWO sprites -- a glowing ring, then the label -- where ptbtn0N.rat declares one. P5's, and the ring's placement is not decoded, so it will be authored from the capture and marked as such. * RMSE against captures has a FLOOR: capture ~ 255*(render/255)^g, g ~ 1.49 menu and EXTRAS, 1.34 title, and it is a ramp the GAME installed, not a capture-path artefact to subtract. Narrow reach (fitted on mostly-dark patches), so the port will not extrapolate it and will not apply it to rendered output. It is a comparison constant, not a rendering one. * Rotation is escalated to a human and the port has NOT acted. The RE half is answered -- about the declared pivot, measured -- and it has zero effect on the five screens at rest. |
||
|
|
0a026074d3 |
port: play the exit ramp, and run the boot sequence unattended
P3. The exit is the group playing ITSELF out, not a black rect over a frozen screen -- and that was settled by a test that discriminates rather than by plausibility. Under the black-rect model every region is scaled by the same 1-alpha, so the button/background brightness RATIO would hold constant through the fade; measured, it falls 6.495 -> 5.574 -> 3.105 -> 2.125 -> 1.935. Implemented by giving the final untimed keyframe a SYNTHETIC time, exit_ramp_units after the last timed one, then interpolating it like any other frame. One code path: arriving and leaving differ only in how far `t` is allowed to run, not in kind. `holding` is what the sequencer clears to send a screen away. The sequencer waits on nothing the disc does not carry. A screen holds until its own group has arrived, then plays out; `dwell` in flow.json is deliberately empty because each screen's dwell IS its keyframe group (publisher wordmark 3.92 s, developer logos 3.17 s, both read from the disc). Any extra hold would be a number nobody measured. The last screen keeps holding -- nothing is taking the title's place, and a boot that ends by fading to black looks like a boot that crashed. flow.json reproduces an OBSERVATION and says so in its header: Q6 closed with a negative, the order is on the disc nowhere, a transition is a call with a name argument chosen by code. The intro video's place in the real boot is named as a gap rather than the order being quietly rewritten to hide it. |
||
|
|
0d2bf1f241 |
export: reach the splash by authored entry index, and key names by entry
There were TWO splash screens and this port had neither. Entries 11/14 are the developer logos; entries 10/13 are the SQUARE ENIX publisher wordmark, the first thing the boot shows, which nothing in this project had noticed. They have no .rat layout child so `is_build` cannot see them, and the RE agent established that no CONTENT rule can either: design size fails (every extra composable bundle sampled is 1280x720, same as every screen) and element count fails (fragments run 2..15 elements in GP_OPTIONS/GP_SAVE_LOAD, the splash halves are 3 and 7 -- the ranges overlap). So `screen_builds` is `is_build` plus an authored allow-list of ENTRY INDICES, each carrying a `why` that says it is a locator and not a claim. An allow-list rather than a loosened predicate because this is safe in GP_TITLE and would not be in general: there, widening adds exactly four bundles and all four are real screens, zero fragments. screen_names.json is rekeyed from enumeration ordinal to pak entry. Widening renumbers ordinals, and a name that moves when the enumeration rule changes is not a name -- the file always called the entry the stronger locator, and it is now the only stable one. The two unnamed plates therefore renamed build_10/11 -> build_12/15; they were always locators, and now they locate the right thing. All 16 export and validate. |