Commit Graph

270 Commits

Author SHA1 Message Date
Sylpheed RE agent
6cbfe0524f re: the sufficient side -- each phase exit now names the condition that FIRES it
Dominance said a phase cannot end unless X.  A port also needs "once X holds, it
must end", and that is a must-reach set: nodes from which END_PHASE is unavoidable,
as a least fixpoint where n qualifies when it has successors and ALL of them qualify.

The conservatism is deliberate and is the honest answer: a loop never enters the set,
because a poll loop reaches its exit only if the polled predicate eventually becomes
true, which is a liveness property rather than a graph one.

A dominating condition is a TRIGGER when the successor it takes on being satisfied
lies in that set.  Over all 28 stages: 732 dominating conditions, 234 triggers
(31.97%).  isl_report.py phase-guards now tags every line precond / TRIGGER.

The split lands where it should.  Stage 02's phase-1 objective exit is six
preconditions -- player alive, TCN004 destroyed, t <= 210, ADT102/ADT107/ADT113
destroyed -- and exactly ONE trigger: hp_pct_test(ADN101, 0) != 1.  Destroying ADN101
is what fires the phase.  That is a sentence a port can implement.

Per-exit distribution over 172 reachable exits: 89 have exactly one trigger, 42 have
none, 41 have several.  The 42 with none are not a failure -- they are the exits no
branch fires; Stage 02's 0x006260 ends on read_freg(0) < 1200, a timeout, and time
passing is not a property of the graph, so declining to call it a trigger is correct.

Recorded as a heuristic rather than a rule: "the first trigger is the point of no
return" holds for 33 of the 41 multi-trigger exits, with 8 counterexamples where a
precondition appears after a trigger.  The likely cause is that the listing is
ordered by file offset, which is not execution order -- coroutines and jumps let a
lower offset run later.  Not asserted.

calls, phase-ends and conditions all regenerate byte-identical; the two phase-guards
artefacts change only by gaining the tags.
2026-08-27 06:34:39 +00:00
Sylpheed RE agent
6c892b5e25 re: read the three built-ins that appear inside clear conditions -- none named
isl-phase-guards produced the per-phase clear conditions, but three of the built-ins
in them were unread, leaving the conditions half-readable.  All three are read now
via the vtable at 0x820A84BC (control: 69 unit_state -> slot 184 -> 0x8226ADF0).
NONE is named: the corpus has withdrawn two names taken from usage shape, and what I
read does not support naming these.

builtin104 (slot 288, 0x8226BFE0) is a THREE-INSTRUCTION PURE GETTER:
    lwz r11, 10160(r3) ; stw r11, 164(r3) ; blr
So all six tutorial stages S18-S23 end on the value of a single engine-written word,
which is why their exits each have exactly one dominating condition and why
isl-builtins.md only ever saw 104 inside a poll loop.  That word has EXACTLY ONE
writer in the image -- sub_821AA1B0 at 0x821AAD9C, gated on a kind field being in
(16, 32] -- and r29 there is the return of a preceding call, so the meaning of the
value and of the gate are both unestablished.

builtin7 (slot 40) indexes [phase+324] by local[4], bails when rec+16 (the documented
unit state) is 1, 3 or 4, and resolves local[12] through [phase+244] -- symbol table 1.
isl.py's SYM1_SLOTS already listed slot 12 for built-in 7, derived from operand ranges
alone; reading the implementation shows the mechanism behind that.  Two independent
methods agreeing.  What it asks ABOUT the route is still unread.

builtin141 (slot 428) has the same unit-array entry and returns 0 when the unit is
gone; everything past 0x8226CA0C is unread.  Stage 16 calls it twice with arguments
differing in one position (0 vs -4000), which looks like a coordinate -- and looking
like one is exactly the evidence this corpus does not accept.

No artefact changes: the listings already printed builtin104/7/141 and still do.
All four Stage 02 artefacts regenerate byte-identical.
2026-08-27 06:27:00 +00:00
Sylpheed RE agent
2876449965 re: phase-guards for all 28 stages, and a 6/6 cross-check from an unrelated method
isl_report.py now accepts a directory, so the dominance analysis runs over the whole
disc: data/isl-phase-guards-all.txt, 177 phase exits, of which only 5 (2.8%) are
reachable from no static entry.  CFG reach ranges 69.5% (S26) to 95.8% (S25), median
about 4 dominating conditions per exit.

The lopsided number in the per-stage table was the six TUTORIAL stages, S18-S23, each
with exactly ONE exit and exactly ONE dominating condition.  That could have been a
degenerate result, so I looked: it is the same condition in all six,

    END_PHASE  <-  builtin104() != 1

and isl-builtins.md reached built-in 104 from call-site USAGE alone -- "S18-S23 only,
followed by wait_s 39/39, preceded by end_coroutine 37/39, a textbook poll loop".
Usage said 104 is the tutorial's polled test; dominance says it is the tutorial's
clear condition.  Two unrelated methods, six for six.

Stage 16 -- the corpus outlier whose script may be compiled C++ -- resolves as well:
read_freg(0) < 600, player_gauge0_test, player_gauge1_test, and two builtin141 calls
differing in a single argument (0 vs -4000), which is the shape of a position or zone
test.  builtin141 is unread, so it is not named.

Stage 02's separate artefact regenerates byte-identical.

Also added: an RLIMIT_AS cap in isl_report's entry point.  The dominator pass
OOM-killed a run earlier on this 15 GB box; a bad input should now fail the process
rather than the machine.

Still not settled and stated in the doc: dominance gives necessary, not sufficient,
conditions; the 5 unreachable exits need the trigger queue at phase+272; builtin104,
builtin141 and builtin7 all appear in clear conditions and are unread.
2026-08-27 06:19:44 +00:00
Sylpheed RE agent
983c5d112c re: the per-phase clear conditions, by dominance over the ISL CFG
Closes the backlog's "which condition guards each END_PHASE".  With the CFG from the
previous commit this is a graph query, not new machinery.

The obvious query is WRONG for this language, and I implemented it first: "one
successor reaches END_PHASE and the other does not" finds 1/62/1 guards across Stage
02's three phases, and the 1s are both the same read_freg(0) < 1200 timeout -- every
objective test missed.  The cause is the dominant idiom: a POLL LOOP's loop-back
branch also reaches the exit, one iteration later, so neither successor discriminates.
The asymmetric 1/62/1 is what exposed it; a uniform number would have read as
plausible.

Dominance has no such blind spot: a condition dominates an exit when every path from
an entry passes through it, so it is NECESSARY for the phase to end that way, and a
poll loop's test dominates its own exit by construction.  Iterative dominators
converge in 3 passes over 15670/18739 instructions (83.6%).

Result for Stage 02 -- every exit in all three phases is dominated by
unit_hp_pct(TCN001, Character_Player_Test) != 0, the player's ship being alive, which
falls out rather than being assumed.  Beyond that, phase 1's objective exit requires
hp_pct_test on ADT102, ADT107 and ADT113; phase 3's requires ADT301 and ADT302;
read_freg(0) gates at 210 / 300 and times out at 1200; random(3) and random(5)
dominate only the exits that pick one of several closing lines.

Two of the 15 exits are reachable from NO static entry, both FORCE_END_PHASE.  That
agrees with the independently measured 389 unreachable routines: they are started from
the trigger queue at phase+272, by data rather than code.

Practical note recorded: the first dominator run was OOM-killed -- 6743 nodes each
holding a Python set of up to 6743 elements.  Integer bitmasks run in seconds.

Not settled, and said so: dominance gives necessary, not sufficient, conditions; only
Stage 02's artefact is committed; one listed condition is still an unresolved
<unknown>; read_freg's units are inferred from the gate values, not read.

calls, phase-ends and conditions all regenerate byte-identical.
2026-08-27 06:12:41 +00:00
Sylpheed RE agent
29eda81ede re: recover ISL conditions by CFG dataflow instead of a linear walk
The linear walk's 10% unknown was a floor imposed by the method: a block entered only
by a branch has a well-defined state, just not one a straight-line pass can see.
tools/re-capture/isl_cfg.py replaces it with a worklist fixpoint that joins each
block's state over its ACTUAL predecessors -- a value survives only if every
predecessor agrees.

