Their finding was that MISSION.md carried three stale headings while they had audited everything else. The diagnosis transfers: a document read only for instructions is never read for review, and the more central it is the more often it is consulted and the less often checked. The brief names five documents to read every iteration. I had audited BLOCKED.md and never PORT-MISSION.md or MODDING.md. Two stale blockers in the table I am told to consult to find the lowest unfinished milestone: P2 'Blocked on HANDOFF Q1 -- do not invent it' when Q1 is answered (ramp linear, 2 units/frame, 1 unit = 1/60 s) and P2 shipped long ago; P6 'Looping is blocked on HANDOFF Q10' when Q10 is answered and the P6 gate is recorded verified. Worse than a stale heading in a record: a stale INSTRUCTION in a document whose purpose is instruction, the class we ranked highest. Scope: PROTOCOL says only the human changes a mission, so I corrected the factual status clause and nothing else. Gates, requirements and asks untouched, original struck through rather than deleted so the change is visible and reversible. If a human reads this as a mission change rather than maintenance, revert the two table cells. MODDING.md is still unaudited and is named as such rather than quietly skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
189 lines
9.4 KiB
Markdown
189 lines
9.4 KiB
Markdown
# Primary objective — the menu shell, running in Godot
|
||
|
||
**Status:** active, set 2026-08-28.
|
||
|
||
Build a Godot 4 project that boots the player's own disc through the sequence the
|
||
real game uses, and let a person move through it:
|
||
|
||
```
|
||
developer logo splash → intro video → title / PRESS Ⓐ → main menu → submenus
|
||
```
|
||
|
||
No gameplay. No 3D. No HUD. No emulator. Done means a human presses a d-pad and
|
||
Ⓐ and moves through those screens with the right art, animation, music and
|
||
transitions.
|
||
|
||
## 1. You are one of two agents
|
||
|
||
A **container agent** does the reverse engineering, in the
|
||
[Syplheed-Reborn][reborn] repository. It runs the emulator; you do not. You build
|
||
the port from what it publishes.
|
||
|
||
The contract is `docs/port/HANDOFF.md` in that repository. **Read it before
|
||
assuming any value is on the disc.** Every answer there is one of three things,
|
||
and the distinction decides what you do:
|
||
|
||
| | meaning | what you do |
|
||
|---|---|---|
|
||
| **decoded** | a field on the disc, with a disc-wide check | read it in the exporter |
|
||
| **measured** | not on the disc, but the running game does *this* | put it in `authored/`, cite the finding |
|
||
| **undecodable** | looked for, provably not there | put it in `authored/`, say it is a decision |
|
||
|
||
If HANDOFF.md does not answer something you need, **say so and move to another
|
||
milestone**. Do not guess and do not reverse engineer it yourself — you have no
|
||
emulator and no oracle, so a guess here is indistinguishable from a fact and will
|
||
be believed later.
|
||
|
||
[reborn]: https://git.mc02.dev/fabi/Syplheed-Reborn
|
||
|
||
## 2. The wall
|
||
|
||
The Godot project **never reads a disc format**. No IPFB, no RATC, no T8aD, no
|
||
XMA, no WMV. If Godot cannot read something, the exporter's job is to emit it
|
||
differently — not to bridge the gap at runtime.
|
||
|
||
* **No GDExtension. No Rust in `port/`.**
|
||
* The decoders come from `sylpheed-formats`, **pinned by TAG**:
|
||
`sylpheed-formats = { git = "...", tag = "formats-pin-2026-08-29" }`.
|
||
|
||
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.
|
||
A tag is a permanent ref, it says what it is in `Cargo.toml`, and it fails
|
||
loudly at *fetch* rather than silently at build.
|
||
* **Do not float the pin** to a branch. It would not do what it sounds like:
|
||
Cargo resolves a git dependency once and writes the sha into `Cargo.lock`, so
|
||
floating gives you staleness you cannot see instead of staleness you can read.
|
||
* Bump deliberately, as its own commit, saying what you wanted from the new
|
||
state. The RE agent tags when it lands something you need and tells you over
|
||
the message channel — that is how you stay current without floating.
|
||
|
||
**In particular, do not reimplement media assembly.** `sylpheed_formats::media`
|
||
already handles the cases where one playable thing is not one archive entry: a
|
||
`.pak` entry that spans segment files, a bank with several sub-waves, and the
|
||
cutscene voices — which are one continuous XMA stream chunked into `VOICE_*.slb`
|
||
entries whose boundaries do **not** match the cues, so *a `.slb` need not hold
|
||
the track its name claims*. That last one is the single easiest thing in this
|
||
project to get subtly wrong. Use `resolve_movie_voice_region`.
|
||
|
||
## 3. Derived vs authored
|
||
|
||
| | `export/` | `authored/` |
|
||
|---|---|---|
|
||
| produced by | the exporter | you, by hand |
|
||
| contains | what the disc says | what we decided |
|
||
| hand-edited | **never** | always |
|
||
| in git | **no** — gitignored | yes |
|
||
| on re-export | overwritten wholesale | untouched |
|
||
|
||
Tempted to hand-fix a file under `export/`? The fix belongs in the exporter or in
|
||
`authored/`. Every `authored/` entry carries a `why`.
|
||
|
||
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.
|
||
|
||
## 4. Never commit game assets
|
||
|
||
`export/` is generated from the user's own disc and is gitignored. Code, schemas,
|
||
`authored/` mappings and docs only. If you are about to commit a sprite PNG or a
|
||
transcoded video, stop.
|
||
|
||
## 5. Milestones
|
||
|
||
A milestone is done when its **artifact** exists, not when the code compiles.
|
||
|
||
| | Milestone | Gate |
|
||
|---|---|---|
|
||
| **P0** | Exporter skeleton; one screen and its sprites to `export/` | `export/screens/title/main_menu.json` validates against FORMAT.md and the PNGs open |
|
||
| **P1** | Godot renders that screen statically at 1280×720 | A Godot screenshot beside `sylpheed-cli screen render` of the same build — they should agree, and where they do not, say which is wrong |
|
||
| **P2** | Keyframe animation | Buttons slide in. ~~Blocked on HANDOFF Q1 (the time unit). Do not invent it~~ — **Q1 is answered**: ramp linear, 2 units per rendered frame, 1 unit = 1/60 s. Gate met |
|
||
| **P3** | Splash → title, with the transition | Both screens back to back, unattended |
|
||
| **P4** | Intro video | `ADV.wmv` plays with audio (§6) |
|
||
| **P5** | Main menu: navigation, focus states, Ⓐ into a submenu, B back | A human clicks through it |
|
||
| **P6** | Audio — menu BGM and move/confirm SFX | Sound on the P5 gate. ~~Looping is blocked on HANDOFF Q10~~ — **Q10 is answered**: two stems of one performance, played together. Gate met |
|
||
| **P7** | New-game intro video after NEW GAME | Plays, then returns to a defined state |
|
||
|
||
Work the lowest unfinished milestone. When one is blocked on an RE answer, say so
|
||
in `docs/BLOCKED.md`, and take the next milestone that is not.
|
||
|
||
## 6. The video problem
|
||
|
||
`ADV.wmv` is **WMV3 video with WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4
|
||
plays only **Ogg Theora** natively.
|
||
|
||
Transcode with ffmpeg, and **record the exact command in the export manifest** so
|
||
a modder who dislikes the quality can re-run it rather than reverse-engineer what
|
||
you did. Theora at 720p is not great; if the result is visibly poor, **say so and
|
||
propose** the FFmpeg-GDExtension fallback — do not adopt a runtime dependency on
|
||
your own authority.
|
||
|
||
Only the boot intro and the one new-game intro are in scope. The disc holds
|
||
3.3 GB of video; transcoding all of it is not this milestone.
|
||
|
||
### The downmix is decided: pin it explicitly
|
||
|
||
**Human decision, 2026-08-29.** The cinematics are 5.1 (see
|
||
[`movie-audio-channels`][mac] for the disc-wide split — 28 surround, 69 stereo,
|
||
and *both* movies this milestone needs are surround). Fold to stereo with an
|
||
**explicit matrix**, not ffmpeg's default:
|
||
|
||
```
|
||
-af "pan=stereo|FL=0.707*FC+1.0*FL+0.707*FLC+0.707*BL+0.707*SL|FR=0.707*FC+1.0*FR+0.707*FRC+0.707*BR+0.707*SR"
|
||
```
|
||
|
||
Centre at −3 dB into both channels, which is the standard ITU fold and keeps
|
||
dialogue sitting correctly against the music. Record the full command in the
|
||
manifest, per the rule above.
|
||
|
||
Pinned rather than left to the default because a default is a decision nobody
|
||
made: it is invisible in the output, it can change between ffmpeg versions, and
|
||
it silently alters how speech sits in the mix. Adjust the matrix if it sounds
|
||
wrong — but adjust it *deliberately*, as a commit.
|
||
|
||
[mac]: https://git.mc02.dev/fabi/Syplheed-Reborn/src/branch/main/docs/re/structures/movie-audio-channels.md
|
||
|
||
## 7. Out of scope
|
||
|
||
3D, gameplay, HUD, missions, save/load, localisation beyond English, the Ready
|
||
Room, and any reverse engineering. If you want an answer the disc has not given
|
||
you, that is a request to the container agent, not a task for you.
|
||
|
||
## 8. Tooling policy — MCP servers and third-party skills
|
||
|
||
Surveyed 2026-08-28. **No Godot MCP server, for now**, and the reason is not
|
||
that they are bad:
|
||
|
||
* The mature ones ([godot-ai][ga], and most of the field) need a **live Godot
|
||
editor** running with a plugin that talks WebSocket to a Python server. This
|
||
agent is headless in a container; that is a daemon, an editor process and a
|
||
second language runtime added to an unattended loop, all of which can fail in
|
||
ways that look like a port bug.
|
||
* Their headline feature is **scene-tree introspection and node manipulation** —
|
||
built for someone hand-authoring scenes in the editor. This port *generates*
|
||
its screens from exported JSON at runtime. The agent writes a loader, not a
|
||
scene tree, so the feature that justifies the complexity does not apply here.
|
||
* What the agent actually needs to verify its work already exists:
|
||
`godot-headless` to run the project and `screenshot` to diff against
|
||
`sylpheed-cli screen render`. The verification loop is the valuable part, and
|
||
it is a bash job.
|
||
|
||
**Third-party skill packs** ([godot-claude-skills][gcs], [GodotPrompter][gp],
|
||
[Godot-Claude-Skills][rcs]) are the opposite trade: pure context, no runtime, no
|
||
daemon. They are worth revisiting. They are **not installed now** because a skill
|
||
is *instructions injected into an agent running with approvals disabled*, which
|
||
is a supply-chain decision and not one to make by default — and because P0/P1 are
|
||
a Rust exporter and a static sprite draw, which need no advanced GDScript.
|
||
|
||
**If you want one, propose it**: name the pack, say which milestone it unblocks,
|
||
and let a human vendor and review it. Do not install from a marketplace on your
|
||
own authority.
|
||
|
||
Revisit this if GDScript quality becomes the bottleneck — most likely at P2,
|
||
where keyframe ramps meet tweens.
|
||
|
||
[ga]: https://github.com/hi-godot/godot-ai
|
||
[gcs]: https://github.com/alexmeckes/godot-claude-skills
|
||
[gp]: https://github.com/jame581/GodotPrompter
|
||
[rcs]: https://github.com/Randroids-Dojo/Godot-Claude-Skills
|