Files
Sylpheed/docs/port/PORT-MISSION.md
MechaCat02 c3758e3850 port: land the play-tested work, and only that
Takes the port branch up to 77320d5e -- the state the human play-tested on
2026-09-02 -- for SOURCE paths only. Not a branch merge: `auto/port-p6-audio`
is 366 commits and 938 files, and most of that must not land.

WHAT COMES IN (76 files, all human-confirmed working):
  * the logo splash animation. 08ed3dd1 found it: `pose_at` ASSIGNED the settle
    instant instead of clamping to it, so the splash never animated at all --
    and the same bug manufactured a passing harness result, because the harness
    photographed t past the settle. Confirmed by play-test: "cannot notice any
    obvious difference from the actual game."
  * gamepad input -- (A)/(B) bound additively (`ui_accept` ships with NO joypad
    binding), stick latched with hysteresis at the game's own 61% digitise
    threshold. This is what made (A), video-skip and Extras work at all.
  * menu navigation and flow, menu audio, the exporter, the authored
    declarations, and 23 verification tools under tools/port/.

WHAT IS DELIBERATELY LEFT ON THE BRANCH:
  * everything after c0ae460a -- the F5/F6 title-timing investigation, whose own
    tip commit calls itself a "hand-off for one-minute human checks". Unchecked
    by definition; it goes through the new review gate like anything else.
  * the OPTIONS menu work of 2026-09-03. Real, probably good, NOT play-tested.
  * the F1 repeat mechanism, which its own commit calls "deliberately inert".

WHAT MUST NOT LAND, AND WHY THE .gitignore CHANGED:
  545 MB of extracted game content was committed on that branch -- 850 sprite,
  audio and transcoded video files under `export-probe/` and `export-probe2/`,
  plus 246 MB of loose .wav and .tsv at the repo root. This repository's own
  rule, in this file, is "never game content".

  The rule was not missing. It was written, and it was tightened on that very
  branch, with a careful comment explaining why BOTH `export/` and `data/base/`
  had to be listed -- while the exporter was writing to a third name that
  nobody had thought to list. Enumerating names is the thing that failed. So
  the ignore rules now describe the SHAPE: any top-level `export*/`, game media
  by extension, and loose capture output at the root. Verified both ways -- it
  catches all four offenders and ignores nothing currently tracked.

Verified: `cargo check --workspace` clean; all nine GDScript files parse in
project context, with a positive control (an injected syntax error is detected,
3 lines) so the clean result means something. `tools/port/check-all` was NOT
run -- it needs the container, the export tree and a display.
2026-09-04 16:17:14 +02:00

9.7 KiB
Raw Blame History

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 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.

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 itQ1 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 GATE MET 2026-09-02. A human walked it: "Menu walk and navigation is fine. Video skips too. Extras open." ../agents/PLAYTEST-2026-09-02-menus.md
P6 Audio — menu BGM and move/confirm SFX Sound on the P5 gate. Looping is blocked on HANDOFF Q10Q10 is answered: two stems of one performance, played together. 🔴 Gate NOT claimed: the same play-test found the SFX mix wrong (F2), and "sound on the P5 gate" means the right sound.
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/port/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 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.

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, 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, GodotPrompter, Godot-Claude-Skills) 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.