scaffold the Godot port, as its own repo with its own agent
The port is deliberately separate from the reverse-engineering project: its own
repository, its own clone, its own container. Two writers in one working tree
means files change under whoever is mid-edit and a `git add -A` by one sweeps up
the other's work -- which happened today in the Reborn tree, so this is set up
not to repeat it.
The wall: Godot never reads a disc format. An offline Rust exporter converts the
user's disc into JSON + PNG + Ogg, and the Godot project reads only that. No
GDExtension, no Rust in port/. Beyond the practical reason -- Godot cannot read
IPFB, RATC, T8aD, XMA or WMV -- there is the design one: modding is a goal, and
if the runtime reads the original formats then modding means reverse
engineering, whereas if it reads JSON it means opening a file.
The decoders come from sylpheed-formats PINNED BY REVISION (8b6dbcf), not
vendored and not reimplemented. `sylpheed_formats::media` in particular already
owns every case where one playable thing is not one archive entry: entries that
span segment files, banks with several sub-waves, and the cutscene voices, which
are one continuous XMA stream chunked into VOICE_*.slb entries whose boundaries
do NOT match the cues. That last one is the easiest thing in this project to get
subtly wrong, so the mission says outright not to re-derive it.
docs/MISSION.md is the objective (P0-P7, each gated by an artifact rather than
by compiling). docs/BLOCKED.md lists what cannot proceed until the RE agent
answers Q1-Q10, and says plainly that none of it may be guessed -- this agent
has no emulator and no oracle, so a value it invents is indistinguishable from a
decoded one a month later.
The container is deliberately small: 3 cpus / 4 GB against the RE container's
6 / 7, and an image with no C++ toolchain, no Vulkan stack and no emulator. Two
full-size containers do not fit on this box beside a desktop.
Its launcher sets the git identity through GIT_AUTHOR_*/GIT_COMMITTER_* rather
than writing [user] into .git/config -- the config route captures every commit
made in that tree, including a human's, which is how six of today's commits
ended up attributed to the RE agent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
23
docs/BLOCKED.md
Normal file
23
docs/BLOCKED.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Waiting on the RE agent
|
||||
|
||||
What this port cannot do until an answer lands in
|
||||
[`/reborn/docs/port/HANDOFF.md`](https://git.mc02.dev/fabi/Syplheed-Reborn).
|
||||
Recorded so it is not re-discovered every iteration.
|
||||
|
||||
| Milestone | Needs | HANDOFF question |
|
||||
|---|---|---|
|
||||
| P2 keyframe animation | the unit of a keyframe time, and the ramp shape | Q1 |
|
||||
| P3 splash → title | which build is which screen state | Q2 |
|
||||
| P1/P3 correct layering | paint order for these six screens | Q3 |
|
||||
| P5 button actions | which button opens which GamePart | Q4 |
|
||||
| P5 navigation | initial focus, wrap-around, what B does | Q5 |
|
||||
| P3 sequencing | the boot order and what drives it | Q6 |
|
||||
| P3 transitions | what happens visually between screens, and its timing | Q7 |
|
||||
| P6 audio | which BGM per screen; which cue on move/confirm/back | Q8 |
|
||||
| P4/P7 video | which movie is the boot intro vs the new-game intro | Q9 |
|
||||
| P6 looping | whether a music bank's sub-waves are intro+loop or variations | Q10 |
|
||||
|
||||
**None of these may be guessed.** A value invented here is indistinguishable from
|
||||
a decoded one a month from now. Where a milestone can proceed with a placeholder,
|
||||
put the placeholder in `authored/` with a `why` naming the question it is standing
|
||||
in for, so it is deleted rather than forgotten when the answer arrives.
|
||||
139
docs/FORMAT.md
Normal file
139
docs/FORMAT.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# The open export format — v1
|
||||
|
||||
The format the disc is converted *into*, and the one the Godot project and any
|
||||
modding tool read. **This is a starting point, and it is yours to revise** — but
|
||||
it is versioned, so a change is a deliberate act with a version bump, not a
|
||||
silent edit.
|
||||
|
||||
Design rules, in priority order:
|
||||
|
||||
1. **A human can read and edit it.** Modding is a goal of this port, which makes
|
||||
the layout part of the product rather than a temp directory.
|
||||
2. **Names, never hashes.** Where the disc's own name was never recovered — the
|
||||
six `*2D` archives and `GP_READY_ROOM` — emit a stable synthetic id **and say
|
||||
in the file that the real name is unknown**. A modder must be able to tell a
|
||||
recovered name from an invented one.
|
||||
3. **Provenance travels with the data.** Source archive, entry index, exporter
|
||||
version. This is what keeps the export auditable against the disc 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, never silently defaulted.
|
||||
|
||||
**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 per schema.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
export/ # DERIVED. Regenerable. Gitignored. Never hand-edited.
|
||||
manifest.json
|
||||
screens/title/*.json
|
||||
sprites/*.png
|
||||
audio/music/*.ogg audio/sfx/*.ogg audio/cues.json
|
||||
video/*.ogv
|
||||
authored/ # AUTHORED. Hand-written. Committed. Survives re-export.
|
||||
flow.json # boot sequence + what each button does
|
||||
paint_order.json # per-screen z-order
|
||||
cue_bindings.json # which cue fires on move / confirm / back
|
||||
```
|
||||
|
||||
Godot loads `export/` first, then applies `authored/` over it.
|
||||
|
||||
## Common header
|
||||
|
||||
```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** — 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": "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] },
|
||||
{ "t": 34, "pos": [542, 157] },
|
||||
{ "t": 64, "pos": [542, 162] }
|
||||
]
|
||||
}
|
||||
],
|
||||
"buttons": ["ptbtn01", "ptbtn02", "ptbtn03", "ptbtn04", "ptbtn05"],
|
||||
"unresolved": ["paint_order", "keyframe_time_unit"]
|
||||
}
|
||||
```
|
||||
|
||||
**`role`** comes from the decoded element kind: `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 navigation order: `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 (measured and refuted). It
|
||||
is right for a vertical menu and should not be trusted for anything else.
|
||||
|
||||
**`keyframes`** carry the on-disc time verbatim in `t`. A keyframe is the **start
|
||||
of a ramp toward the next**, not a pose that is held. The unit of `t` is HANDOFF
|
||||
Q1 and is unanswered — keep `t` raw so the conversion lives in exactly one place.
|
||||
|
||||
**`rest`** is the resting pose: the longest run of consecutive keyframes with an
|
||||
unchanged value, falling back to longest-dwell. Neither the first nor the last.
|
||||
|
||||
**`unresolved`** lists what this file does not answer; a consumer needing one of
|
||||
those must get it from `authored/`.
|
||||
|
||||
## `authored/flow.json`
|
||||
|
||||
```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 placeholder for HANDOFF Q4" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`goto` may name an 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 Q4 and is not).
|
||||
|
||||
## `export/manifest.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"format": "sylpheed.manifest/1",
|
||||
"exporter": "sylpheed-export 0.1.0",
|
||||
"formats_rev": "8b6dbcf",
|
||||
"video_transcode": "ffmpeg -i ADV.wmv -c:v libtheora -q:v 8 -c:a libvorbis -q:a 5 ADV.ogv",
|
||||
"warnings": ["GP_READY_ROOM not exported -- out of scope"]
|
||||
}
|
||||
```
|
||||
|
||||
`formats_rev` pins which decoders produced this export, and `video_transcode`
|
||||
records the exact command so a modder can re-run it rather than reverse-engineer
|
||||
what was done.
|
||||
117
docs/MISSION.md
Normal file
117
docs/MISSION.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Primary objective — the menu shell, running in Godot
|
||||
|
||||
**Status:** active, set 2026-08-28.
|
||||
|
||||
Build a Godot 4 project that boots the player's own disc through the sequence the
|
||||
real game uses, and let a person move through it:
|
||||
|
||||
```
|
||||
developer logo splash → intro video → title / PRESS Ⓐ → main menu → submenus
|
||||
```
|
||||
|
||||
No gameplay. No 3D. No HUD. No emulator. Done means a human presses a d-pad and
|
||||
Ⓐ and moves through those screens with the right art, animation, music and
|
||||
transitions.
|
||||
|
||||
## 1. You are one of two agents
|
||||
|
||||
A **container agent** does the reverse engineering, in the
|
||||
[Syplheed-Reborn][reborn] repository. It runs the emulator; you do not. You build
|
||||
the port from what it publishes.
|
||||
|
||||
The contract is `docs/port/HANDOFF.md` in that repository. **Read it before
|
||||
assuming any value is on the disc.** Every answer there is one of three things,
|
||||
and the distinction decides what you do:
|
||||
|
||||
| | meaning | what you do |
|
||||
|---|---|---|
|
||||
| **decoded** | a field on the disc, with a disc-wide check | read it in the exporter |
|
||||
| **measured** | not on the disc, but the running game does *this* | put it in `authored/`, cite the finding |
|
||||
| **undecodable** | looked for, provably not there | put it in `authored/`, say it is a decision |
|
||||
|
||||
If HANDOFF.md does not answer something you need, **say so and move to another
|
||||
milestone**. Do not guess and do not reverse engineer it yourself — you have no
|
||||
emulator and no oracle, so a guess here is indistinguishable from a fact and will
|
||||
be believed later.
|
||||
|
||||
[reborn]: https://git.mc02.dev/fabi/Syplheed-Reborn
|
||||
|
||||
## 2. The wall
|
||||
|
||||
The Godot project **never reads a disc format**. No IPFB, no RATC, no T8aD, no
|
||||
XMA, no WMV. If Godot cannot read something, the exporter's job is to emit it
|
||||
differently — not to bridge the gap at runtime.
|
||||
|
||||
* **No GDExtension. No Rust in `port/`.**
|
||||
* The decoders come from `sylpheed-formats`, **pinned by revision**. Do not vendor
|
||||
them, do not reimplement them, and do not float the pin — a decoder change
|
||||
landing mid-milestone is exactly the confusion this pin prevents.
|
||||
* Bump the pin deliberately, as its own commit, saying what you wanted from it.
|
||||
|
||||
**In particular, do not reimplement media assembly.** `sylpheed_formats::media`
|
||||
already handles the cases where one playable thing is not one archive entry: a
|
||||
`.pak` entry that spans segment files, a bank with several sub-waves, and the
|
||||
cutscene voices — which are one continuous XMA stream chunked into `VOICE_*.slb`
|
||||
entries whose boundaries do **not** match the cues, so *a `.slb` need not hold
|
||||
the track its name claims*. That last one is the single easiest thing in this
|
||||
project to get subtly wrong. Use `resolve_movie_voice_region`.
|
||||
|
||||
## 3. Derived vs authored
|
||||
|
||||
| | `export/` | `authored/` |
|
||||
|---|---|---|
|
||||
| produced by | the exporter | you, by hand |
|
||||
| contains | what the disc says | what we decided |
|
||||
| hand-edited | **never** | always |
|
||||
| in git | **no** — gitignored | yes |
|
||||
| on re-export | overwritten wholesale | untouched |
|
||||
|
||||
Tempted to hand-fix a file under `export/`? The fix belongs in the exporter or in
|
||||
`authored/`. Every `authored/` entry carries a `why`.
|
||||
|
||||
When the RE agent later decodes something you had authored, **delete the authored
|
||||
entry** and let the exporter emit it. That deletion is the measure of progress.
|
||||
|
||||
## 4. Never commit game assets
|
||||
|
||||
`export/` is generated from the user's own disc and is gitignored. Code, schemas,
|
||||
`authored/` mappings and docs only. If you are about to commit a sprite PNG or a
|
||||
transcoded video, stop.
|
||||
|
||||
## 5. Milestones
|
||||
|
||||
A milestone is done when its **artifact** exists, not when the code compiles.
|
||||
|
||||
| | Milestone | Gate |
|
||||
|---|---|---|
|
||||
| **P0** | Exporter skeleton; one screen and its sprites to `export/` | `export/screens/title/main_menu.json` validates against FORMAT.md and the PNGs open |
|
||||
| **P1** | Godot renders that screen statically at 1280×720 | A Godot screenshot beside `sylpheed-cli screen render` of the same build — they should agree, and where they do not, say which is wrong |
|
||||
| **P2** | Keyframe animation | Buttons slide in. **Blocked on HANDOFF Q1** (the time unit). Do not invent it |
|
||||
| **P3** | Splash → title, with the transition | Both screens back to back, unattended |
|
||||
| **P4** | Intro video | `ADV.wmv` plays with audio (§6) |
|
||||
| **P5** | Main menu: navigation, focus states, Ⓐ into a submenu, B back | A human clicks through it |
|
||||
| **P6** | Audio — menu BGM and move/confirm SFX | Sound on the P5 gate. **Looping is blocked on HANDOFF Q10** |
|
||||
| **P7** | New-game intro video after NEW GAME | Plays, then returns to a defined state |
|
||||
|
||||
Work the lowest unfinished milestone. When one is blocked on an RE answer, say so
|
||||
in `docs/BLOCKED.md`, and take the next milestone that is not.
|
||||
|
||||
## 6. The video problem
|
||||
|
||||
`ADV.wmv` is **WMV3 video with WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4
|
||||
plays only **Ogg Theora** natively.
|
||||
|
||||
Transcode with ffmpeg, and **record the exact command in the export manifest** so
|
||||
a modder who dislikes the quality can re-run it rather than reverse-engineer what
|
||||
you did. Theora at 720p is not great; if the result is visibly poor, **say so and
|
||||
propose** the FFmpeg-GDExtension fallback — do not adopt a runtime dependency on
|
||||
your own authority.
|
||||
|
||||
Only the boot intro and the one new-game intro are in scope. The disc holds
|
||||
3.3 GB of video; transcoding all of it is not this milestone.
|
||||
|
||||
## 7. Out of scope
|
||||
|
||||
3D, gameplay, HUD, missions, save/load, localisation beyond English, the Ready
|
||||
Room, and any reverse engineering. If you want an answer the disc has not given
|
||||
you, that is a request to the container agent, not a task for you.
|
||||
85
docs/loop-task.md
Normal file
85
docs/loop-task.md
Normal file
@@ -0,0 +1,85 @@
|
||||
Build the Godot menu port, one milestone at a time.
|
||||
|
||||
## Your objective
|
||||
|
||||
`docs/MISSION.md` — read it every iteration. It defines the milestones P0…P7 and
|
||||
the gate each must pass, the wall between the exporter and Godot, and the
|
||||
derived/authored split.
|
||||
|
||||
**You do not reverse engineer.** A separate container agent does that, in the
|
||||
Syplheed-Reborn repository, mounted read-only at `/reborn`. You have no emulator
|
||||
and no oracle, so a guess of yours is indistinguishable from a fact and will be
|
||||
believed later. If you need an answer the disc has not given you, write it in
|
||||
`docs/BLOCKED.md` and move to another milestone.
|
||||
|
||||
## Read these first, every iteration
|
||||
|
||||
1. `docs/MISSION.md` — milestones, gates, scope.
|
||||
2. `/reborn/docs/port/HANDOFF.md` — **the contract.** What is decoded, what was
|
||||
measured off the running game, and what is known undecodable. `git -C /reborn
|
||||
pull` first; the RE agent publishes continuously.
|
||||
3. `docs/FORMAT.md` — the open format. It is versioned and it is yours to
|
||||
revise, but a change is a deliberate act with a version bump.
|
||||
4. `docs/BLOCKED.md` — what you are waiting on, so you do not re-discover it.
|
||||
|
||||
`/reborn/docs/re/disc-atlas.html` maps how the assets reference each other.
|
||||
|
||||
## Each iteration
|
||||
|
||||
1. **Pick the lowest unfinished milestone.** If it is blocked on an RE answer,
|
||||
record that in `docs/BLOCKED.md` and take the next one that is not.
|
||||
2. **Build the smallest thing that reaches its gate.** The gate is an artifact —
|
||||
a validating JSON file, a screenshot, a clickable build — never "it compiles".
|
||||
3. **Keep derived and authored apart.** `export/` is regenerated wholesale and
|
||||
never hand-edited. A fix you are tempted to make there belongs in the exporter
|
||||
or in `authored/`, and every `authored/` entry carries a `why`.
|
||||
4. **Write down what you decided**, in `docs/`. A decision that lives only in
|
||||
your context is lost when the container dies.
|
||||
5. **Commit** to `auto/<topic>`, 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 gitignored and generated from the
|
||||
user's own disc. Code, schemas, `authored/` mappings and docs only.
|
||||
* **No Rust in `port/`, no GDExtension.** If Godot cannot read something, the
|
||||
exporter emits it differently.
|
||||
* **Do not vendor or reimplement `sylpheed-formats`** — it is pinned by revision.
|
||||
In particular do not reimplement media assembly: `sylpheed_formats::media`
|
||||
already handles segment-spanning entries, multi-sub-wave banks and the
|
||||
continuous cutscene-voice stream, and that last one is the easiest thing here
|
||||
to get subtly wrong.
|
||||
* **`/reborn` is READ-ONLY.** Never commit there, never edit it. It belongs to
|
||||
the other agent and you share no working tree with it.
|
||||
* **Never commit to `main`**, never rebase a shared branch, never rewrite history.
|
||||
* **Do not adopt a runtime dependency on your own authority.** Propose it.
|
||||
|
||||
## Verifying
|
||||
|
||||
* `sylpheed-cli screen render` (built from `/reborn`) is the reference renderer.
|
||||
When Godot draws a screen, diff against the CLI's composite of the same build.
|
||||
Where they disagree, one of them is wrong — say which, and why, rather than
|
||||
tuning until they match.
|
||||
* Godot runs headless (`godot-headless`), and windowed under Xvfb with
|
||||
`screenshot` for a capture.
|
||||
* A regenerated `export/` that comes out byte-identical is strong evidence a
|
||||
change was additive. When it does change, check that every diff line pairs.
|
||||
|
||||
## Publishing
|
||||
|
||||
`push-work` pushes the current branch to origin. It refuses anything that is not
|
||||
`auto/*` and never force-pushes, so the consolidated line stays a human's
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
The loop runs on a fixed interval set by the harness, so you do **not** need to
|
||||
arm the next wakeup yourself. Spend that attention on the write-up instead.
|
||||
Reference in New Issue
Block a user