diff --git a/.gitignore b/.gitignore index 72134dc..2e9c688 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ 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 695d6c7..c041c94 100644 --- a/docker/agent/AGENT.md +++ b/docker/agent/AGENT.md @@ -55,6 +55,28 @@ 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 e4b320a..d130dff 100644 --- a/docker/agent/Dockerfile +++ b/docker/agent/Dockerfile @@ -68,6 +68,26 @@ 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 ee56126..a32c2b4 100644 --- a/docker/agent/loop-task.md +++ b/docker/agent/loop-task.md @@ -1,44 +1,91 @@ -Work the Project Sylpheed reverse-engineering backlog, one item at a time. +Build the Godot menu port of Project Sylpheed, one milestone at a time. -## Read these first, every iteration, before proposing anything +## Your objective -They are short on purpose, and they are the reason this prompt is short: +`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. -1. `Syplheed-Reborn/docs/re/REFUTED.md` — **claims already tested and dead.** - If your idea is on that list, it is finished; pick another. Grep it for your - nouns before you design anything. -2. `Syplheed-Reborn/docs/re/METHOD.md` — the traps this corpus has already paid - for. Most wasted iterations are one of these repeated. -3. `Syplheed-Reborn/docs/re/INDEX.md` — what is already **decoded**. Do not - re-derive a row that is already ✅. Re-deriving a known format is not a - finding; asking whether its values *resolve* is. -4. `Syplheed-Reborn/docs/re/BACKLOG.md` — the open items. It is long; skim the - `##` headings and open only the one you pick. -5. `Syplheed-Reborn/docker/agent/AGENT.md` — the container's tooling. +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. -**These files are the memory.** If a finding, a dead end or a trap lives only in -your context, it is lost at the end of the run. Write it down where the next -iteration will find it — that is what makes this a corpus rather than a -transcript. +## 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. +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. +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. + +**These files are the memory.** A finding that lives only in your context is lost +when the container dies. ## Each iteration -1. **Pick one item** from `BACKLOG.md`, preferring the one whose "first step" is - cheapest and most decisive. If you are mid-item, 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. **Write the result down** in `docs/re/` under the ✅/🟡/❔ convention, with the - evidence and the *reach* of any negative. A withdrawn result is a real - result — record it, with the reasoning. - * If you **refuted** something, add a line to `REFUTED.md`. - * If you were bitten by a general trap, add a line to `METHOD.md`. - * If you **closed** a format, update its `INDEX.md` row. -4. **Commit** to `auto/`, one logical change per commit. -5. **Publish**: `push-work`. Your branch must leave the container or the work - dies with it. See "Publishing" below. -6. **Say plainly what you did not settle**, and stop the 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. + * 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. +5. **Commit** to `auto/`, one logical change per commit. +6. **Publish**: `push-work`. Every iteration that produced a commit. +7. **Say plainly what you did not settle**, and stop. + +## 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. +* **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. + +## 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. ## Publishing @@ -47,35 +94,13 @@ transcript. decision. Run it **every iteration that produced a commit** — not at the end of some longer arc, which is exactly when a container dies. -If it reports no credentials, say so in your reply and continue working; do not -improvise another route out (no remote rewrite, no credential helper of your -own, no alternate transport). A push that is blocked is a blocked push. - -## Hard rules - -* **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.** An iteration that reasons about the - game without running it is a red flag unless it is a pure static-format task. -* **Verify with an artifact**, not with "it compiles": `build-reborn test` (it - wires up `SYLPHEED_DISC` — without it the disc tests silently self-skip and a - green run means almost nothing), `sylpheed-cli mesh render` / `screen render` / - `save info`, a screenshot. A regenerated artefact that comes out - byte-identical is a strong check that a change is additive; when it does - change, check that every diff line pairs exactly. - -## When you are blocked - -If an item needs something the container cannot do — hardware Vulkan, a decision -only the user can make — **do not improvise around it**. Write what you found, -note the blocker in `BACKLOG.md`, and move to the next item. +If it reports no credentials, say so in your reply and continue working. Do not +improvise another route out: no remote rewrite, no credential helper of your own, +no alternate transport. A push that is blocked is a blocked push. ## Pacing -One experiment plus its write-up is a good iteration; a marathon is not. Stop +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. An emulator session must fit inside ONE turn — a Stop hook kills xenia when the diff --git a/docs/port/FORMAT.md b/docs/port/FORMAT.md new file mode 100644 index 0000000..51561d0 --- /dev/null +++ b/docs/port/FORMAT.md @@ -0,0 +1,205 @@ +# 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/MISSION.md b/docs/port/MISSION.md new file mode 100644 index 0000000..a734233 --- /dev/null +++ b/docs/port/MISSION.md @@ -0,0 +1,171 @@ +# Primary objective — a playable menu shell in Godot + +**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. + +## What we are building + +A Godot 4 project that boots to an interactive main menu using the player's own +disc, through the sequence the real game uses: + +``` +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. + +**Ready Room is a gated stretch goal.** See §6 — do the probe first, then stop +and report rather than committing to it. + +## 1. The two halves, and the wall between them + +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: + +``` + disc ──▶ crates/sylpheed-export ──▶ export/ ──▶ port/ (Godot 4) + (Rust, uses sylpheed-formats) JSON + PNG + OGG reads ONLY + + Ogg Theora open formats +``` + +* **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. + +## 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/` | +|---|---|---| +| 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 | + +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: + +* **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. + +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. + +## 4. Do not commit game assets + +`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. + +## 5. Milestones, each with a gate + +Do these in order. A milestone is done when its **artifact** exists, not when the +code compiles. + +| | 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*. + +```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". + +**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 + +**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. + +**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. + +**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.