port: the container agent answers the questions, it does not build the port
Some checks failed
Some checks failed
Corrects the split I got wrong in b5a1938. The container is for reverse
engineering, now focused on the menu port; a SEPARATE agent builds the port from
its investigation results. My previous version had the container agent writing
the exporter and the Godot project, which is not the intent.
This lands on the research/engineering line that was already in the estimate:
the container agent takes the research half, the port agent the engineering half.
docs/port/MISSION.md is now a list of open QUESTIONS (Q1-Q9) rather than build
milestones, ordered by what blocks the port earliest -- the keyframe time unit,
which build is which screen state, paint order for the six screens, button ->
GamePart, navigation semantics, the boot sequence driver, transitions, menu
audio bindings, and video binding. Each is done when a written result with
evidence exists, not when something compiles. S1, the Ready Room probe, stays
gated at one iteration and a go/no-go.
Most of these are BEHAVIOUR questions -- timing, transitions, what a d-pad press
does at the end of a list -- so the mission and the loop prompt both push hard
on measuring the oracle rather than reasoning from the file.
docs/port/FORMAT.md is deleted. The export schema is the port agent's design and
was not mine to specify. It is replaced by docs/port/HANDOFF.md, the single page
the port agent reads: a status table, what is already settled and can be relied
on today, and the facts that will trip the port up (the WMV3/WMA Pro intro, the
Static.slb size over-declaration, the voice-vs-music downmix, JNGL_001).
The derived/authored idea survives as the thing it always was -- a finding, not
a design. Every answer must be classified DECODED, MEASURED or UNDECODABLE-with-
reach, and never a fourth thing, because measured and undecodable both mean the
port agent is authoring that value and has to know it. Labelling a guess as a
decode would put it into the port wearing the badge of a measurement.
Reverts the Godot install from the RE container, its AGENT.md section, and the
export/ gitignore entry -- none of that belongs on this side of the wall.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
93
docs/port/HANDOFF.md
Normal file
93
docs/port/HANDOFF.md
Normal file
@@ -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 <n> 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.
|
||||
@@ -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/<lang>/` — 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 <n> "$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.
|
||||
|
||||
Reference in New Issue
Block a user