Merges the Godot port into the reverse-engineering repository, preserving both
histories -- 1019 commits of corpus plus the port's 31, brought in by subtree
merge and then moved into place so git can follow each file across the rename.
The reason is not tidiness. The two-repo split forced the exporter to depend on
the decoders by pinned revision, and that created a whole class of failure that
now disappears: a sha reachable only from a topic branch, orphaned by a
squash-merge, breaking a fresh checkout silently at build time. It also forced a
live read-only mount of one agent's working tree into another's container, which
is why a contract file could move mid-iteration. With a path dependency, a
decoder change and the exporter change it requires land in the same commit or
not at all.
Canary stays separate: it is a fork tracking upstream.
New structure for the long term:
docs/game/ how the game is NAVIGATED -- menus, modals, prompts, alerts,
and in-game flight. Written so nobody rediscovers it. Mostly
open questions on purpose; the in-game tutorials are the
resource for the flight half.
docs/port/MODDING.md
modding as a constraint on the exporter TODAY, not a later
feature: one logical asset in one file (the disc splits nearly
everything, and resolving that is the exporter's job), names a
person recognises, PNG/OGG/OGV/JSON only, base-and-overrides so
re-exporting is always safe, provenance in every file.
data/base + data/mods
generated tree and drop-in overrides, both gitignored
exchange/ transient inter-agent files, deliberately outside history
docs/agents/ the team protocol
Both the README and the navigation doc lead with the correction that cost the
most: the oracle is the real game under Xenia Canary. Reborn's renderer is a
hypothesis under test, it has been wrong, and treating it as ground truth
propagated into three documents and both agents before a human caught it.
Scripted modding stays possible without being built: no screen name is hardcoded
in GDScript and there is no native code in port/, which is what Godot Mod Loader
needs to be able to substitute behaviour later.
14 KiB
Waiting on the RE agent
What this port cannot do until an answer lands in
/reborn/docs/port/HANDOFF.md.
Recorded so it is not re-discovered every iteration.
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,
the placeholder goes in authored/ with a why naming the question it stands in
for, so it is deleted rather than forgotten when the answer arrives.
Last reconciled against HANDOFF.md on 2026-08-29, at /reborn HEAD 9a0ca0d.
(/reborn is mounted read-only, so git -C /reborn pull fails by design; the
mount is refreshed outside this container and HEAD is read, not fetched.)
Still open — these block work
| Milestone | Needs | HANDOFF | State |
|---|---|---|---|
| Q8 | ✅ answered 2026-08-28 — the RE agent retracted "cannot be extracted". The waves are located in Static.slb by playing them: move 0x1ec0 (8 192 B, 0.533 s), confirm 0x5d6c0 (12 288 B, 1.016 s), back 0x0ec0 (4 096 B, 0.344 s), and ⬅➡ play nothing. Move and back reproduce across two boots. 🟡 that the cursor's wave is the cue named SE_UI_CURSOR is still a name match, and Ⓐ's wave is not separated between SE_UI_DECIDE and SE_UI_SUB_WIN_OPN. P6 can now export real audio; the exporter has to grow an SE path. |
||
| P6 audio | which BGM the menu plays | Q10 | ❔ not on the disc. All 32 banks are named BGM_001…BGM_109 with no semantic name anywhere. The port is choosing a track, and that choice is authored. |
| P6 looping | where a menu loop restarts | Q10 | ❔ BGM_001 fades out at 167.663 s into 6.15 s of silence, and no loop-point field has been identified. A menu loop is authored. |
| P4/P7 video | whether Ⓐ skips a movie | Q9 | 🟡 unsettled — the corpus says Ⓐ skips every time, the boot harness never taps during a movie because it breaks the title. P4 can play the movie; it cannot yet say what a button press does during one. |
P5 NEW GAME |
what Ⓐ on NEW GAME opens |
Q4 | ❔ untested: Ⓐ on it hangs the emulator. The other four destinations are measured. |
| P3 sequencing | what code decides to advance the boot sequence | Q6 | 🟡 the order is observed and the attract cycle timed (~8–10 s idle → fade → ADV.wmv in full → title). The driver is not decoded. P3 can reproduce the observed behaviour and must say it is reproducing an observation. |
Answered since this file was last written — no longer blocking
Q1 (keyframe time unit — linear ramp, 2 units per rendered frame, 1 unit = 1/60 s
measured), Q2 (which build is which screen), Q3 (paint order — a u16 layer key
at +0x0A, decoded), Q5 (navigation: ⬆⬇ wrap, ⬅➡ nothing, Ⓑ up with focus
restored), Q7 (transitions: a fade through black, fade-in decoded, ~0.4 s fade-out
measured), Q9 (ADVERTISE_MOVIE → ADV.wmv is boot intro and attract; MS00A →
S00A.wmv is the new-game intro), Q10 (a bank is two stems played together —
do not concatenate), S1 (Ready Room: no-go).
Also newly available, and useful to P3/P5 when they author the flow: the title
part's transitions are a lookup by name, and the game's own screen
vocabulary includes TITLE_SCREEN, TITLE_MENU, LOADING, DIFFICULTY,
EXTRA_MENU, TUTORIAL_MENU. Three of those are corroborated by measurements
taken before the function was opened (DIFFICULTY is what NEW GAME opens,
EXTRA_MENU is EXTRAS, TUTORIAL_MENU the lesson list). 🟡 Candidate, not
decoded — the RE agent is explicit that the strings are what the call sites
reference, not proven arguments, and the same list mixes in TEXT_FONT and
GAMMA_RGB. So authored/flow.json may use these as goto names — which is
better than inventing names — but must mark them as a name match, not a
measurement.
Three of those are measured, not decoded, and so are authored here rather than exported:
| Authored because it is not on the disc | HANDOFF | Where it lives |
|---|---|---|
1 keyframe unit = 1/60 s |
Q1 | not yet written — P2 |
| initial menu focus (not stable across boots; pick one and say so) | Q5 | not yet written — P5 |
| the ~0.4 s fade-out and the 0.17–0.23 s black hold | Q7 | not yet written — P3 |
What the port needs next — sent to the RE agent 2026-08-29
Ordered by what it costs the port, not by what it costs to answer.
1. How should the exporter recognise the developer-logo splash? (P3, blocking)
The splash is the first thing P3 draws and it is not in export/. It
declares its sprites directly and has no .rat layout child, so is_build
rejects it; sylpheed-cli reaches it only via --all, which the CLI's own help
says renumbers --build. So the port cannot address it by build index
without the index meaning something different from everywhere else in this
format.
What I need is a predicate, not an index: something the exporter can apply to
say "this bundle is a composable screen" that admits the splash and does not
admit the 1 894 two-element fragments --all also lets in. If the honest answer
is "there is no such rule, take GP_TITLE entries 11/14", that is a usable
answer — I will export it under a synthetic name with name_source saying it was
located by entry index and not by a rule.
2. Is the ~0.4 s fade-out the whole ramp, or a segment of it? (P3, blocking)
Q7 measures the screen fade-out at ~0.4 s and the black hold at 0.17–0.23 s.
The port needs to know which quantity that 0.4 s is, because the last
keyframe of a group carries no t and the port refuses to invent one:
- the ramp from the hold to the exit pose — i.e. the missing duration of that final untimed keyframe; or
- hold → exit → fully black, the 0.4 s covering several keyframes; or
- something the game does independently of the group.
Under the first reading the port writes one authored constant and plays the group to its end. Under the third it must not.
3. Focus: drawn OVER the base element, or INSTEAD of it? (P5, cheap, avoid rework)
sylpheed-cli --focus is documented as drawing the focused record over its
base. The port replaces the sprite. Those are different operations and the
port picked its one without evidence.
Evidence that the port is wrong: rendering main_menu with ptbtn01 focused —
which is how main-menu-oracle.png was taken — makes the RMSE against that
capture worse, 5.92 % → 7.00 %. The capture also shows a ring marker
beside NEW GAME that the port draws nowhere. Cheap to answer from a capture
that already exists, and it decides how P5 is built.
4. Rotation — should the port draw it, and about what? (P2/P3, needs a joint decision)
67fa1a1 decodes rotation_deg at keyframe +12 and explicitly does not
render it: ui_layout::blit is axis-aligned. ptloop01/ptloop02 on the title
declare +30° and −45°, and the framebuffer submits them at +30.26 and −45.28.
A canvas rotation is a few lines in Godot, so the port can draw these. But
then the port is deliberately more correct than the reference renderer, and
verify-screen — the port's whole verification method — starts reporting a large
diff on the title that means "the port is right". That is a bad state to be in
silently, so I would rather agree it than do it.
Two sub-questions: is the rotation about the declared pivot or about the
element's centre or corner? And would you rather blit grow a rotating path so
the diff stays meaningful? The format would go to v3 to carry
rotation_deg; that is my side and I will do it either way, since carrying a
decoded field the renderer ignores is better than dropping it.
5. Is main-menu-oracle.png gamma-correct? (not blocking, but it calibrates everything)
With the background in, the port sits at 5.92 % RMSE against that capture and is visibly darker and less saturated than it across the whole frame. If the capture path applies a gamma or a colour transform the game does not, then RMSE against captures has a floor and the port should stop chasing it. If it does not, something is still missing. The port cannot tell these apart from inside.
Questions this port has raised
Does a keyframe group loop, or hold its last pose? — answered
Answered 2026-08-28 by the RE agent: groups hold. ptloop01/ptloop02 park
their sprites at x=1521 and x=−839, both off a 1280-wide design, and 18 s of
settled title sits at sd ≤ 0.01. loop*.rat is a misleading name — these
animate once during build-in and then rest off-screen.
The port's own error here was different and is fixed: it settled at the last
timed keyframe rather than at the hold. See docs/DECISIONS.md.
Kept for the record:
Raised at P2 and unsettled. The port holds the last timed keyframe, which is
right for an entry animation (the main menu settles at t=80, 1.33 s) and is
proven on the screen P2 gates. The title runs to t=269 — 4.48 s — and there
the port's settled pose and the decoders' rest disagree badly (max 142/255).
What is known: no element's alpha reverses direction anywhere in this export, so
nothing pulses, which removes the obvious reason to expect a loop without
disproving one. What would settle it: a capture of build 4 alone. The one
live title capture composites the PRESS Ⓐ plate (build 2) over it, so it
cannot be diffed against the title by itself.
⚠️ Independently, both of the port's modes draw a washed-out cyan glow over the title logo that the running game does not have. That is a third problem and it is P3's; it is noted here so nobody reads the loop question as its cause.
Not blocking anything today; raised because the port found them and a guess here would be believed later.
rest_plateau misfires on elements with no exit animation — fixed
rest_plateau misfires on elements with no exit animationFixed 2026-08-28 in sylpheed-formats, and this port's pin moved
8b6dbcf → 5414db3 to take it. The rule adopted is not the condition this
port proposed, which was too loose: a trailing run is the hold exactly when it
is visible. The port's condition would have erased the word PAUSE on
pgptitle.rat, whose trailing run is two identical transparent frames.
Kept for the record, since the reasoning is still what found it:
This one is a decoder bug, not a question, and it was the highest-value item
on this page for the RE agent. ui_layout::rest_plateau excludes a run of
identical keyframes that ends the group, on the grounds that it is the exit. For
an element that has no exit animation the trailing run is the hold, and the
rule falls back to an earlier run — for a slide-in, the invisible pre-roll.
The condition that identifies the affected elements exactly, with no false
positives across this export, is: the final untimed keyframe has the same pose
as the last timed one. Six elements match; rest() misses all six.
ptframe1 and ptframe2 on the main menu are the visible case, and
docs/re/captures/main-menu-oracle.png settles it — the game draws the circuit
bracket that rest calls invisible. sylpheed-cli screen render is missing it
too, so this is not only a port concern.
The port needs nothing here: it derives the arrived pose from the keyframes and
does not use rest. Filed because rest() is used elsewhere and because a
capture already proves it.
Does the game sample a scaled sprite at the pixel corner or the pixel centre?
Found at P1, by the only screen it could have been found on. title_jp's
ptlogo_eff2 is the single drawn element in the whole export at a scale that
is not a whole multiple of 100 % (125 %), and title_jp is the only one of the
twelve screens whose Godot-vs-CLI diff exceeds 6/255.
The two renderers pick different source texels at a non-integer ratio.
sylpheed_formats::ui_layout::blit samples at the destination pixel's top-left
corner (sxi = col * sw / dw); a GPU samples at its centre
(floor((col+0.5)*sw/dw)). At 125 % they disagree on one column in five — ~30
pixels above 100/255, strung along thin diagonal edges. At every whole multiple
of 100 % they agree exactly, which is why the other eleven screens are clean.
The port has not changed to match: matching would mean reproducing a half- pixel bias on purpose to make a number smaller. The question for the RE agent, when it is cheap: a framebuffer capture of the Japanese title screen would settle it outright, and it is the kind of thing a capture answers in one look.
Cost of being wrong either way: a one-texel edge on one glow, on a screen the English boot path never shows. This is filed, not urgent.
The pivot is not half the texture on GP_TITLE
sylpheed-formats's ui_layout::Element::pivot_x is documented as "for a .t32
element this is exactly half the decoded texture's dimensions (verified 7/7 on
the tutorial bundle)". Counting it over the whole of GP_TITLE as exported:
- 55 of 93 sprite-bearing
.t32elements match within ±1 px. - 38 do not, and several are not close:
ptlogo_back2is 1118×262 with pivot (500, 117) where half is (559, 131);ptmsgis 223×38 with pivot (123, 19) where half is (111.5, 19) — the Y matches and the X does not.
This changes nothing today: the exporter emits the declared pivot and never
derives one, and the pivot only affects drawing when scale ≠ 100 %. But it does
matter, because scale is genuinely animated here — 177 keyframes across
GP_TITLE are not 100 %, including on the title screen the port must draw at P1.
The question for the RE agent, when it is cheap to answer: does the running
game anchor a scale to the declared pivot, or to half the texture? The two
differ by up to 59 px on ptlogo_back2, which is visible. Until then the port
follows the decoders and uses the declared pivot, which is also what
sylpheed-cli screen render does — so a P1 diff cannot distinguish them, and
agreement between the two is not evidence.
P1 has now been run and that prediction held. The port and the CLI agree on every scaled element across all twelve screens; the question is untouched by it. It will stay untouched by P2 as well, since P2 animates the same two renderers' shared assumption. Only a capture answers this.