Files
Sylpheed/docs/FORMAT.md
MechaCat02 9d96be609e 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>
2026-08-28 17:56:44 +02:00

4.9 KiB

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

{
  "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

{
  "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: 0x3002button, 0x10primitive, 0x0decoration. 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

{
  "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

{
  "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.