Files
Sylpheed/docs/port/RUNNING.md
MechaCat02 147767c49d
Some checks failed
CI / Native — linux (pull_request) Failing after 1h21m4s
CI / WASM — Web (pull_request) Successful in 29m43s
CI / Formatting (pull_request) Successful in 24s
docs(port): the two steps a host needs that the container hides
Both were found by following RUNNING.md on this host from a clean checkout, and
both fail in a way that blames the wrong thing.

`godot --path port` without a prior `--import` produces THIRTY parse errors
naming half the project -- ScreenView, MenuAudio, Gamepad, MenuFlow, ExportTree
all "not declared", each cascading into "cannot infer the type of ..." -- and
then hangs. That reads like a port that does not compile. It is one gitignored
file: `port/.godot/global_script_class_cache.cfg`, which Godot writes at import
time and which `class_name` resolution needs. After `--headless --path port
--import`: zero errors, `main_menu` loads 16 elements and settles at t=56.

The exporter shells out to ffmpeg and ffprobe for the movies AND the audio, and
`sylph-ci:local` has neither -- so the documented `cargo run` fails minutes in,
after the screens are already written and it looks like it is working. The image
to use is `sylph-ci:ffmpeg`.

Neither bites in the agent container: its image carries an imported tree and its
own ffmpeg. That is exactly why neither was ever written down.

Also recorded: `--headless` CANNOT `--capture` (the dummy renderer never emits
`frame_post_draw`; the port detects this and says so), and `xvfb-run`, which its
message suggests, is not installed on every host -- it is not on this one.

Measured 2026-09-26 on fabi-Hyrican-PC: 31 s build, 4 min 11 s export, 149 MB,
`sylpheed-export check` validates all 16 screens against sylpheed.screen/3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-26 17:56:52 +02:00

9.2 KiB

Running the port

P5's gate is "a human clicks through it", and until now there was no page telling a human how. The commands existed — in boot.gd's header comment and scattered through a twelve-thousand-line DECISIONS.md. A capability that lives only in the record is, to the person who needs it, absent.

Everything below has been run. Where a number is quoted it was measured in this container, and where the container distorts it that is said rather than left for the reader to discover.

0. Import the project once, on a checkout that has never run it

godot --headless --path port --import

🔴 Skip this and the port looks broken rather than unimported. Godot resolves class_name through port/.godot/global_script_class_cache.cfg, which is gitignored and is written at import time. Without it every class fails — ScreenView, MenuAudio, Gamepad, MenuFlow, ExportTree — and each failure cascades into a dozen more:

SCRIPT ERROR: Parse Error: Could not find type "ScreenView" in the current scope.
SCRIPT ERROR: Parse Error: Cannot infer the type of "_pad" variable …
ERROR: Failed to load script "res://scripts/boot.gd" with error "Parse error".

Thirty-odd parse errors naming half the project reads like a port that does not compile. It is one missing cache, and the import takes seconds. Measured on a fresh checkout 2026-09-26: before, 30 errors and a hang; after, zero.

The container does not hit this because its image carries an imported tree, which is exactly why the step was never written down.

⚠️ --headless cannot capture. Godot's dummy renderer never emits frame_post_draw, so --capture= under --headless would hang; the port detects it and says so. A capture needs a display — xvfb-run -a godot …, and note that xvfb-run is not installed on every host.

1. Build the asset tree

The Godot project reads export/, never the disc.

cargo run --release -p sylpheed-export -- export --disc /disc --out export

Roughly four minutes, most of it transcoding two movies.

🔴 The exporter needs ffmpeg and ffprobe on PATH — it shells out to them for both the movies and the audio. On a host, that means running it through the CI container built WITH them:

SYLPH_CI_IMAGE=sylph-ci:ffmpeg docker/ci/run \
  cargo run --release -p sylpheed-export -- export --disc /disc --out export

sylph-ci:local has neither, and the failure arrives several minutes in, after the screens have already been written. Measured 2026-09-26 on fabi-Hyrican-PC: 31 s to build, 4 min 11 s end to end, 149 MB of export/, and sylpheed-export check then validates all 16 screens. It rewrites export/ wholesale — never hand-edit anything in there; hand-written decisions live in authored/ beside it, and survive a re-export.

2. The P5 walk, from a cold start

godot --path port -- --boot --play

This is the one a human should judge. It boots the way the game does — two splashes, the ADV intro, the title — hands over to the menu on Ⓐ, and then stays live and waits for input.

you press what should happen
Ⓐ on the title the main menu opens on NEW GAME
⬆ / ⬇ one item, wrapping at both ends
⬅ / ➡ nothing — measured, and implemented as an explicit no-op
Ⓐ on EXTRAS the EXTRAS submenu, opening on MISSION SELECT
Ⓑ in EXTRAS back to the main menu, on the item you left
Ⓑ on the main menu back to the title
Ⓐ on the title again the menu, still on the item you left

