diff --git a/.gitignore b/.gitignore index 2e9c688a..72134dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,3 @@ Thumbs.db # Trunk build output dist/ __pycache__/ - -# The Godot port export is generated from the user's own disc. -# Code, schemas and authored mappings are versioned; game assets are not. -export/ diff --git a/docker/agent/AGENT.md b/docker/agent/AGENT.md index c041c94b..695d6c75 100644 --- a/docker/agent/AGENT.md +++ b/docker/agent/AGENT.md @@ -55,28 +55,6 @@ python3 tools/re-capture/gmem.py find hex:820af844 400 * A Bevy system-parameter conflict is invisible to the type checker and panics at startup. If you touch viewer systems, *run the binary*, don't just build it. -## The Godot port - -The primary objective — see `docs/port/MISSION.md`. Two binaries are installed: - -```bash -godot-headless --path port/ --script res://tools/check.gd # no display needed -DISPLAY=:99 godot --path port/ # windowed, under Xvfb -screenshot ~/shots/godot.png # then capture it -``` - -* **Godot is pinned to one version** in the Dockerfile. An engine bump changes - rendering, and this project diffs Godot's output against a reference renderer — - so do not upgrade it to fix a bug without saying that is what you did. -* **`sylpheed-cli screen render` is the reference.** When Godot draws a screen, - render the same build with the CLI and compare. Where they disagree, one of - them is wrong; say which, and why, rather than tuning until they match. -* **ffmpeg has libtheora**, which is the transcode target for video. Keep the - exact command in the exporter config — a modder who dislikes the quality should - be able to re-run it, not reverse-engineer what you did. -* **Never commit anything under `export/`.** It is generated from the user's own - disc and gitignored. Code, schemas, `authored/` mappings and docs only. - ## Reporting State what you measured, what you assumed, and what you could not settle. If a diff --git a/docker/agent/Dockerfile b/docker/agent/Dockerfile index d130dff7..e4b320a4 100644 --- a/docker/agent/Dockerfile +++ b/docker/agent/Dockerfile @@ -68,26 +68,6 @@ RUN for t in clang clang++ lld ld.lld llvm-ar llvm-ranlib llvm-nm clang-cpp; do # Python consumer to protect, so installing into it is the honest simple option. RUN pip3 install --no-cache-dir --break-system-packages duckdb -# ── Godot 4 ────────────────────────────────────────────────────────────────── -# The port's runtime (docs/port/MISSION.md). Two binaries, deliberately: -# -# godot the editor/windowed build, run under Xvfb, for screenshots -# that can be diffed against `sylpheed-cli screen render` -# godot-headless --headless, for script-only checks that need no display -# -# The official builds are self-contained binaries, not a package, so they are -# fetched rather than apt-installed. Pinned: an engine version bump changes -# rendering, and this project compares screenshots against a reference renderer. -ARG GODOT_VERSION=4.3 -RUN cd /tmp \ - && curl -fsSLO "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}-stable/Godot_v${GODOT_VERSION}-stable_linux.x86_64.zip" \ - && unzip -q "Godot_v${GODOT_VERSION}-stable_linux.x86_64.zip" \ - && mv "Godot_v${GODOT_VERSION}-stable_linux.x86_64" /usr/local/bin/godot \ - && chmod +x /usr/local/bin/godot \ - && printf '#!/bin/sh\nexec /usr/local/bin/godot --headless "$@"\n' > /usr/local/bin/godot-headless \ - && chmod +x /usr/local/bin/godot-headless \ - && rm -f "Godot_v${GODOT_VERSION}-stable_linux.x86_64.zip" - # ── Node + Claude Code ─────────────────────────────────────────────────────── RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ diff --git a/docker/agent/loop-task.md b/docker/agent/loop-task.md index a32c2b4d..cd5a0d79 100644 --- a/docker/agent/loop-task.md +++ b/docker/agent/loop-task.md @@ -1,49 +1,60 @@ -Build the Godot menu port of Project Sylpheed, one milestone at a time. +Answer the open questions the Godot menu port is blocked on, one at a time. ## Your objective -`Syplheed-Reborn/docs/port/MISSION.md` — read it every iteration. It defines the -milestones (P0…P7 plus a gated S1), the gate each one must pass, and the two -decisions already made. Work the **lowest unfinished milestone**; do not skip -ahead because a later one looks more interesting. +`Syplheed-Reborn/docs/port/MISSION.md` — read it every iteration. It lists the +open questions Q1…Q9 plus a gated probe S1, and the gate each one must pass. -Reverse engineering has not stopped, but it is no longer the goal. An RE item -earns attention when the port is blocked on it, and the write-up still goes in -`docs/re/` under the usual convention. +**You do not build the port.** A separate agent does that, from what you produce. +Your deliverable is decoded, verified, written-down answers with the evidence. +If you find yourself designing an export schema or writing GDScript, you have +crossed the line — go back to the question you were answering. + +This is still reverse engineering. What changed is what earns attention: an item +is worth doing when the menu port is blocked on it. ## Read these first, every iteration Short on purpose, and the reason this prompt is short: -1. `docs/port/MISSION.md` — the objective, the milestones, what is out of scope. -2. `docs/port/FORMAT.md` — the export schema. It is **versioned**; changing it is - a deliberate act with a version bump, not a silent edit. +1. `docs/port/MISSION.md` — the open questions, their gates, what is out of scope. +2. `docs/port/HANDOFF.md` — what the port agent has been told so far. **Update it + when you answer something.** An answer not reachable from that page has not + been delivered. 3. `docs/re/REFUTED.md` — claims already tested and dead. Grep it for your nouns before designing anything. 4. `docs/re/METHOD.md` — the traps this corpus has already paid for. 5. `docs/re/INDEX.md` — what is already decoded. Re-deriving a ✅ row is not a - finding. + finding; asking whether its values *resolve* is. 6. `docker/agent/AGENT.md` — the container's tooling. -`docs/re/disc-atlas.html` is the map of how the assets reference each other — -useful when you need to find what feeds what. +`docs/re/disc-atlas.html` maps how the assets reference each other — useful when +you need to find what feeds what. **These files are the memory.** A finding that lives only in your context is lost when the container dies. ## Each iteration -1. **Pick the lowest unfinished milestone** from MISSION.md. If you are mid-item, - continue it rather than starting another. -2. **Build the smallest thing that reaches its gate.** The gate is an *artifact* — - a validating JSON file, a screenshot, a human-clickable build — never "it - compiles". -3. **Keep derived and authored apart.** `export/` is regenerated wholesale and is - never hand-edited; `authored/` is hand-written and survives a re-export. If you - are tempted to hand-fix a file under `export/`, the fix belongs in the exporter - or in `authored/`. Every `authored/` entry carries a `why`. -4. **Write down what you learned** — in `docs/port/` for port decisions, in - `docs/re/` for anything about the disc. +1. **Pick one question** from MISSION.md, preferring the one that blocks the port + earliest and whose first step is cheapest. If you are mid-question, continue it + rather than starting another. +2. **Do the smallest experiment that could settle it**, and try to *refute* your + hypothesis before believing it. Run the known-positive through any new filter + first; a filter that fails its own control is dead, not tuneable. +3. **Classify the answer honestly.** Every answer is exactly one of: + * **decoded** — the field, plus a disc-wide check; + * **measured** — not on the disc in any form you found, but here is what the + running game does, and here is the capture; + * **undecodable, with reach** — you looked here, here and here, and this is + why it is not there. + + Never a fourth thing. *Measured* and *undecodable* mean the port agent will + author that value by hand, and it must know it is authoring rather than + transcribing. Labelling a guess as a decode puts it into the port wearing the + badge of a measurement. +4. **Write it down** in `docs/re/` under the ✅/🟡/❔ convention, with the evidence + and the *reach* of any negative, then update the row in `docs/port/HANDOFF.md`. * Refuted something → a line in `REFUTED.md`. * Bitten by a general trap → a line in `METHOD.md`. * Closed a format → update its `INDEX.md` row. @@ -53,39 +64,40 @@ when the container dies. ## Hard rules -* **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. -* **No Rust in the Godot project, no GDExtension.** If Godot cannot read - something, the exporter emits it differently. The wall between the two halves is - the design, not an inconvenience. -* **Do not touch `crates/sylpheed-viewer`.** The Explorer is the human's - verification tool, it is independent of the port, and it keeps its - static-data-only rule. Do not refactor it to share code with the exporter. +* **Do not build the port.** No Godot project, no GDScript, no asset pipeline, no + export schema, no transcoding. Those belong to the port agent. +* **Do not touch `crates/sylpheed-viewer`.** The Explorer is the human's tool for + exploring and verifying the RE work; it keeps its static-data-only rule and the + port does not depend on it. * **Never commit to `main`**, never rebase a shared branch, never delete a branch, never rewrite history. * **Do not touch another agent's worktree.** `git worktree list` first; branches marked `+` are checked out elsewhere. * **One emulator at a time** — `run-canary` enforces it with a lockfile. -* **Measure the oracle; never infer it.** If you need to know what the real game - does, run it. This applies with full force to the keyframe time unit and to any - transition timing. -* **Do not improvise around a blocker.** If a milestone needs a decision only the - user can make — a new dependency, a scope change, the Ready Room go/no-go — - write what you found, note it in MISSION.md, and move to the next thing you can - actually finish. +* **Measure the oracle; never infer it.** Most of the open questions are about + *behaviour* — timing, transitions, what a button does, what a d-pad press does + at the end of a list. Those cannot be answered from the file. An iteration that + reasons about the game without running it is a red flag unless the question is + a pure static-format one. +* **Do not improvise around a blocker.** If a question needs a decision only the + user can make, or the container cannot do it, write what you found, note it in + MISSION.md, and move to the next question you can actually finish. + +## The S1 probe + +One iteration, then **stop and write the go/no-go**. Do not start Ready Room work +on your own authority — MISSION.md §S1 says why. ## Verifying * `build-reborn test` wires up `SYLPHEED_DISC`; without it the disc tests self-skip and a green run means almost nothing. -* `sylpheed-cli screen render` is the reference renderer. When Godot draws a - screen, diff it against the CLI's composite of the same build — they should - agree, and where they do not, one of them is wrong and you must say which. -* Godot runs headless in this container (`godot --headless`), and - `screenshot` captures the windowed one under Xvfb. -* A regenerated artifact that comes out byte-identical is strong evidence a - change was additive. When it does change, check that every diff line pairs. +* Verify with an **artifact**, not with "it compiles": `sylpheed-cli screen + info` / `screen render` / `mesh render` / `save info`, a capture, a screenshot. +* Commit the reference data beside the finding, so the port can be built without + a disc in the loop during development. +* A regenerated artifact that comes out byte-identical is strong evidence a change + was additive. When it does change, check that every diff line pairs exactly. ## Publishing @@ -100,8 +112,8 @@ no alternate transport. A push that is blocked is a blocked push. ## Pacing -One milestone step plus its write-up is a good iteration; a marathon is not. Stop -with a clean commit, a push, and an honest list of what is still open. +One experiment plus its write-up is a good iteration; a marathon is not. Stop with +a clean commit, a push, and an honest list of what is still open. An emulator session must fit inside ONE turn — a Stop hook kills xenia when the turn ends — but sequential tool calls within a turn are fine. diff --git a/docs/port/FORMAT.md b/docs/port/FORMAT.md deleted file mode 100644 index 51561d0e..00000000 --- a/docs/port/FORMAT.md +++ /dev/null @@ -1,205 +0,0 @@ -# The open export format — v1 - -The format the disc is converted *into*, and the one the Godot project and any -modding tool read. Versioned: every file names its schema, and a breaking change -bumps the number rather than editing this page quietly. - -**Design rules, in priority order:** - -1. **A human can read and edit it.** That is the whole point — modding is the - second goal of the port. -2. **Names, never hashes.** If we never recovered the disc's name, say so in the - file rather than passing off a synthetic id as real. -3. **Provenance travels with the data.** Every generated file says where on the - disc it came from and which exporter version wrote it, so it stays auditable - against the original instead of drifting into an unverifiable fork. -4. **Say what is unknown.** A field we could not decode is absent and listed in - `unresolved`, never guessed and never silently defaulted. - -## Layout - -``` -export/ # DERIVED. Regenerable. Gitignored. Never hand-edited. - manifest.json # what this export is: disc id, exporter version, contents - screens/ - title/ - splash_developer.json - title.json - main_menu.json - submenu_extras.json - sprites/ # PNG, RGBA8, one per T8aD surface - audio/ - music/BGM_001.ogg - sfx/… - cues.json # cue name → sound id → bank, straight off the disc table - video/ - ADV.ogv - text/en/… - -authored/ # AUTHORED. Hand-written. Committed. Survives a re-export. - flow.json # boot sequence + what each button does - paint_order.json # per-screen z-order overrides - cue_bindings.json # which cue fires on move / confirm / back -``` - -`export/` is wiped and rewritten wholesale. `authored/` is never touched by the -exporter. Godot loads `export/` first and then applies `authored/` over it. - -## Common header - -Every generated file starts with the same three keys: - -```json -{ - "format": "sylpheed.screen/1", - "exporter": "sylpheed-export 0.1.0", - "source": { "archive": "dat/GP_TITLE.pak", "entry": 5 } -} -``` - -`source.entry` is the pak **entry index**, which is the stable locator — not the -display ordinal, which renumbers whenever the enumeration rule changes. - -## `screens/*.json` - -```json -{ - "format": "sylpheed.screen/1", - "exporter": "sylpheed-export 0.1.0", - "source": { "archive": "dat/GP_TITLE.pak", "entry": 5 }, - "name": "main_menu", - "name_source": "authored", - "design": [1280, 720], - - "elements": [ - { - "id": "ptbase", - "sprite": "sprites/ptbase.png", - "role": "decoration", - "pivot": [320, 180], - "rest": { "pos": [320, 180], "scale": [1.0, 1.0], "tint": "#ffffffff" }, - "keyframes": [] - }, - { - "id": "ptbtn01", - "sprite": "sprites/ptbtn01.png", - "focus_sprite": "sprites/ptbtn01f.png", - "role": "button", - "pivot": [42, 22], - "rest": { "pos": [542, 162], "scale": [1.0, 1.0], "tint": "#ffffffff" }, - "keyframes": [ - { "t": 28, "pos": [542, 142], "scale": [1.0, 1.0], "tint": "#ffffffff" }, - { "t": 34, "pos": [542, 157], "scale": [1.0, 1.0], "tint": "#ffffffff" }, - { "t": 64, "pos": [542, 162], "scale": [1.0, 1.0], "tint": "#ffffffff" } - ] - } - ], - - "buttons": ["ptbtn01", "ptbtn02", "ptbtn03", "ptbtn04", "ptbtn05"], - "unresolved": ["paint_order"] -} -``` - -**`role`** is derived from the element kind, which is a decoded field: -`0x3002` → `button`, `0x10` → `primitive`, `0x0` → `decoration`. Anything else -exports as `"unknown"` with the raw value in `kind_raw` — do not invent a name -for a kind nobody has decoded. - -**`buttons`** is the navigation order: the `button`-role elements sorted by -resting Y. This is geometric, not a decoded neighbour graph — the disc's real -navigation structure is unknown, and `opt ` is **not** a focus link (that was -measured and refuted). The ordering is right for a vertical menu and should not -be trusted for anything else. - -**`keyframes`** carry the on-disc time value verbatim in `t`. A keyframe is the -**start of a ramp toward the next one**, not a pose that is held. The unit of `t` -is not yet confirmed — see MISSION.md §7 — so `t` stays raw and the consumer -applies the conversion in one place. - -**`rest`** is the resting pose: the longest run of consecutive keyframes with an -unchanged value, falling back to longest-dwell for groups that ramp throughout. -It is neither the first nor the last keyframe. - -**`unresolved`** lists what this file does *not* answer. A consumer that needs -one of these must get it from `authored/`. - -## `authored/flow.json` - -The part the disc has not told us. Every entry states its evidence. - -```json -{ - "format": "sylpheed.flow/1", - "boot": ["splash_developer", "intro_video", "title", "main_menu"], - - "screens": { - "main_menu": { - "actions": { - "ptbtn01": { "label": "NEW GAME", "goto": "new_game_intro", - "why": "label read off the sprite; target is a guess" }, - "ptbtn02": { "label": "LOAD GAME", "goto": "GP_LOAD", - "why": "label read off the sprite; GamePart id 3 from the .rdata table" } - }, - "back": null, - "why_back": "no B-handler observed; the main menu may be a root with no back" - } - } -} -``` - -`goto` may name another exported screen or a **GamePart id** from the -executable's own table (29 entries at `.rdata 0x820A1630` — that table is a disc -fact; which button reaches which entry is not). - -## `authored/paint_order.json` - -```json -{ - "format": "sylpheed.paint_order/1", - "screens": { - "main_menu": { - "order": ["ptbase", "pteff05", "ptframe1", "ptbtn01", "…"], - "why": "hand-authored. The declaration table is NOT the paint order -- a - per-draw capture of the running title paints element 13 first and - elements 0/1 late. Deriving the order from the bundle is open." - } - } -} -``` - -When paint order is derived from the file, this file is deleted and `screens/*` -gains an `order` array. **That deletion is the measure of progress** — until it -happens, every screen here is a hand-tuned approximation wearing the right label. - -## `export/audio/cues.json` - -Straight off the disc's own cue index; no interpretation. - -```json -{ - "format": "sylpheed.cues/1", - "source": { "archive": "dat/tables.pak", "record": "SOUNDS" }, - "families": { "SE": [1, 901], "BGM": [1001, 1109], "VOICE": [1500, 7331] }, - "cues": { "SE_CURSOR_MOVE": 12 }, - "banks": { "12": "audio/sfx/se_012.ogg" } -} -``` - -Which cue a menu *event* fires is not on the disc as far as we know, so it is in -`authored/cue_bindings.json`, not here. - -## `export/manifest.json` - -```json -{ - "format": "sylpheed.manifest/1", - "exporter": "sylpheed-export 0.1.0", - "disc": { "title": "Project Sylpheed: Arc of Deception", "region": "USA/Europe" }, - "video_transcode": "ffmpeg -i ADV.wmv -c:v libtheora -q:v 8 -c:a libvorbis -q:a 5 ADV.ogv", - "screens": ["title/splash_developer", "title/title", "title/main_menu"], - "warnings": ["GP_READY_ROOM not exported -- see MISSION.md §6"] -} -``` - -`video_transcode` records the exact command used, so a modder who dislikes the -quality can re-run it rather than reverse-engineer what we did. diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md new file mode 100644 index 00000000..5fd8ee32 --- /dev/null +++ b/docs/port/HANDOFF.md @@ -0,0 +1,93 @@ +# Handoff — what the menu port needs, and where it stands + +The single page the **port agent** reads. Everything here is produced by the +container agent's reverse engineering; nothing here is a design decision about +the port itself. + +Keep it current. It is a summary with links into `docs/re/`, not a second copy of +the findings — but an answer that is not reachable from this page has not been +delivered. + +## How to read an answer + +Every row below is one of exactly three things, and the distinction is the point: + +| | meaning | what the port should do | +|---|---|---| +| **decoded** | a field on the disc, with a disc-wide check | read it from the data | +| **measured** | not on the disc in any form we found, but the running game does *this* | hardcode it, and cite this page | +| **undecodable** | we looked in these places, it is not there, here is the reach of the negative | author it by hand, knowingly | + +There is no fourth kind. If a row says *measured* or *undecodable*, the port is +**authoring** that value, not transcribing it — and it should be kept somewhere a +human can see it is a human decision, so that when it is later decoded the +authored version can be deleted. + +## Status + +| | Question | State | Answer / link | +|---|---|---|---| +| Q1 | keyframe time unit + ramp shape | ❔ open | blocks all animation | +| Q2 | which build is which screen state | 🟡 partial | build 4 title, 5 main menu, 6/8/9 submenus, `palogo` splash — unconfirmed against captures | +| Q3 | paint order for the six screens | ❔ open | runtime-solved only; declaration table is refuted | +| Q4 | button → GamePart | ❔ open | labels are baked into sprites | +| Q5 | navigation semantics | ❔ open | | +| Q6 | boot sequence + what drives it | 🟡 partial | order observed; the driver is not decoded | +| Q7 | transitions | ❔ open | | +| Q8 | menu audio bindings | ❔ open | cue table complete, event binding is not | +| Q9 | video binding + playback rules | 🟡 partial | `ADV.wmv` is the boot intro; new-game intro unidentified | +| S1 | Ready Room go/no-go | ❔ open | probe not run | + +## Already settled — the port can rely on these today + +* **`GP_TITLE.pak` is the whole title-side tree.** Build 4 is the title with the + animating wordmarks, build 5 the five-button main menu, builds 6/8/9 are + submenus, and the developer splash is the `palogo` bundle in the same archive. + ✅ decoded (enumeration), 🟡 the state labels are not yet capture-confirmed. +* **Buttons are identifiable as data.** Element kind `0x3002` = button, `0x0` = + decoration, `0x10` = primitive. ✅ decoded. +* **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is + ✅ correct for a vertical menu and is **not** a decoded neighbour graph — the + disc's real navigation structure is unknown, and `opt ` is *not* a focus link + (measured and refuted, see + [`ui-focus-and-effect-elements.md`](../re/structures/ui-focus-and-effect-elements.md)). +* **Highlighted states pair by name** — `ptbtn01.rat` ↔ `ptbtn01f.rat`. 🟡 a + naming convention that holds for all 54 real pairs, not a decoded field. +* **The resting pose is the hold**, not the first, last or longest-dwell keyframe; + a keyframe is the **start of a ramp**. + [`ui-resting-pose.md`](../re/structures/ui-resting-pose.md). ✅ +* **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the + executable's own registration strings. ✅ This is the screen vocabulary; which + button reaches which entry is Q4 and is *not* part of it. +* **The logo splash is a screen, not a video.** `logo1`–`logo4` are + manifest-bound with no `.wmv` on the disc. ✅ +* **Sprites carry their own labels.** No font rendering or localisation is needed + for this milestone. ✅ + +## Facts the port will trip over + +* **`ADV.wmv` is WMV3 video + WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4 + plays only Ogg Theora natively. How to handle that is the port's decision, not + ours — but it is not optional. +* **The disc holds 3.3 GB of video.** Only the boot intro and the one new-game + intro are in scope. +* **`Static.slb` over-declares its size** by 616 768 bytes — it is the + highest-offset entry in `sound.pak` and its size field is an allocation size. A + reader must allow a short read there and only there. +* **Voice downmixes to mono, music does not.** The left-channel downmix is correct + for spoken lines and discards half a music mix. +* **`JNGL_001.slb` does not decode.** One bank in 9 519; its payload is not a whole + number of XMA1 packets from any known data offset. + +## Reference data + +Committed alongside the findings, so the port can be built without a disc in the +loop during development: + +* `sylpheed-cli screen info --build GP_TITLE.pak` — the element table, per + build, with pivots, kinds, focus links, keyframes and resting poses. +* `sylpheed-cli screen render` — the reference composite. When the port draws a + screen, this is what it should be diffed against; where they disagree, one of + them is wrong and the disagreement is worth reporting back. +* `docs/re/captures/` — framebuffer captures of the real screens, for anything + that has to be checked against the game rather than against our renderer. diff --git a/docs/port/MISSION.md b/docs/port/MISSION.md index a734233e..d691adb0 100644 --- a/docs/port/MISSION.md +++ b/docs/port/MISSION.md @@ -1,171 +1,131 @@ -# Primary objective — a playable menu shell in Godot +# Primary objective — answer everything the menu port needs -**Status:** active, set 2026-08-28. This supersedes "work the RE backlog" as the -agent's primary objective. Reverse engineering continues, but it is now *in -service of* this goal: an item earns attention because the port needs it. +**Status:** active, set 2026-08-28. This replaces "work the RE backlog" as the +agent's primary objective. It does not change what the agent *does* — it is still +reverse engineering — it changes what earns attention: an item is worth doing +when the menu port is blocked on it. -## What we are building +## Who builds what -A Godot 4 project that boots to an interactive main menu using the player's own -disc, through the sequence the real game uses: +**You do not build the port.** A separate agent will build it, from what you +produce. Your deliverable is decoded, verified, written-down answers plus the +reference data that proves them. + +| | container agent (you) | port agent | +|---|---|---| +| decodes the disc | ✅ | ❌ — consumes your answers | +| measures the running game | ✅ | ❌ — no emulator | +| writes `docs/re/` and `docs/port/HANDOFF.md` | ✅ | reads them | +| Godot project, asset pipeline, transcoding | ❌ | ✅ | + +If you find yourself designing an export schema or writing GDScript, you have +crossed the line. Stop and go back to the question you were answering. + +`crates/sylpheed-viewer` is also **not yours to change** for this objective. The +Explorer is the human's tool for exploring and verifying the RE work, it keeps its +static-data-only rule, and the port does not depend on it. + +## The target + +Someone else has to build this, from your answers alone: ``` developer logo splash → intro video → title / PRESS Ⓐ → main menu → submenus ``` -No gameplay. No 3D. No emulator. The success condition is a human pressing a -d-pad and Ⓐ and moving through those screens with the right art, the right -animation, the right music and the right transitions. +No gameplay, no 3D, no HUD, no missions. If an answer is not needed to put those +five screens on a display and let a person move through them with a d-pad and Ⓐ, +it is not in this objective. -**Ready Room is a gated stretch goal.** See §6 — do the probe first, then stop -and report rather than committing to it. +## What is already answered -## 1. The two halves, and the wall between them +Do not re-derive these. They are in `docs/re/` and the +[disc atlas](../re/disc-atlas.html): -The Godot project **never reads a disc format**. It reads only open formats. The -conversion happens offline, in Rust, and the wall between the two is the point: +* **The screen archive.** `GP_TITLE.pak` holds the whole title-side tree — build 4 + the title with animating wordmarks, build 5 the five-button main menu, builds + 6/8/9 submenus, and the developer splash as the `palogo` bundle in the same pak. +* **Buttons are identifiable as data.** Element kind `0x3002` is a button, `0x0` + decoration, `0x10` a primitive; buttons sort top-to-bottom by resting Y; each + pairs with an `f`-suffixed highlighted variant. +* **The screen vocabulary.** The GamePart id table, 29 entries at `.rdata + 0x820A1630`, confirmed by the executable's own factory-registration strings. +* **The resting pose rule** — the hold, not the longest dwell — and that a + keyframe is the *start of a ramp*. +* **Screen composition**, pixel-accurate for the tutorial pause menu and the title + main menu, via `sylpheed-cli screen render`. +* **The logo splash is not a video.** `logo1`–`logo4` are manifest-bound with no + `.wmv` on the disc; the splash is the RATC screen, which already renders. -``` - disc ──▶ crates/sylpheed-export ──▶ export/ ──▶ port/ (Godot 4) - (Rust, uses sylpheed-formats) JSON + PNG + OGG reads ONLY - + Ogg Theora open formats -``` +## The open questions — these are the objective -* **No GDExtension. No Rust in the Godot project.** If Godot needs something it - cannot read, the exporter's job is to emit it in a form Godot can, not to - bridge the two runtimes. -* **`crates/sylpheed-viewer` is not involved.** The Explorer is the human's tool - for exploring and verifying the RE work. It stays Rust/Bevy, it keeps its - static-data rule, and it is neither a dependency of nor a dependent on the - port. Do not refactor it "to share code with" the port. -* `sylpheed-formats` is shared, because it is the decoder and there must be one. +Ordered by what blocks the port earliest. Each is done when its **gate** exists: +a written `docs/re/` result with the evidence, and reference data committed +alongside it. -## 2. The format is a deliverable, not a dump - -The second goal of this port is **modding**. That makes the exported layout part -of the product, not a temp directory: - -* **JSON, not XML.** Godot parses JSON natively with `JSON.parse_string`; its - `XMLParser` is a SAX-style API that would need a hand-written binding for every - schema. JSON is also what every external tool a modder might reach for already - speaks. -* **Names, never hashes.** A file is `screens/title/main_menu.json`, not - `0x90822a39.json`. Where the disc's own name was never recovered — the six - `*2D` archives and `GP_READY_ROOM`, see [the atlas](../re/disc-atlas.html) — - emit a stable synthetic id **and record in the file that the real name is - unknown**. A modder must be able to tell a recovered name from one we invented. -* **A tree that means something.** `screens/`, `sprites/`, `audio/music/`, - `audio/sfx/`, `video/`, `text//` — organised by what a person is looking - for, not by which archive it came out of. -* **Provenance in every generated file.** Source archive, entry index, and the - exporter version. This is what keeps the export auditable against the disc - instead of drifting into an unverifiable fork. - -The schema lives in [FORMAT.md](FORMAT.md) and is **versioned**. Changing it is a -deliberate act with a version bump, not a silent edit. - -## 3. Derived vs authored — keep these apart - -This is the discipline that matters most, and it is the corpus's own rule applied -to a new artefact. - -| | `export/` | `authored/` | +| | Question | Gate | |---|---|---| -| produced by | running the exporter | a human or the agent, by hand | -| contains | what the disc says | what we decided | -| edited by hand | **never** | always | -| committed to git | **no** (see §4) | yes | -| when regenerated | overwritten wholesale | untouched | +| **Q1** | **What is a keyframe time?** Values run 16…269. 60 Hz frames would make the title intro ~4.5 s — plausible and untested. Also: is the ramp linear, or eased? | A measured answer against the running game, not an inference. Everything animated downstream depends on this number | +| **Q2** | **Which build is which screen state?** Confirm build↔state for splash, title/PRESS Ⓐ, main menu and each submenu | A table, each row confirmed against a capture of the real screen | +| **Q3** | **Paint order for these six screens.** Solved at runtime, unsolved from the file — the declaration table is provably not it | Either a rule derived from the bundle, or six measured orders and a clear statement that no file-side rule was found | +| **Q4** | **What does each button do?** Labels are baked into the sprites; no decoded field says which GamePart a button opens | Button → GamePart id, from code or from driving the game. Say which | +| **Q5** | **Navigation semantics.** Initial focus, wrap-around at the ends, whether left/right does anything, what B does on each screen | Observed behaviour, per screen | +| **Q6** | **The boot sequence, and what drives it.** Order is observable; the *data or code* that sequences it is not decoded. Include the attract loop and what returns to the title | The sequence, plus whatever the game reads to decide it | +| **Q7** | **Transitions.** What happens visually between screens — the `pteff00.prm` quads, a fade, a cut — and its timing | Described and timed against a capture | +| **Q8** | **Menu audio.** Which BGM per screen; which cue on move / confirm / back / error. The cue table is complete; the event binding is not | Cue names bound to events, with how you established each | +| **Q9** | **Video binding.** Which movie is the boot intro vs the new-game intro; whether playback is skippable and what ends it | Named movies plus the playback rules | +| **S1** | **Ready Room probe.** *Gated* — one iteration, then stop | A written go/no-go (see below) | -Some things this milestone needs are **not on the disc in any form we have -decoded**, and they must live in `authored/` with a stated reason: +## Known unknowns — say so, do not fill them in -* **Which button does what.** `ptbtn01…05` are sprites with baked-in labels. A - human can read "NEW GAME" off the image; there is no field that says so. The - mapping button → GamePart id is an authored guess until code says otherwise. -* **Paint order.** Solved at runtime, unsolved from the file — the declaration - table is provably not it. For these six screens, author the z-order and mark it - authored. Do **not** let a hand-tuned order leak into `export/` and later get - mistaken for a disc fact. -* **Sound-cue bindings.** Which cue fires on move / confirm / back. +Some of these may turn out to be undecodable. That is a valid, useful answer, and +it is better than a guess, because the port agent will otherwise have to author +the mapping by hand and needs to know it is authoring rather than transcribing. -Every `authored/` entry carries a `why` field. When a later iteration decodes the -real answer, the authored entry is deleted and the exporter emits it instead — -and that transition is the measure of progress. +For each question, the answer is one of: -## 4. Do not commit game assets +* **decoded** — here is the field, here is the disc-wide check; +* **measured** — not on the disc in any form we found, but here is what the + running game does, and here is the capture; +* **undecodable, with reach** — we looked here, here and here, and this is why it + is not there. -`export/` is generated from the user's own disc and **must be gitignored**. The -repository stays a clean-room port: code, schemas, authored mappings and -documentation. Anyone building it supplies their own disc. If you find yourself -committing a PNG of a game sprite or a transcoded video, stop. +Never a fourth thing. In particular: if Q4 ends as "read the labels off the sprite +images by eye", say exactly that — it is then an authored mapping on the port +side, not a disc fact, and mislabelling it would put a guess into the port wearing +the badge of a measurement. -## 5. Milestones, each with a gate +## The Ready Room probe (S1) — one iteration, then stop -Do these in order. A milestone is done when its **artifact** exists, not when the -code compiles. +`GP_READY_ROOM.pak` is the largest UI archive on the disc, 1 106 entries, and only +**6 of its names resolve**. It is also ISL-scripted. That is either a week or a +quarter, and one cheap test tells you which. -| | Milestone | Gate (the artifact) | -|---|---|---| -| **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 screenshot from Godot beside `sylpheed-cli screen render` of the same build — they should agree | -| **P2** | Keyframe animation | The main menu's buttons slide in. **Measure the time unit first** (§7) | -| **P3** | Splash → title, with the transition | Godot runs the two screens back to back unattended | -| **P4** | Intro video | `ADV.wmv` plays in Godot with audio (§7) | -| **P5** | Main menu: d-pad navigation, focus states, Ⓐ into a submenu, B back | A human clicks through it | -| **P6** | Audio — menu BGM and move/confirm SFX | Sound on the gate in P5 | -| **P7** | New-game intro video after NEW GAME | Plays, then returns to a defined state | -| **S1** | *Gated* — Ready Room. Run the probe in §6 and **stop** | A written go / no-go | - -## 6. The Ready Room probe — do this early, it is one iteration - -`GP_READY_ROOM.pak` is the largest UI archive on the disc, 1 106 entries, and -only **6 of its names resolve**. It is also ISL-scripted. That could mean a week -or a quarter, and one cheap test tells you which. - -**The probe:** our screen catalog enumerates bundles by **content**, not by name -— `is_build` / `is_composable` read the bytes. Unrecoverable *paths* therefore do -not necessarily mean unrenderable *screens*. +Our screen catalog enumerates bundles by **content**, not by name — `is_build` / +`is_composable` read the bytes — so unrecoverable *paths* do not necessarily mean +unrenderable *screens*. ```bash sylpheed-cli screen list "$SYLPHEED_DISC/dat/GP_READY_ROOM.pak" sylpheed-cli screen render --build "$SYLPHEED_DISC/dat/GP_READY_ROOM.pak" /tmp/rr.png ``` -Report: how many builds it finds, whether any composite looks like a Ready Room, -and whether the screens are 2D at all or whether the room is 3D with a UI overlay -— because if it is 3D, this milestone is out of scope by definition and the -answer is no-go, not "try harder". +Report how many builds it finds, whether any composite looks like a Ready Room, +and **whether the room is 2D at all** or 3D with a UI overlay — if it is 3D the +answer is no-go by definition, not "try harder". **Then stop and write the go/no-go.** Do not start Ready Room work on your own authority. -## 7. Two decisions already made, and one measurement owed +## Handing it over -**Video: transcode to Ogg Theora.** `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 **keep the exact command in the exporter's config** so it can be re-run -with different settings. Theora at 720p is not great; if the result is visibly -poor, say so and propose the FFmpeg-GDExtension fallback — do not silently adopt a -dependency. Only `ADV.wmv` and the one new-game intro are in scope; the disc holds -3.3 GB of video and transcoding all of it is not this milestone. +[`HANDOFF.md`](HANDOFF.md) is the single page the port agent reads. Keep it +current as you answer questions: it is a summary with links into `docs/re/`, not a +second copy of the findings. An answer that is not reachable from HANDOFF.md has +not been delivered. -**Screens: `GP_TITLE.pak` is the whole title-side tree.** Build 4 is the title -with the animating wordmarks, build 5 the five-button main menu, builds 6/8/9 are -submenus, and the developer splash is the `palogo` bundle in the same archive. -Buttons are identifiable as data: element kind `0x3002`, ordered top-to-bottom by -resting Y, each paired with an `f`-suffixed highlighted variant. You do **not** -need the undecoded navigation graph to make a vertical menu work. +## Out of scope -**The measurement owed: what is a keyframe time?** Values run 16…269 on the title -build. 60 Hz frames would make the title intro ~4.5 s, which is plausible and -untested. Settle it against a capture before building animation on it — this is -exactly the kind of number that silently poisons everything downstream. -[`ui-resting-pose.md`](../re/structures/ui-resting-pose.md) already establishes -that a keyframe is the **start of a ramp**, not a pose that is held. - -## 8. What is explicitly out of scope - -3D, gameplay, HUD, missions, the emulator, save/load, localisation beyond English, -and any archive outside `GP_TITLE`, `tables.pak`, `sound.pak` and `dat/movie/` -— except for the Ready Room probe in §6. +3D, gameplay, HUD, missions, save/load, localisation beyond English, the Godot +project itself, any asset pipeline, and any archive outside `GP_TITLE`, +`tables.pak`, `sound.pak` and `dat/movie/` — except for the S1 probe.