Over all 28 stages:
  instructions reached by the CFG          85.0%
  condition sites, unknown LHS             756 (10.00%) -> 402 (5.32%)
  of those, never reached at all           389
  joined away (predecessors disagree)       13
  both resolve but DISAGREE                161   <- linear walk was wrong here

Those 161 are on top of the 889 the previous jmp fix caught.

Two zero-results on the way, both my own bug, both caught because the number looked
wrong rather than because a test failed:

  * The first CFG run reached only 36% of instructions and made things WORSE (35%
    unknown).  Cause: the phase bases reach almost nothing.  Most routines are
    COROUTINES the engine starts from its trigger queue, with no static predecessor,
    so every start_coroutine target has to be seeded as an entry.
  * That seeding then found ZERO entries in a file with 216 start_coroutine calls,
    because the target is staged in TWO steps -- special[0] = imm, then
    local[0] = special[0] -- and I matched only the direct-immediate form.

Reachability went 36% -> 64% -> 85% as each was fixed.

The 389 still unreached are an honest limit rather than a gap: nothing in the bytecode
starts them; they are entered from the trigger queue at phase+272, by data rather than
code, so no purely static analysis reaches them.

isl_report.py conditions now uses isl_cfg; calls and phase-ends regenerate
byte-identical.  Stage 02 unknowns drop from 71 to 25.
2026-08-27 06:02:37 +00:00
Sylpheed RE agent
b42cd7183c re: builtin80 is a command -- and finding that exposed an 11.75% bug in my tracker
Reading builtin80's body (0x82268460) to name it: it is NOT a predicate.  It
allocates a 20-byte object, stamps vtable 0x820A8CB0, magic 0xAB0311BA and the
unit's live object into it, pushes it onto a queue via the same helper push.i uses,
and returns 1 -- or 0 when the unit is absent.  A command.

That made the conditions listing impossible: it showed a six-way switch
`if builtin80(TCT206) == 0 … == 5` on a function returning 1 or 0.  Disassembling the
site shows two unconditional `jmp`s between the call and the compare, so 0x1B6C0 is
reached ONLY by a branch and its special[0] has nothing to do with builtin80.

op12 is unconditional -- the next instruction is never reached by fall-through -- and
the tracker walked through it exactly as it had walked through end_coroutine.  Last
iteration I fixed the instance and not the class, leaving 22x more bad sites in place
than the fix removed.

A/B over all 28 stages, 7563 sites, resetting at jmp as well:
  sites whose operands change              889  (11.75%)
  LHS unresolved, before -> after     34 (0.45%) -> 756 (10.00%)

So the previous commit's headline "0.0% unresolved" was a MISSING CHECK, not a strong
result: the linear walk always had some value to report, and reporting it was the bug.
10% is the honest figure and the other 90% is trustworthy for a reason.

Also corrected: isl-unit-args.md illustrated its diff with 0x1B6C0, which is one of
the bogus sites.  The UNIT_ARG result itself stands -- it came from reading
implementations, not from this listing -- but the example was picked from bad output.

Not done, and said so: recovering the 756 needs a dataflow join over each block's
actual predecessors, a CFG fixpoint rather than a linear pass.  The branch targets are
all known so the CFG is available; the analysis is not written.

calls and phase-ends regenerate byte-identical; conditions changes on 187 lines.
2026-08-27 05:53:20 +00:00
Sylpheed RE agent
8af97adbc5 re: derive UNIT_ARG from the implementations -- 31 built-ins take a unit, not 31 of 55
isl.py's UNIT_ARG decides whether a built-in's slot-4 operand prints as a unit name
or a raw number.  It was inferred statistically from operand ranges and, by its own
comment, listed a slot "only when the ratio stayed below 1.0" -- conservative.

The vtable base makes it a lookup instead: every unit-taking built-in's implementation
opens with lwz 324(phase) / lwz 4(argbase) / rlwinm 2,0,29 / lwzx / lwz 4(rec).  Read
directly for all 147:

  implementation indexes [phase+324] by an argument   55
  of the statistical set's 31, confirmed              31  (zero false positives)
  UNIT_ARG claims a unit, implementation does not      0
  implementation says unit, UNIT_ARG missed it        24

The 24 include builtin80, group_ratio_pct, is_engaged, set_unit_flags,
squadron_trace, wait_units_ready and deploy_and_wait.  Hand-verified by reading
builtin7, 16, 80, 105, 117 and 136.

Recorded because it nearly passed: my FIRST control -- whether the additions' operands
resolve to a symbol-table-2 index -- is worthless.  The additions score 100.0%, but so
do the 31 baseline (100.0%) AND the 92 built-ins in neither set (99.3%).  Symtab 2 is
dense enough that almost any small integer lands in it.  A control the negative class
also passes is not evidence.

The control that discriminates is the tag word: a symbol operand is a two-word pair
whose first word is the constant 1, so slot0 == 1 exactly when slot 4 is a unit --
100.0% (13677 calls) / 100.0% (140) / 2.5% (2903).  A 40x separation.

Artefacts: isl-stage02.txt and -phase-ends.txt regenerate byte-identical; -conditions
changes on 28 sites, every diff line pairing, each a raw number becoming a unit name.

Left unnamed on purpose: all 24.  builtin80 returns a small enum (tested 0..4 in a
switch) but its body past the liveness check is unread; builtin103 is a predicate over
[phase+10152]/[phase+10156]; builtin105 tests a unit record's +16 against 4.
2026-08-27 05:45:10 +00:00
Sylpheed RE agent
4f37a4eb4c re: fix the wrong ISL conditions -- the cause was a coroutine boundary, not the filter
The listing showed end_coroutine as the left-hand side of 34 comparisons disc-wide.
That is impossible -- it returns no value a script can test -- so it was the bug
reporting itself.