That last row is the one worth checking deliberately: the main menu remembers its cursor, and every submenu resets to its own opening item. Both are measured, and they disagree on purpose.

⏱ The intro is ~157 s. To skip straight to the menu:

godot --path port -- --menu=main_menu

and to drive it unattended:

godot --path port -- --menu=main_menu --script=down,down,down,down,accept,cancel

🔴 This example used to say down,down,accept,cancel, and it walked the wrong path. Two ⬇ from the opening item lands on TUTORIAL, whose destination this export does not carry — so the example exercised a not-carried message and returned, never opening a submenu at all. EXTRAS is the fifth item, so it takes four. The one submenu P5's gate rests on was the one the runbook's own command did not reach. Verified 2026-08-31 by running both.

🔴 --script without --play or --menu refuses and says so. It used to parse, be stored, and do nothing.

3. What is knowingly missing — not bugs

Four of the five main-menu destinations are measured but not in this export: they live in other archives (GP_SAVE_LOAD, GP_OPTIONS, …). Pressing Ⓐ on them prints what it would have opened and why it cannot:

(LOAD GAME) opens a screen this export does not carry:
   The save-slot list is GP_SAVE_LOAD, not in this export. Destination MEASURED.

EXTRAS is the only Ⓐ-into-a-submenu this milestone can walk, which is why the P5 gate rests on it.

NEW GAME is a deliberate gap of a different kind: the real chain is NEW GAME → DIFFICULTY → SELECT DATA → the S00A movie, and the port jumps to the movie, printing the two screens it skipped. That is a gap, stated out loud; nobody should read the port's behaviour there as the game's.

4. What this container distorts

  • No GPU. 720p Theora decodes +6.7 % … +6.9 % slower than real time here (5 runs, both movies, on a quiet box). The boot's printed seconds carry that deficit. It is a property of the machine, not of the port.
  • No sound card. Godot falls back to a dummy driver, so you will hear nothing. The audio is present and measurable — docs/port/AUDIO-VERIFICATION.md answers every audio question without a device, and tools/port/verify-menu-audio asserts it — but "I heard it" is not available in here.
  • A leaked-object warning at exit is engine-side, not the port's. Measured: releasing every reference the port owns moves the count from 8 to 8.

5. Modding

data/mods/ shadows export/ by path. Each override is announced as it is read, and at the end of a run any file that can never apply is listed:

mod:   sprites/title/main_menu/ptbase.png  <-  data/mods/...
mods:  1 file(s) in data/mods can shadow NOTHING -- no such path in the export:
  inert: sprites/title/TYPO_menu/pteff05.png

A file whose path exists in the export but was simply not read this run is not listed. See docs/port/MODDING.md for the five rules the asset tree keeps.

6. Where the work is, and what P5's gate is waiting on

P5's gate is the only one that needs a person, and it is not waiting on code.

Everything above runs from auto/port-p6-audio.

🔴 This section used to quote counts — "256 commits ahead, 58 files" — and they were stale the moment they were committed, because committing them incremented the count. By the time anyone read it, it said 256 and the answer was 258. A number written into a document meant to inform a decision decays with every commit either agent makes, and the Decoder hit the same thing in their own merge-state page one message after recording the class.

So what follows are the invariants, which do not move, and the commands to re-derive anything that does.

invariant check passes when
main is an ancestor of this branch — a fast-forward, nothing to resolve git merge-base --is-ancestor origin/main HEAD; echo $? prints 0. ⚠️ The command itself prints nothing on success — without the echo a reader cannot tell success from failure
main is an ancestor of the Decoder's branch too git merge-base --is-ancestor origin/main origin/auto/build-ordinal-audit; echo $? prints 0, same caveat
the two change sets touch zero files in common comm -12 <(git diff --name-only origin/main...HEAD | sort) <(git diff --name-only origin/main...origin/auto/build-ordinal-audit | sort) | wc -l prints 0
merging both produces no conflicts git merge-tree --write-tree HEAD origin/auto/build-ordinal-audit | wc -l prints 1 — one line is the tree id; conflicts would follow it. Read-only: this merges nothing

Last run here: 0, 0, 0, 1. A command published without a pass condition is half a check — the reader gets a number and no way to know whether it is the right one — so each row states what the right one is.

📌 So the sentence is not "N commits behind", which sounds like something to schedule. It is: two fast-forwards over disjoint file sets, mergeable in either order with zero conflicts. Counts if you want them: git rev-list --count origin/main..HEAD.

What a person is actually being asked to do

  1. godot --path port -- --boot --play, then walk §2's table.
  2. Say whether it behaves as described. Not whether it matches the game — that comparison is the oracle's job and is already asserted by tools/port/check-all.
  3. If it does, P5's gate is met and nothing else is blocking P6, which asserts its own audio and has no human step.

⚠️ You will hear nothing (§4), and the intro takes ~157 s. --menu=main_menu skips straight to the part being judged.