docs: FORMAT v2, what P0 decided, and BLOCKED reconciled
FORMAT is bumped to v2 with a "Changes from v1" table giving a reason per row. v1 was written before HANDOFF answered Q1 and Q3 and before the two-colour modulate was known; each change is a thing v1 could not have said. The paint order moves from `unresolved` into the export, because Q3 decoded it — a u16 layer key at +0x0A, stable-sorted — and a decoded answer is read in the exporter. `paint_order_ties` replaces it, because the tie-break is still unknown. Where a layer key is not in the file it comes from the decoders' table of keys measured off the running game. That is a different kind of fact, so it is labelled: `layer_source` is "sprite", "implied" or "none". BLOCKED is reconciled against HANDOFF at /reborn e81dcad. Seven of its ten rows are answered and are moved out; what remains is Q8 (no cue-to-event binding), the two Q10 unknowns (which BGM, and where a loop restarts), Q9's unsettled skippability, Q4's untested NEW GAME, and Q6's undecoded boot driver. It also raises one question back, found by counting the export rather than by reverse engineering anything: the decoders document a .t32 element's pivot as "exactly half the decoded texture's dimensions (verified 7/7 on the tutorial bundle)", and on GP_TITLE that holds for 55 of 93 sprite-bearing .t32 elements. 38 do not, some grossly — ptlogo_back2 is 1118x262 with pivot (500,117) where half is (559,131). It changes nothing today, because the exporter emits the declared pivot and the pivot only matters when scale != 100%. But scale IS animated here — 177 keyframes across GP_TITLE are not 100%, including on the title screen P1 has to draw — so the question of what the running game anchors a scale to is worth an answer before P2. Noted there that the port and `sylpheed-cli screen render` make the same choice, so a P1 diff cannot distinguish them and their agreement is not evidence.
This commit is contained in:
116
docs/DECISIONS.md
Normal file
116
docs/DECISIONS.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Decisions
|
||||
|
||||
One entry per decision that outlives the container it was made in. Newest last.
|
||||
A decision that lives only in an agent's context is lost when that container
|
||||
dies, which is what this file is for.
|
||||
|
||||
---
|
||||
|
||||
## P0 — the exporter, 2026-08-28
|
||||
|
||||
### The exporter reads one authored file, and stamps its provenance into the output
|
||||
|
||||
`export/` is derived and `authored/` is hand-written, and the natural reading of
|
||||
that is that the exporter never touches `authored/`. But a screen has to be
|
||||
*called* something, and the disc does not name its builds — the identification of
|
||||
build 5 as the main menu is HANDOFF Q2, **measured against a live capture**, not
|
||||
a field.
|
||||
|
||||
Two ways to handle that:
|
||||
|
||||
1. the exporter emits `build_05.json` and the runtime renames it from
|
||||
`authored/screen_names.json`;
|
||||
2. the exporter reads that map and writes `main_menu.json` directly.
|
||||
|
||||
Chose **2**, with a condition: every name it applies carries `name_source:
|
||||
"authored"` and a `name_why` quoting the evidence, and `check` **rejects** an
|
||||
authored name with no `why`. The file that lands in `export/` is therefore still
|
||||
honest about which of its fields is a measurement — which is the property the
|
||||
derived/authored split exists to protect — while a human opening the tree sees
|
||||
`main_menu.json` rather than having to resolve a rename in their head. A build
|
||||
nobody has identified exports as `build_NN` with `name_source: "index"`, which is
|
||||
a locator and not a claim.
|
||||
|
||||
This is the **only** authored input the exporter takes. Everything else in
|
||||
`authored/` is applied by the runtime over `export/`.
|
||||
|
||||
### Sprites are per screen, not a flat pool
|
||||
|
||||
`main_menu` and `extras` both ship a `ptbase.t32` and they are different
|
||||
pictures. A flat `sprites/` directory would have silently collided; whichever
|
||||
screen exported second would have won, and the loser would have drawn the wrong
|
||||
background with no error anywhere. `sprites/<subdir>/<screen>/<name>.png`.
|
||||
|
||||
### The format is executable
|
||||
|
||||
`sylpheed-export check --out export` validates a tree against `docs/FORMAT.md`
|
||||
with no disc in hand. It exists because "the export is correct" is otherwise an
|
||||
assertion, and because the P0 gate is *"validates against FORMAT.md"* — which is
|
||||
not a thing anyone can confirm by reading.
|
||||
|
||||
It reads the tree the way Godot will: as a stranger, with no access to the disc,
|
||||
the decoders, or the exporter's internals. It deliberately does **not** check the
|
||||
export against the disc — that is what `sylpheed-cli screen render` is for, at P1.
|
||||
|
||||
Checked that it bites, rather than assuming: five mutations of a valid
|
||||
`main_menu.json` — a broken `paint_order` permutation, a dangling
|
||||
`focus_sprite`, a reversed `buttons` list, a `#rrggbbaa` colour, an invented
|
||||
`name_source` — are each caught with a specific message.
|
||||
|
||||
### The highlight sprite pairs by name; `opt ` is exported but not believed
|
||||
|
||||
FORMAT v1 said `focus_sprite` came from the element's `opt ` link. That reading
|
||||
was **measured and refuted** by the RE agent, and this export shows why plainly:
|
||||
on the main menu, `opt ` chains `ptloop01 → ptloop02 → ptbtn01` — two decorations
|
||||
and then a button. It is a linked list of something, and it is not focus.
|
||||
|
||||
The highlight is paired by **sprite name** instead (`ptbtn01.t32` ↔
|
||||
`ptbtn01f.t32`), which is HANDOFF's convention and holds for all 54 real pairs on
|
||||
the disc. It resolves all five main-menu buttons. The raw link is still exported
|
||||
as `opt_link`, renamed so that nothing downstream mistakes it for navigation, and
|
||||
so that whoever eventually decodes it has the data.
|
||||
|
||||
Note this is 🟡 a naming convention, not a decoded field. It is authored in
|
||||
effect, and lives in the exporter only because it is a rule over disc data rather
|
||||
than a value we chose.
|
||||
|
||||
### The paint order is exported, not authored
|
||||
|
||||
Q3 decoded it — a `u16` layer key at `+0x0A` of each `T8aD` sprite header,
|
||||
stable-sorted with declaration index. So it is read in the exporter, per the
|
||||
contract's own rule for a decoded answer, and `paint_order` in `export/` is a
|
||||
derived field. `"paint_order"` is gone from `unresolved`; **`paint_order_ties`
|
||||
replaces it**, because the tie-break is still unknown and costs one element's
|
||||
blend on one screen.
|
||||
|
||||
Where an element has no `T8aD` header the key comes from the decoders' table of
|
||||
keys **measured off the running game**. That is a different kind of fact, so it
|
||||
is labelled: `layer_source` is `"sprite"`, `"implied"` or `"none"`, and a
|
||||
consumer that needs to know whether a layer is read or measured can tell.
|
||||
|
||||
### Colours are exported as two fields with the byte order in the name
|
||||
|
||||
There are two modulate colours and they multiply: `tint` is RGBA, `fade` is
|
||||
**ARGB** and its high byte is the alpha that ramps. v1's single `"#ffffffff"`
|
||||
could not carry both and silently discarded the ramping alpha. They are exported
|
||||
as `tint_rgba` and `fade_argb`, raw hex, byte order in the key — because getting
|
||||
it backwards is silent and looks like an art bug rather than a parse bug.
|
||||
|
||||
### `t` stays raw
|
||||
|
||||
HANDOFF Q1 is answered — linear ramp, 2 units per rendered frame, working
|
||||
conversion 1 unit = 1/60 s — but that conversion is **measured off the running
|
||||
game, not read from the file**, and the finding itself flags the 27.6 present-
|
||||
frames/second measurement as the part worth re-testing. If the game turns out to
|
||||
present at 60 Hz, every duration halves.
|
||||
|
||||
So `t` is exported exactly as the disc spells it, `keyframe_time_unit` stays in
|
||||
`unresolved`, and the conversion will live in one authored place at P2. One
|
||||
constant to change, in a file that says it is a decision.
|
||||
|
||||
### The final keyframe has no `t`, and `check` enforces that
|
||||
|
||||
The disc has no time slot on the last keyframe of a group. A file that carries
|
||||
one there has invented it. `check` rejects it — this is the one place where the
|
||||
temptation to emit a plausible number is strongest and the resulting error is
|
||||
completely invisible.
|
||||
Reference in New Issue
Block a user