The recorded fix ("set special[0] only for built-ins that write [phase+164]") is
REFUTED.  end_coroutine's handler 0x82272624 is `addi r11,r0,1 ; addi r3,r0,3 ;
stw r11,164(r31)` -- it DOES write [phase+164], so that filter would have kept it.
Reading the handler before writing the filter is what caught this.

The real cause: end_coroutine returns 3, which DESTROYS the thread.  Execution does
not continue past it, so the instructions following it in the flat stream belong to
a different routine and every tracked value is stale.  The linear walk that makes
the decode possible is exactly what walks across that boundary.

A/B over all 28 stages, 7563 sites, resetting the tracker at end_coroutine:
  sites whose operands change            34  (0.45%)
  LHS = end_coroutine, before -> after   34 -> 0
  left as an explicit unknown            34  (0.45%)

The two counts being equal is the result: the leak was confined to exactly the sites
that displayed the impossible value, so the other 7529 conditions were never
affected.  Those 34 now print "<unknown: reached after a coroutine boundary>".

Not done, and said so: their RHS is still exact and the LHS is recoverable by seeding
the tracker at coroutine entries, whose targets are staged slot 0 of start_coroutine.

data/isl-stage02-conditions.txt regenerated; calls and phase-ends both byte-identical.
2026-08-27 05:36:02 +00:00
Sylpheed RE agent
f32def9a05 re: resolve every ISL condition's comparand -- the clear conditions are readable
The deque ops are an EXPRESSION STACK: push the left operand, evaluate the right
(a built-in call, whose result lands in special[0]), pop the comparand back into
special[1], compare.  Tracking that through the linear decode is enough to recover
what each site tests.

Evidence the model is right, not just plausible:
  push vs pop across all 28 stages          1877 vs 1877
  files that underflow or end unbalanced    0 of 28
  Stage 02 pop.i sites followed by cmp.i    319 / 319
  ops immediately before a pop.i            call x313, cmp.a x6

isl.conditions() recovers 7563 condition sites disc-wide with 0.0% left as an
unresolved special[N]; 83.2% have a built-in call as the LHS and 99.7% compare
against a plain number.  Most-tested: hp_pct_test 1955, unit_state 1257,
unit_relation 796, dist_lt 450, unit_alive 413.

They read as conditions now:
  if unit_alive(TCN105) != 1
  if hp_pct_test(ADT308, 0) != 1
  if dist_lt(ADT308, TCN000, 15000) != 1      (world unit = 1 m, so 15 km)
  if unit_state(ADT308) == 1

data/isl-stage02-conditions.txt was a stale artefact with NO generator -- the thing
isl_report.py's docstring complained about.  It has one now (isl_report.py
conditions).  The calls and phase-ends artefacts both regenerate byte-identical, so
the change is additive.

Recorded rather than glossed: 15 of Stage 02's 965 sites (1.6%) attribute the LHS to
end_coroutine, which returns no value -- the tracker sets special[0] on EVERY call,
so those show a stale value and are wrong, not imprecise.  The fix is to set it only
for built-ins that write [phase+164], which the vtable work makes checkable.
2026-08-27 05:28:50 +00:00
Sylpheed RE agent
0287a78633 re: close the ISL operand chain -- built-in result is special[0], via a vtable
Answers what the previous commit left open: naming the branches did not give a
clear condition, because that needs the operand chain feeding each compare.

First, a correction to my own work.  isl-bytecode.md -- which OWNS the opcode table
-- already named ops 21-24 push.i/push.f/pop.i/pop.f.  isl-branches.md, which I
wrote last iteration, said op21 and op23 were unread.  The stale file was mine.
Verified from the thunks rather than accepted: 21 pushes [phase+168] onto the deque
at phase+44, 22 pushes [phase+184] onto phase+64, and the 23/24 handlers touch only
r3+168 and r3+184.  So pop.i lands in special[1].

New: the 147-entry built-in table is a thin DISPATCH LAYER, not implementations.
Each stub resolves the local[] argument base and tail-calls a fixed ScriptPhase
vtable slot.  112 of 147 dispatch that way; 17 write [phase+164] inline; 0 write
+184.  Every named predicate is in the vtable group -- unit_state 184, unit_alive
188, hp_pct_test 64, dist_lt 56, is_engaged 252, timer_elapsed 372 -- which is the
control that the split separates engine queries from script bookkeeping.

The vtable is 0x820A84BC, derived from a known implementation rather than a stride:
MARK_LAST_PHASE is documented as [phase+300]=2; the function 0x8226B498 is exactly
that stub; it appears as a data word at exactly one address, 0x820A8570; built-in
39 uses slot 180.  The check NOT used in the derivation: built-in 40 mark_not_last
uses slot 176, and slot 176 holds the [phase+300]=1 stub.  Predicted and confirmed.
The db's own vptr_writes independently lists 0x820A84BC, written at 0x82261B80.

unit_state = slot 184 = 0x8226ADF0, which indexes [phase+324] by local[4] and writes
its answer to [phase+164] = special[0] at both exits.  The phase-3 poll loop now
reads end to end: unit_state(ADT308) -> special[0]; pop.i -> special[1]; cmp.i; beq.

isl.py names ops 21-24; the calls artefact regenerates with NO diff.

Left open and said so: the other 111 vtable slots, which comparand each site pushes,
the 35 non-vtable built-ins, and the vtable's length.
2026-08-27 05:21:10 +00:00
Sylpheed RE agent
504a042698 re: read the ISL branch handlers -- it is a condition-code machine
Closes the backlog item that was the last thing between the flat decode and a
per-phase clear condition, and closes isl-builtins.md's standing "op10 + op13 look
like a switch -- NOT confirmed".

op10 resolves two operands, issues a SIGNED cmp, and writes three condition bits to
a bitset at phase+24: bit 0 = EQ, bit 1 = GT, bit 2 = LT.  op11 is the same machine
for floats via fcmpu.  op13-op18 branch on those bits to [phase+232] + word@+4 --
the same phase-relative target form as the unconditional op12:

  13 bit0 set    beq      16 bits 2 then 0   ble
  14 bit0 clear  bne      18 bits 1 then 0   bge
  15 bit2 set    blt      17 bit1 set        bgt

13/14/15/17 are byte-identical apart from the bit index and the polarity.  All six
relations are present and each appears exactly once; that completeness is the check
that the reading is right, rather than the usage pattern -- which the item
explicitly warned against.

Operand order recorded because it is easy to reverse: LHS = (kind byte[1], word@+4),
RHS = (kind byte[0], word@+8).

Method note in the doc: the jump table at 0x822635FC holds THUNKS, and the handler
is the bl target inside each.  My first pass guessed handler addresses at a fixed
stride, landed mid-function, and produced a 20-line "difference" that was pure
misalignment.

isl.py names the ops; data/isl-stage02.txt is regenerated and every diff line pairs
exactly, only the op-name column changing (op10->cmp.i x5, op13->beq x4,
op14->bne x1).  data/isl-stage02-phase-ends.txt now shows the phase-3 poll loop
reading as one: unit_state(ADT308) -> op23 -> cmp.i -> beq back to 0xFEB4.

Left unnamed on purpose: op23 (0x82271C30) and op21 (0x82175C20).
2026-08-27 05:10:50 +00:00
Sylpheed RE agent
228680edf0 re: the ISL stream is flat -- refute the "needs coroutine entry points" blocker
Two files (isl_report.py's docstring and structures/isl-builtins.md) recorded the
same blocker on a faithful per-phase condition listing: that it needs the coroutine
entry points from start_coroutine's operand.  Measured against isl.call_sites(),
which enumerates by scanning the encoding rather than by decoding and so is an
independent denominator:

  linear + jumps, stopping at ret (what the tool did)   133 / 2846 =   4.7%
  linear + jumps, continuing past ret                  2275 / 2846 =  79.9%
  ... + following start_coroutine (the recorded fix)   2355 / 2846 =  82.7%
  plain linear decode, no control flow at all          2846 / 2846 = 100.0%

Following the coroutine entries buys 2.8 points.  Disc-wide, a plain linear decode
from the first phase base reaches 25705/25705 call sites over all 28 stages, and
28/28 decode clean to code_end with no desync.

The real bug was isl.dis ending on `if op == 20: break`.  Op 20 is `ret`, but this
is a coroutine VM -- the thread suspends and resumes at the FOLLOWING instruction,
so code continues past it.  dis() now takes stop_at_ret (default True, preserving
the old output: data/isl-stage02.txt regenerates byte-identical) and
isl.linear_offsets() is the correct walk.

By-product, kept with its control: start_coroutine's target is staged slot 0 --
73/83 phase-1 sites land on a valid instruction, against a 38.7% chance rate for an
arbitrary 4-aligned offset.

New artefact data/isl-stage02-phase-ends.txt with a committed generator
(isl_report.py phase-ends).  It shows END_PHASE's call site is the WRONG place to
read a clear condition: all 12 Stage-02 sites sit in one stereotyped outro.  Not
settled, and stated as such: op10/op13/op14/op21/op23 are unread handlers, so the
condition in the poll loop upstream cannot be named yet.
2026-08-27 05:00:49 +00:00
Sylpheed RE agent
a22ef5f96f re: LOCATE REMAINING OB at 0xbdb69668, and refine the ...9668 pattern
Ran this file's own transition filter on a guarded Stage 02 run (stage asserted),
reading the HUD from a crop taken at the same instant as each memory sample:
scan at HUD 004 -> 41537 candidates; filter at HUD 008 -> 8; verify across the
008 -> 012 transition, which was NOT selected on -> exactly ONE survivor.

That survivor, 0xbdb69668, tracked 4 -> 8 -> 12 against the HUD's 004 -> 008 ->
012.  The other seven collapsed into noise at the first unselected transition,
which is precisely what that rule exists to catch.

The file's "try 0xbdb59668 first, re-scan when it reads 0" rule worked verbatim:
it read a hard 0 here, and the re-scan cost about the predicted five minutes.

The ...9668 page-offset pattern is REFINED, not reinstated: the three located
addresses (0xbdb49668, 0xbdb59668, 0xbdb69668) are three ADJACENT 64 KB pages at
one offset, and in this run exactly one of 8192 probed pages held 12 -- the
counter -- making it a one-step lookup.  But the 2026-08-26 refutation stands as
measured (zero ...9668 VAs held the HUD value in that run), so this is a fast
heuristic to be HUD-checked, not a law.
2026-08-27 00:23:22 +00:00
Sylpheed RE agent
2add603485 re: refute the "counter sits at page offset 0x9668" prediction
The file proposed that the counter lives at a fixed offset inside an allocation
whose base moves by whole 64 KB pages, and stated the test itself: "a third scan
should again land on ...9668".  Ran it on a fresh guarded Stage 02 run.

Probing all 8192 pages of the form 0x????9668 across 0xa0000000-0xbfffffff: with
the HUD at 004, exactly two VAs held 4 (0xbc3f9668, 0xbe3f9668); with the HUD at
012, ZERO held 12.  Both candidates also failed the file's own transition rule --
over 252 s 0xbc3f9668 held a flat 4 and 0xbe3f9668 flickered 4/0 while the HUD
went 004 -> 012.

Both arms were sampled at the same instant (cropped HUD digits beside each memory
read), after a stale-screenshot comparison earlier in this session produced a
spurious 13-vs-004 mismatch.

Scope kept narrow: this refutes the page-offset prediction, not the confirmed
finding that 0xbdb59668 carries the counter in some runs.  The counter's address
in THIS run remains unknown -- no transition filter was run.
2026-08-26 23:57:25 +00:00
Sylpheed RE agent
c2b7c93792 re: correct my own OB write-up -- I refuted a stale status line, not an open question
The section I added last iteration claimed to be testing an untested 🟡 and
concluded "the address is run-dependent, always re-derive".  Both halves were
wrong, and the evidence was already further down the SAME file:

 - cross-run stability was not untested -- the body records 0xbdb59668 carrying
   the counter in 3 of 5 measured runs and reading a hard 0 in the other two.  The
   stale claim was in the status line at the top, which I took at face value.
 - "always re-derive" is worse advice than the rule the file already gives: try
   0xbdb59668 first, check it against the HUD, re-scan only when it reads 0.

What my run genuinely adds is a SIXTH data point with a new failure mode: the
address read neither the counter nor 0 but 95748078, constant over four samples.
Status line rewritten to match the body (🟡 recurs but not universal), so the
document no longer contradicts itself.
2026-08-26 23:57:01 +00:00
Sylpheed RE agent
45a46fadea re: REMAINING OB's address is run-dependent -- cross-run stability refuted
The doc carried a 🟡 saying cross-run stability was untested.  Tested now on a
fresh guarded Stage 02 run (stage asserted): the HUD reads "Remaining OB : 004"
while RAM at the documented 0xbdb59668 reads 95748078, constant over four samples
12s apart.  Not 4, not near 4, not moving.  So the address belongs to that run's
heap, as the corpus's own heap-reallocation warning predicts.

No constant-shift shortcut either: a BE u32 equal to 4 occurs 1654 times within
+-1 MB of the old address and 11202 times within +-16 MB, far too many to isolate
without the transition filter.  The durable result is the METHOD (ob_scan.py:
scan at one value, filter against live memory at a DIFFERENT value), not the
number.

Also fixes a contradiction in INDEX.md, which said in one row that the address is
"still " while another row linked the doc that had already CONFIRMED it.
2026-08-26 23:34:36 +00:00
Sylpheed RE agent
4673818c03 re: RESTORE "one world unit is one metre" -- the objection conflated two families
The 2026-08-26 downgrade rested on two numbers, and grouping the CollisionSet
names by prefix dissolves both:

 - The 447 km "largest object" is rob_f002_cmesh, one of only FOUR rob_ meshes
   (44k-448k).  That family is not hull size: rob_f001_bdy_cmesh measures 50179
   and f001 is the player's own Delta Saber, whose disc record gives Size_Radius
   10.0.  A 50 km player fighter is impossible under every unit convention, so
   rob_ spans something else entirely and is not evidence about the world unit.
 - The "small" 133 m craft is small: rou_e010_cmesh = 133.2 is the 7th smallest
   of 78 rou_ meshes (median 636.2, max 9534.9).  Against its own family, the
   game calling e010 small AGREES with the metre.

Measured this run: the 500 km box is a cube of exactly 500000.0 units on every
axis (+-250000), and the second box mesh is exactly 100000.0 -- the same ruler.

Independent dynamic confirmation already in the corpus (flight-speed-law.md):
443.6 world units per wall-second, mission clock 1.26x wall, so 352 units per
game-second against a HUD reading of 350.
2026-08-26 21:18:54 +00:00
Sylpheed RE agent
ce0720a3e9 re: correct a mislabelled blocker -- the world unit needs a run, not a waiver
I wrote "blocked on the oracle" for the unit-to-metre conversion.  That was a
mislabel: red is for what the container cannot do, and run-canary works here.
What the km-name sweep actually established is narrower -- no STATIC test can
settle it, because the disc has exactly one size-bearing asset name.

The run is well-supported by tooling that already exists: findplayer.py
recovers the player position triple from motion, the HUD prints the distance to
the selected target in the game own units, and the same separation read both
ways is the conversion.  Recorded as amber with the experiment written out.
2026-08-26 10:07:01 +00:00
Sylpheed RE agent
35f67a2a37 re: retract the paint-key census and redo it over all 21 184 sprites
The census filtered pak entries whose own first four bytes are T8aD.  A sprite
is usually a child of a RATC bundle, and a bundle entry's magic is RATC, so a
top-level magic filter cannot see one:

    top-level T8aD entries (counted)    4 525 sprites,  45 keys
    T8aD inside RATC bundles (missed)  16 659 sprites, 204 keys
    both                               21 184 sprites, 216 keys

171 of the 216 keys exist only inside bundles.  The sharpest statement of the
error: that census never saw GP_TITLE.pak at all -- the pak holding both of the
screens this page's entire evidence comes from.

Retracted: "45 values", "the keys are pak-local", "each auxiliary pak occupies
its own narrow high-byte band".  On the full population 68/216 keys (31%, not
9%) cross a pak family and the per-pak ranges overlap heavily -- GP_BUNK
0x8000-0xa110, GP_TITLE 0x8000-0xc150, GP_LEADERBOARD 0x8000-0xf100.  The tidy
banding was an artifact of seeing one or two keys per pak.  So the key looks
like a shared vocabulary, which is the opposite of what I published.

Survives, now on the full population: the field is a u16 at +0x0A (upper half
zero 21 184/21 184), and it is an enumeration (216 values for 21 184 sprites).

Three wrong numbers on this page now, all the same shape -- a statistic computed
over a population I had not checked was the population in question.  Stated once
at the end of the section rather than three times: check the sampling frame
before the statistic.
2026-08-26 10:06:23 +00:00
Sylpheed RE agent
e5dde6f4db re: flag that the paint-key census may have sampled the wrong sprite population
The census filters pak entries whose own first four bytes are T8aD.  The
sprites this page measures paint order on are children of a RATC bundle --
ui_layout.rs reaches them via ratc::parse, and a bundle entry's magic is RATC,
so a child T8aD never matches a top-level magic filter.

So the 45 keys may describe a population that only partly overlaps, or does not
overlap at all with, the one the page's two measured screens come from.  I do
not yet know which; the comparison is running.  Marking the section rather than
leaving the counts to be read as covering the screens' sprites.

Same failure shape as the 37/45 language-duplication note lower down the page:
a number computed over a population I had not checked was the population in
question.  Recording it as such.
2026-08-26 09:58:32 +00:00
Sylpheed RE agent
e0394647b2 re: census the T8aD paint-order key -- it is pak-local, not a global vocabulary
The page rested on twelve values from two screens.  This walks all 4525 sprites
on the disc.

  * The field is a u16 at +0x0A.  The upper half of the 32-bit word the page
    reads is zero in 4525/4525.  Nothing above changes -- 0x00008100 sorts the
    same as 0x8100 -- but a future value with the high half set would mean
    something had been misread rather than that the layer got deeper.
  * It is an enumeration: 45 values for 4525 sprites, one of which (0x8100)
    covers 1188 of them.
  * The reading worth trying -- a global layer vocabulary shared across the UI
    -- is refuted.  Only 4 of 45 keys cross a pak family and 33 of 45 live only
    in GP_MAIN_GAME_2D; every other pak owns a narrow high-byte band (0x90-0x94
    for the in-game overlays, 0xa4 mission log, 0xb1-0xb2 save/load).  A screen
    that owns one or two keys is not ordering itself with them.

That supports "group id in the high bits, order in the low bits", which is what
the page already suspected, but it does NOT test it: paint order has been
measured on two screens and both are inside GP_MAIN_GAME_2D, so there is no
ground truth to check the split against.  Left amber.

The first number I got was 37/45 shared, which would have supported precisely
the wrong conclusion.  It came from counting paks instead of pak families: the
six GP_MAIN_GAME_*2D paks are the same screens in six languages and their key
sets are byte-for-byte identical.  Recorded on the page, because the shape
recurs -- a corpus with near-duplicate members manufactures agreement.
2026-08-26 09:50:29 +00:00
Sylpheed RE agent
83d019515f re: confirm the cmesh<->model link via GameResourceID; world unit is oracle-blocked
Two follow-ups on yesterday's^Wthis morning's CollisionSet write-up.

1. The _cmesh <-> render-model link, which I recorded as UNTESTED because
   matching stems against .xbg object names covered 4 of 158.  The disc keeps
   only one build manifest, so that corpus was never going to answer it.  The
   right corpus is the GameResourceID field of the DefTables / GP_MAIN_GAME
   records -- 480 distinct values.  Against those, with a control that shuffles
   the characters of each stem:

       ship/mob stems prefixed by a real resource id   108/112 = 96.4%
       same stems, characters shuffled  (control)        0/112 =  0.0%
       asteroid stems prefixed (expected none)           0/46

   So a CollisionSet entry is <GameResourceID>[_<part>]_cmesh.  The 0/46 on
   asteroids matters as much as the 108/112: a test that fired on everything
   would be the bound-check hazard again.

2. The world unit.  Sweeping every pak for a name carrying a kilometre figure
   returns mapmesh_box_500km.col/.rgn and nothing else -- 162 references, all to
   that one pair.  The reading rests on a single filename with no corroborating
   instance anywhere in the data, so no static test can settle it; marking it
   blocked on the oracle rather than leaving it as an open static question.

   My objection's premise did survive: rou_e010 is a real GameResourceID and
   e010_ADAN_Attacker_S is in the stage tables, so the 133-unit mesh does belong
   to a craft the game calls an attacker.  Whether the trailing _S means "small"
   is a further guess (there are _EX4 / _HF / _HF_Wayne variants), so it stays
   suggestive rather than evidence.
2026-08-26 09:41:46 +00:00
Sylpheed RE agent
cc1f9e6734 re: decode CollisionSet_*.bin -- the per-object collision-mesh library
All 18 blobs are byte-identical: the per-stage naming is nominal, and every
stage points at one shared 1675148-byte library stored eighteen times.  That
identical size was the reason to open the item, and it turned out to be the
answer to it.

Record layout: {u32 size, u32 name_len, char name[name_len], u32 nv, u32 nt,
f32[3] x nv, u32[3] x nt}, next record at off + 8 + size.  The indices are u32
here where MCOL uses u16 -- two different serialisers in one archive.

What makes this a decode rather than a plausible reading: the walk consumes the
file to the byte over 158 variable-length records, with the size word predicted
from the two counts 158/158.  A wrong field would desynchronise within a few
records and could not land exactly on the end.  All indices in range 158/158;
98.24% of edges shared by exactly two triangles; 147/158 fully manifold.

158 meshes, 90 836 triangles: per-part ship proxies (_bdy/_brg/_eng/_wep/_sld,
the XBG7 sub-part vocabulary) plus 46 stage asteroid meshes whose prefixes are
exactly the stages that have an _AsteroidVolume_wp MCOL.

Two things this file makes me walk back:

  * The "1 unit = 1 metre" reading from mapmesh_box_500km is downgraded to
    amber.  The 500000 arithmetic stands, but it implies that a craft the game's
    own tables call "small" is 133 m and that rob_f002 is 447 km -- 89% of the
    arena width.  The format check survives; the interpretation has no
    independent support.
  * The _cmesh <-> render-model name link is recorded as UNTESTED, not
    confirmed: only one .xbg build manifest survives on the disc, so matching
    stems against object names covers 4 of 158, which is no coverage at all.
2026-08-26 09:31:22 +00:00
Sylpheed RE agent
3916f30ce2 re: name-resolve all 40 MiscBin entries, and demonstrate the REGN<->MCOL pairing
The names live outside MiscBin: they are the MapPath / MapMesh /
CollisionMeshes field values of the per-stage StageResource object (IDXD schema
3c9ae32e, in every GP_MAIN_GAME_<lang>.pak), and each hashes with the ordinary
pak name_hash straight to a TOC entry.  40/40 resolve, no collisions -- the 11
REGN as <stem>.rgn, the 11 MCOL as <stem>.col, and the 18 remaining blobs as
CollisionSet_S01..S16 / _Tutorial / _test.bin.  The .pe string table at 651540
was the way in: MapMesh and MapPath sit adjacent there.

This upgrades the pairing claim.  The first section of mcol-collision.md could
only say REGN and MCOL had matching *distributions* of bbox and cell size, and
flagged that as not an object-to-object link.  A phase record names one .rgn and
one .col, and all 11/11 pairs share a stem and agree exactly on both.

The names also check the format work from outside it: mapmesh_box_500km.col is
the object decoded here as 8 vertices and 12 triangles spanning exactly
+-250000, and its name says that cube is 500 km across -- so one world unit is
one metre, and a wrong stride could not have produced a box that measures what
its own filename claims.  70 of the 87 phases use it: most stages' only
collision is the arena wall, and _AsteroidVolume_ names the rest.

Still open: the 18 CollisionSet_*.bin are named but not decoded (all exactly
1675148 bytes), and CMapColliderBridge in the RTTI names the runtime consumer
without following it into the code.
2026-08-26 09:21:00 +00:00
Sylpheed RE agent
e1db4d84ff re: MCOL solved -- a closed triangle collision mesh in a uniform grid
The 0x50 header word, which the first section of this page had dismissed as "a
large value", is two u16 counts: vertices and triangles.  They give the two
remaining blocks their stride, and every derived length is exact in 11/11 --
len(0x54) == align16(12*nv), len(0x58) == align16(6*nt), and nt equals the
bounding-sphere count decoded last iteration.

Checks that cannot pass by accident:

  * sphere i is the TIGHT bounding sphere of triangle i, 4768/4768, with
    max|v-c|/r median 0.99990 (a fixed 1.0001 epsilon), against a 1.32%
    random-triangle control;
  * the mesh is watertight -- every edge shared by exactly two triangles,
    7152/7152, zero degenerate triangles, zero unreferenced vertices;
  * the two smallest objects are 8 vertices and 12 triangles whose positions
    are the eight +-250000 corners of the map bbox: a bare bounding cube.

The cell lists are a correct broad phase: with an exact triangle/box SAT test
only 3 overlapping triangles in 18 577 entries are absent, so a query walking
one cell's list cannot miss a hit.  The 730 conservative extras bracket the
builder's own test between exact-SAT and AABB, which retires the 18 unexplained
"sphere misses" from the previous commit as that same margin.

mcol_probe.py gains `mesh` and `obj`; `verify` now runs all three checks and its
output is recorded in docs/re/data/mcol-verify.txt.
2026-08-26 09:12:36 +00:00
Sylpheed RE agent
cda5a31601 re: MCOL's 0x5C block is bounding spheres at stride 16 -- the 0.75 was 12/16
The unexplained ~0.75 ratio left at the end of the last iteration was my own
stride.  I had read the block as 12-byte points because REGN's vertex section
is 12 bytes, and never checked it: len(0x5C) is not a multiple of 12 in 5 of
the 11 objects, so that stride was never arithmetically possible.

At stride 16 the relation is exact in 11/11 -- max u16 == len(0x5C)/16 - 1 --
and the record reads as {centre f32[3], radius f32}.  Powered test, since a
u16 is reached through a specific grid cell: the sphere it names reaches that
cell in 18 559/18 577 = 99.90%, against a 12.02% random-sphere control.  Both
fields carry signal (centre alone 26.75%, radius shuffled 70.19%).

The converse -- is the list *exactly* the intersecting set? -- is 0.38%, which
is the expected direction: a bounding sphere is conservative, so membership
implies overlap but not the reverse.  The tighter geometry is in 0x54/0x58,
still undecoded.  18 entries (0.10%) go the wrong way and are recorded as open.

tools/re-capture/regn_decode.py is copied unchanged from auto/regn-reader so
the probe's POF0 reader is the known-good one rather than a second copy.
2026-08-26 09:04:30 +00:00
Sylpheed RE agent
324389c612 re: MCOL's u16 entries do not index the point block -- the bound-check was a false positive
Two tests. Counts modulo 3 are spread across all three residues (639/2063/1786),
so the u16 array is not a triangle list. And a B record is reached through a
specific cell, so a point it references should lie in that cell -- referenced
points score 0.79% against a 0.48% random-point control. Chance.

The contrast is the point. One section earlier the same u16 entries scored
18379/18379 (100%) on 'are these valid point indices'. I flagged that at the
time as the weak bound-check and recorded it as consistent rather than as a
finding. The caution was right: the powered version of the same question now
returns chance, and had the 100% been written up as the decode this page would
carry a confident false statement about MCOL's geometry.

Fifth appearance of the pattern across REGN and MCOL and the first time both
halves have been run on the same field, so the page now states it plainly: a
bound-check asks whether something could be an index, and the answer is set by
the size of the target collection rather than by the field's meaning.

Datum for the next attempt: the maximum u16 is consistently about 0.75x the
point count (923/1232, 1019/1360, 1163/1552, 59/80), too consistent to be
coincidence and not explained.
2026-08-26 08:52:44 +00:00
Sylpheed RE agent
ee3188bd4c re: MCOL array B decoded -- {count, u16 index array}, and the chain closes
Two corrections got there. MCOL has 9020 relocated words and only 4488 are
A-record pointers; I assumed the rest sat at B+8 mirroring A, refuted 0/4488.
Measuring their offset from the nearest preceding B record gives B+4 for 4488
(99.0%) and 44 before the first B -- exactly the four header pointers times 11
objects. Nothing unaccounted for.

So B is {u32 count, pointer at +4}. The pointers advance by exactly twice the
count: 4477/4477 (100.00%) over all 11 objects. That is a packed u16 array with
no padding, and it is the load-bearing evidence -- an exact arithmetic identity
over 4477 consecutive pairs. The companion check that those u16s are valid point
indices is the same weak bound-check flagged earlier and is recorded as
consistent rather than as evidence.

Chain: position -> cell -> A {cell index, count 1, ->B, sphere} -> B {count n,
->u16[n]} -> n indices into the point block. Same shape as REGN's cell -> item
-> refs -> geometry.

Open: the 0x54 and 0x58 blocks, which this chain never reaches, and what the
indexed points form.
2026-08-26 08:45:58 +00:00
Sylpheed RE agent
fe06b26751 re: MCOL A->B is 1:1 with count 1, matching REGN's cell-index convention
Filtering array A by the cell-index criteria and following each pointer: every A
record points at a distinct B record, 11/11, and every A count field is exactly
1, 11/11. Per object the A-record count is the number of occupied cells (110 to
575) and the counts sum to it exactly.

That is the same design REGN uses -- the corpus already records 'every occupied
cell has count exactly 1' there. Two sibling formats, one convention, and a
further independent confirmation of the A reading since the filter and the
cardinality are unrelated criteria.

Array B resisted, and both attempts failed in ways worth recording. The record
boundary was off by 8 again, producing records that start with the tail of the
previous structure -- the same mistake as the 0x74 check two iterations ago. And
the u16-index test had no power: the 0x5C block holds ~1232 points, so 'is this
u16 below the point count' passes for almost any small value, and duly reported
100% at seven offsets. Fourth time in these two formats that a bound-check
against a large collection produced a meaningless 100%.

Recorded what would have power instead: B records are 1:1 with occupied cells
and A carries that cell's bounding sphere, so a B field can be tested for
spatial consistency with that specific cell.
2026-08-26 08:38:46 +00:00
Sylpheed RE agent
e684eb45dd re: MCOL array A confirmed as the per-cell record -- 100% once split
Separating the two interleaved arrays by address and re-running the same three
criteria: array A (2509 records) gives byte3==1 at 100.00%, a valid cell index
at 100.00%, and the sphere reaching that cell at 99.92%. Array B (6467) gives
30.65% and 30.60% -- a different record type, and the control showing A's 100%
is not what any 32-byte block would score.

So array A is the per-cell record: cell index (x,y,z), count, pointer into array
B, bounding sphere -- the same role REGN's section 3 plays. That confirms the
earlier 50% was the interleaving artifact and not a half-working reading.

The split was crude, first-half-by-address giving 2509 vs 6467 rather than an
even cut, and A still came out at 100%. A rough partition isolating a perfect
population is stronger than a careful one isolating a good-ish population.

Array B's layout is still unread.
2026-08-26 08:32:41 +00:00
Sylpheed RE agent
d1c89e5d79 re: MCOL's 32-byte record is a cell entry, and there are two interleaved arrays
Read as 8 big-endian words, word 0 as four bytes is (x,y,z,1) -- a 3-D cell
index, matching the object's 5x5x5 grid. Words 4-6 are a position and word 7 a
positive scalar (a bounding sphere); word 1 is a count, word 2 the relocated
pointer. Every record pointer lands in the same region, 8976/8976, each a fixed
distance on with the same stride -- so there are two parallel arrays, A and B.

The 50% is the tell. Three independent criteria -- byte 3 == 1, valid cell
index, sphere reaches that cell -- all land on 50.0%, which says half the
records are not this type rather than that the reading half-works. The POF0 slot
list interleaves both arrays and I was testing B's records against A's layout.

So array A is a per-cell record, the same role REGN's section 3 plays. Array B
is unread, and the criteria have not yet been re-run on A alone -- if the
reading is right they should go to 100%.
2026-08-26 08:31:46 +00:00
Sylpheed RE agent
a2aa0a4cf8 re: MCOL's pointer layout, read from POF0
Used the known-good decoder rather than my own broken one. My version had three
errors: the delta stream starts at table+16 not +8, the tag bits are
0x40/0x80/0xC0 rather than 0/1/2, and slots carry the +16 fixup base. Sanity
check passes -- on REGN the tool returns header slots 0x70-0x84 exactly.

On MCOL, over all 11: the header-region relocated slots are exactly 0x54, 0x58,
0x5C and 0x74 (11/11); 0x5C resolves to 0x80, the first byte after the header
(11/11); and 0x74 resolves to eight bytes before the first array pointer (10/11).
So MCOL has four top-level pointers where REGN has six.

92.7% of gaps between consecutive relocated words are 32 bytes, in 7-127
contiguous runs per object, which with the 0x74 offset reads as arrays of
32-byte records each carrying a pointer at +8.

The four targets are two float blocks, a block of small ints / u16 pairs, and
counts followed by the record array -- the shape of a mesh, stated as a reading
of the shape since none of the blocks is decoded.

Open: the record layout, what the index block indexes, the one object where 0x74
does not land 8 before the array, and whether the 7.3% non-32 gaps are just run
boundaries.
2026-08-26 08:25:29 +00:00
Sylpheed RE agent
af834b7ff4 re: open MCOL -- same container as REGN, same map parameters
MCOL sits beside REGN in hidden/MiscBin.pak, 11 of each, never decoded. Over all
11: POF0 at data_size+16 11/11, bbox pad words 1.0/1.0/0.0 11/11, and
extent == max-min 11/11. So the header prefix is the same shape as REGN's and
the POF0 mechanism applies, which means the chunk+0x10 base and the loader's own
pointer list -- the two things that cracked REGN -- are available here too.

The map parameters are not merely similar but identical in distribution: bboxes
2/6/3 at 250k/50k/25k and the 0x40 triple 2 at 50000 and 9 at 10000, matching
REGN exactly. Eleven maps, each with an MCOL and a REGN over the same volume at
the same cell size. Noted that this matches distributions, not a demonstrated
object-to-object pairing.

Everything past 0x40 diverges from REGN and is open. Also recorded that my own
POF0 delta decoder is wrong here -- eight leading zero deltas -- and that the
working one is regn_decode.py on auto/regn-reader, which should be used rather
than re-derived. Re-deriving it is the mistake I made.
2026-08-26 08:18:17 +00:00
Sylpheed RE agent
da499901b5 re: REGN is excluded as the wave scheduler -- it is a navmesh
The original reason for investigating REGN was that a mission's enemy count
rises and falls, so a scheduler with parameters must exist somewhere, and a
per-map uniform grid is what such a thing would be indexed by.

Now that it is decoded that reasoning is answered: REGN is a tetrahedral
navigation mesh -- vertices, faces carrying plane equations and adjacency,
tetrahedra with portal costs between face pairs, and a grid indexing which tets
fall in each cell. Every section is accounted for by that structure, and there is
no time field, no unit reference and no trigger anywhere in it.

So the wave-scheduler search should treat REGN as excluded rather than unread.
The page's original hedge was right to keep the reading provisional, but the
reasoning it hedged was a guess from shape, and the shape belonged to
pathfinding -- which is what pointed the whole investigation here.

The arrival timetable in Route_S<NN>.tbl, keyframed per squadron per phase with
t in seconds, remains the only located part of the mechanism.
2026-08-26 08:11:36 +00:00
Sylpheed RE agent
6f81ef93cf re: independently confirm the REGN face decode -- 133573/133573 against a 0.69% control
Re-derived the other branch's central check with my own code, my own reading of
the record and my own control, rather than accepting the number.

At base chunk+0x10, with the plane at intra-record +0 and three u16 vertex
indices at +32: every one of 133573 faces has all three named vertices
satisfying its own plane equation. Random-vertex control 2782/400719 = 0.69%.
100% against 0.69% is not a fit.

That also settles the record boundary from my side: the u16s describing a plane
sit after it in the same 48-byte record, so my earlier 'four zeros at the start
of each record' was those integers seen 16 bytes out of position, one record
late.

Two implementations, two independent guesses at the intra-record layout, the
same 100%.
2026-08-26 08:06:30 +00:00
Sylpheed RE agent
214ae8fca2 re: resolved -- the REGN base is chunk+0x10 and my section-0 offsets were 16 bytes early
The other branch supplied concrete offsets: for 3506e972 its face record 0 and
its plane normal both begin at 0x1c700, and my chunk + offset_at_0x78 + 16 gives
0x1c700. Same bytes, different bookkeeping -- so the n.p+d result stands
unchanged and was never in dispute.

The base is chunk+0x10, on evidence with power: the loader does addi r3,r31,16;
at +0x10 the six POF0-relocated slots land exactly on 0x70-0x84, the six section
pointers, whereas at +0 they would relocate the u16 counts and leave two section
pointers unrelocated, which is non-functional; and section-0 record 0 reads as a
bbox corner at +0x10 and garbage at +0.

So my '13467/13467 points inside the bbox' was vacuous. Only 11 of 13467 read as
denormal at the wrong base -- the rest were still plausible coordinates, because
a 16-byte shift inside a packed array of f32 triples yields other floats from
the same array. Recorded the general form: a containment test cannot detect a
shift inside a homogeneous array, because the shifted values come from the same
distribution. For that class of error it is not a weak check, it is no check.

'Section 0 is a point list' happens to be right; the evidence I gave for it was
not evidence.
2026-08-26 08:00:18 +00:00
Sylpheed RE agent
84358e61eb re: record an open conflict with auto/regn-reader over the +0x10 base
That branch decodes REGN as a tetrahedral navmesh with strong checks and claims
the POF0 fixup base is chunk+0x10, so every offset on this page was read 16
bytes early.

I could not reproduce that on the one independently checkable thing here: the
plane list gives 133573/133573 unit normals at the unshifted base and 0/133573
at +16, and n.p+d = 0 holds to float round-off unshifted. A 16-byte shift
destroys it, so the blanket statement does not hold for this record.

Likely reconciliation is bookkeeping: a 48-byte face whose plane fields sit at a
different intra-record offset addresses the same bytes from a different origin.
That is a guess and I am not adopting either wording until checked.

Also confirmed: my own section-0 point test passes at 100% at BOTH bases, so it
never had power to distinguish them and should not be cited as validating the
offsets.
2026-08-26 07:55:12 +00:00
Sylpheed RE agent
67cce0e4c0 re: correct my last two savegame sections -- they analysed the COMPRESSED stream
Both preceding sections split the file at 0x88 and called everything after it
the slot table. Wrong: this page already documents the container as GDHA + a
146-byte header + a zlib stream, and savegame.rs implements it. 0x88 is where
the Z1/zlib payload begins (5a31 = 'Z1', 78da = zlib), so the bytes I treated as
slot fields are deflate output.

Invalidated: the '6 pointer-shaped words in the slot region' are not words and
not pointers, so the false-positive rate I derived from them measured nothing.

Survives with different reasoning: 'the slot region is byte-identical' is true
because the two COMPRESSED streams are identical, which does imply identical
payloads -- and inflating both confirms it, 545 bytes byte-for-byte equal.

Stands and is better founded: all 12 differing words are in the header, which is
the only uncompressed region and therefore the only place a word-wise diff means
anything.

Done correctly, the result is a confirmation rather than a discovery: inflating
all three payloads reproduces the documented clear-ratio field stepping 5 -> 6
on the developed save.

The lesson: I found Z1/78da by inspection and nearly wrote it up as new. It was
already decoded four sections above where I was appending. Reading the whole
page first would have saved two wrong commits, and the wrongness was not
cosmetic -- a pointer census ran over deflate output and produced a
plausible-looking table.
2026-08-26 07:50:29 +00:00
Sylpheed RE agent
91074d1ba7 re: all savegame variation is in the header; slot region identical -- and my heuristic overstates
Splitting the same comparison at the documented slot-table boundary (save+136):
the header holds 34 words and ALL 12 differing words, while the slot table holds
35 words and ZERO differences. The slot region is byte-identical between the two
same-state saves, so the documented slot fields are untouched by the churn and a
slot-region diff is meaningful where a whole-file diff is not.

Also corrects my own framing from the previous commit. The slot region contains
6 words in the pointer value range that do NOT change across runs -- a pointer
captured from a moving heap would not sit still, so those are data whose values
merely land in 0x70-0x8F / 0xB0-0xBF. The value-range test alone does not
identify a pointer; a third of what it flags here is not one. What identifies a
pointer is being pointer-shaped AND varying, ideally with a shared constant
delta. So '26% of the file is heap addresses' overstates it and the defensible
figure is the 8 header words that are both.

Left the earlier section in place with the correction after it: a value-range
classifier on 32-bit words always has a false-positive rate, and quoting its raw
count as a fact about the format is the error.
2026-08-26 07:44:55 +00:00
Sylpheed RE agent
2aa5894212 re: the save embeds live guest pointers, and a three-way diff of the captures is invalid
Comparing the two same-state saves (both 276 bytes), only 12 of 69 u32 words
differ, and nine of those carry guest addresses -- 0xBC/0xBD/0x70 prefixes, the
same regions the runtime work uses. Three differ by exactly 0x101080 and two by
exactly 0x300000: a shared constant offset is what a relocated heap does to a
pointer and not what data does.

The split is sharp: pointer-shaped words are 26% of the file and 44% of them
differ; non-pointer words are 8%. So a quarter of this structure is captured
heap addresses that change run to run regardless of play. That is the concrete
form of the page's existing 'much of the rest is uninitialised memory', now
demonstrated by constant deltas rather than inferred from odd-looking values.

Also records that my first pass was wrong: a byte-wise three-way diff reported
135 of 276 bytes differing, but game03 is 280 bytes with a ZERO-byte common
suffix, so offsets do not correspond past 0x00A. The valid equal-length pair
gives 30 bytes in 12 words. The inflated figure looked plausible -- half a save
changing is what dense state would do -- which is why it is written down.
2026-08-26 07:39:13 +00:00
Sylpheed RE agent
7c1507a466 re: REGN cell payload indexes neither points nor planes -- static search exhausted
The previous test only tried section-1 targets. Closing that gap: the payload's
three index-shaped u32s, followed into the point list and the plane list and
checked for the target lying inside the referencing cell, all sit at the 0.203%
random control.

Two cells read 0.81%, 4x the baseline. I am not treating that as a lead: across
this and the previous iteration roughly twenty such tests have been run, and at
that count a single 4x enrichment on ~8000 trials is what noise looks like.
Calling it a signal would be the multiple-comparisons error a long hypothesis
sweep invites.

So REGN's header, grid, points, planes and cell index are decoded, section 1's
slot regions are censused, and the link between the grid and the geometry is not
reachable by any static test I can construct. The honest next step is the PE
code that reads a REGN object -- the same kind of work that cracked the .slb
packing phase -- rather than a twenty-first correlation.
2026-08-26 07:32:08 +00:00
Sylpheed RE agent
1006523767 re: the REGN cell index does not reference section 1 -- two powered tests
The natural coupling in a file with a uniform grid and a list of small volumes
is that the grid indexes the volumes. Tested by spatial agreement, it does not.

Test 1: every u16 in a cell's 32-byte payload, tried as a section-1 index and
checked for its centre lying inside the referencing cell. Every field sits at
the 0.138% random-control rate.

Test 2: every float triple in the payload, checked for lying inside its own
cell. 0.15-0.81%, also chance.

Recorded a worthless number from the same run rather than dropping it: those
triples lie inside the object's BOUNDING BOX in 100.00% at five different
offsets. The bbox spans the whole 500 km map so any mid-range triple passes, and
overlapping windows at +0 and +4 both scoring 100% is the tell -- a real field
would not survive a four-byte shift. Third time in this investigation that a
containment test against something large has produced a meaningless 100%.

Incidental and real: u32 slots at +0, +8 and +12 are below 0x10000 in 100% of
payload records while +4/+16/+20/+24 are in 11% and +28 never, so the record has
three index-shaped fields and four wide ones.
2026-08-26 07:26:45 +00:00
Sylpheed RE agent
a20c5d56f9 re: refute my own BVH reading of REGN section 1
Last iteration I offered 'position + scalar + integer links is the shape of a
BVH node' as a reading of the shape. Tested properly it fails.

Following every u16 half of every integer slot and checking child-sphere-inside-
parent-sphere gives 0.00% for every candidate -- but the informative number is
the RANDOM control, also 0.00%. No node's sphere contains any other node's
sphere anywhere in the file, so there is no nesting for an index to point at and
the hypothesis dies before the indices matter.

The reason is scale: slot 7 has a median of 3139 against a median inter-node
distance of 45457, 14x smaller, and a random other centre falls within it 0.40%
of the time. It is also smaller than the smallest grid cell on any map.

So slot 7 is a LOCAL scale, not a hierarchy radius. 63410 scattered centres each
with a sub-cell extent is the shape of many small independent volumes, which
would fit per-object collision hulls for asteroids and debris -- a reading, not
a measurement.

What this removes is a wrong frame: the file is not a tree, so tree-shaped tests
will keep returning nothing.
2026-08-26 07:20:39 +00:00
Sylpheed RE agent
488e8599b4 re: REGN section 1 slot census, and an index test with no power
96 bytes is 24 slots. Over all 63410 records: slots 4-6 hold values in the
header bbox range (a position), slot 7 is always positive 519..107600 (a radius
or extent), slots 8-11 are DENORMAL as floats -- 1.4e-45 upward -- so they are
integers a float reader would turn into near-zero garbage, and slots 12-23 are
six pairs with distinct even/odd distributions. Slots 2 and 3 are ~always zero.

Position + positive scalar + integer links is the shape of a BVH node, which
would fit a file carrying a point list and a plane list. That is a reading of
the shape and nothing more.

Recorded a failed test and why it failed: splitting the integer slots into u16
halves and checking them against each section's record count accepts ALL THREE
sections at ~100% for slots 8 and 9. A test that accepts every hypothesis
rejects none -- section 2 has tens of thousands of records, so the check
measures the section's size rather than the field's meaning. Slot 11's halves
are consecutive in 54%, which is suggestive and not a rule.

What would settle it is a test with power: follow a candidate index and check
the target is spatially consistent with the record's own position and radius.
2026-08-26 07:14:39 +00:00
Sylpheed RE agent
eb278c7941 re: REGN sections 0/1/2 have strides, and section 2 is a plane list
The three sections recorded as undecoded are fixed-stride arrays and counts[0..2]
are their record counts: 12, 96 and 48 bytes. Section 1's remainder is exactly 0
in 11/11 objects and section 2's exactly 96 in 11/11, which is what makes these
strides rather than a coincidence of division.

Section 0 is a point list: 13467 of 13467 records lie inside their object's own
header bounding box.

Section 2 is a plane list, 12 f32: four zeros, a unit normal (|n|=1 in
133573/133573), a signed distance, a point inside the bbox (133573/133573), and
a trailing 1.0 (133573/133573). The decisive check is algebraic -- n.p + d must
vanish for a real plane, and over all 133573 records the relative residual has a
median of 2.29e-08 and a maximum of 2.15e-07. That is float round-off, not a fit.

So a REGN object carries a point list and a plane list beside its uniform grid,
which fits collision or region-boundary geometry and sits next to MCOL.

Still open: section 1 (96 B, 60631 records), what queries the planes, the zeros
at [0..3], and the constant 96-byte tail.
2026-08-26 07:07:10 +00:00
Sylpheed RE agent
fd71f37d8f re: RATC +0x10 is a u16 pair too, and no bit is a clean predicate
The u16 at +0x10 is 0 in all 2985 bundles; the content is a 16-bit flag word at
+0x12 with 83 distinct values. Reading it as a u32 inflates the field and hides
that the header is built from u16 pairs -- the same shape +0x0c turned out to
have. All 16 bits are used, from 1.4% to 91.5%.

Cross-tabulated every bit against four properties measurable from the bundle:
multi-element, animated, window-starts-at-zero, 30fps. No bit is close to a
clean predicate. The strongest is bit 10 against window-at-zero, 0.79 vs 0.21 --
a real association but not a rule, and exactly the kind of moderate split that
invites over-reading.

Bit meanings stay open, but four candidate readings are excluded rather than
untried and the field is correctly sized. Every property visible in the file has
now been tried, so assigning meanings likely needs the game observed with
individual bundles loaded.
2026-08-26 06:59:08 +00:00
Sylpheed RE agent
a191569167 re: the RATC +0x0c interval is authored, not a keyframe extent
Tested the alternative I recorded last iteration. Parsing every bundle's
keyframe times (2985/2985 parse), the derived-summary reading -- (high,low) ==
(min,max) keyframe time -- holds in 6 of 2985 (0.2%).

The apparent 34.2% match on 'high == min' is a coincidence of zeros: the minimum
keyframe time is 0 in 96% of bundles and high is 0 in 34.9%, so the 1022
'matches' are exactly the both-zero cases. Worth noting that last iteration I
declined to treat the high==0 share as support -- it turned out to be the
confound rather than the signal.

The interval is also narrow: (low-high)/(max-min) has a median of 0.019, about
2% of the keyframe span. It lies inside the keyframe range in 88.6%, entirely
after in 174 and entirely before in 68.

A short authored window is not the shape of a playback range or a whole-animation
loop region, so those readings weaken too. What it is stays open.
2026-08-26 06:52:44 +00:00
Sylpheed RE agent
1fc68e391d re: RATC +0x0c is two u16s forming an ordered interval, not one number
Read as a u32 it looks meaningless -- 179 distinct values up to 248581842. The
raw values give it away (0x0007000F, 0x000F001A, 0x003C0064): two big-endian
u16s. Over all 2985 bundles, high < low in 2985/2985 with no equal and no
inverted cases, and both are bounded by the animation length at +0x08. Span
runs 1-1200, clustering on 1/10/30/8/20; low equals the animation length in 4%.

A strict ordering holding 2985 times rules out flags or a packed count. Which
interval it is stays yellow -- playback range, loop region and active window all
fit equally.

Recorded the alternative I did NOT test: that (high, low) is simply the min and
max keyframe time, making it a derived summary rather than an authored range.
First step written down. The 34.9% of bundles with high == 0 leans against it
but is not evidence on its own.
2026-08-26 06:46:50 +00:00
Sylpheed RE agent
0800e40c80 re: lead the .slb page with its settled state, not its history
The page is 724 lines of chronological record across 16 sections, several of
them superseded readings kept for their reasoning. A reader arriving fresh had
to read all of it to know what is currently true.

Adds a summary table at the top covering the twelve questions the page answers,
each with its confidence, plus the wave-enumeration recipe and an explicit note
that everything below is history. Also names the four mistakes recorded below so
a reader knows they are deliberate rather than stale.
2026-08-26 06:39:40 +00:00
Sylpheed RE agent
2634632787 re: refute the block-id chain -- 8 of 281 is coincidence
Last iteration I noticed BR09_04's block id names BR10_03, itself the next
exception entry, and flagged it as suggestive but untested. Tested: sorting all
282 by offset, entry[i].field equals entry[i+1]'s own id in 8 of 281 (2.8%),
indistinguishable from chance -- and BR09_04 was one of the eight.

Recorded rather than dropped because it is exactly the kind of pattern that
reads as a discovery when spotted in a sample of one.

The offset from the entry's own id is broadly spread, 7 to 14 and beyond with a
peak at 9-11, so the field is near the entry's id without standing in any fixed
relation to it.
2026-08-26 06:34:59 +00:00