Sets a new mission: boot the real disc through developer splash -> intro video -> title -> main menu -> submenus in Godot 4, interactively, with no gameplay, no 3D and no emulator. docs/port/MISSION.md defines it -- eight gated milestones, each finished by an ARTIFACT rather than by compiling, plus the Ready Room as an explicitly gated stretch goal with a one-iteration probe that decides go/no-go. GP_READY_ROOM is 1106 entries with 6 recoverable names and is ISL-scripted, so it is either a week or a quarter, and the agent must not start it on its own authority. Architecture, per the user's decision: the Godot project is INDEPENDENT of the Rust viewer and never reads a disc format. An offline Rust exporter converts the disc into open formats; Godot reads only those. No GDExtension, no Rust in the Godot project, and sylpheed-viewer is off limits -- it stays the human's verification tool with its static-data rule intact. docs/port/FORMAT.md specifies the open format, versioned, because modding is the port's second goal and that makes the layout a deliverable rather than a temp directory: JSON over XML (Godot parses JSON natively; its XMLParser is SAX), names never hashes, provenance in every generated file, and unknowns listed rather than guessed. The discipline the whole thing rests on is the derived/authored split. `export/` is regenerated wholesale and never hand-edited; `authored/` is hand-written and survives a re-export. Three things this milestone needs are NOT on the disc in any decoded form -- which button does what, paint order, and menu sound cues -- so they live in `authored/` with a stated `why`. Deleting an authored entry because the exporter can now emit it IS the measure of progress. `export/` is gitignored: it is generated from the user's own disc and this stays a clean-room repo. Container: adds a pinned Godot 4 (windowed under Xvfb for screenshots, plus a headless wrapper). ffmpeg already carries libtheora, which is the video target -- Godot 4 plays only Ogg Theora natively and the disc's ADV.wmv is WMV3/WMA Pro. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
206 lines
7.1 KiB
Markdown
206 lines
7.1 KiB
Markdown
# 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.
|