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>
140 lines
4.9 KiB
Markdown
140 lines
4.9 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. **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.
|