diff --git a/campaign/board.md b/campaign/board.md index 6510cb4..afb55ff 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -171,3 +171,9 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | combat resolver mapped | control-flow | verified | high | 85% | 2026-09-08 | Lane J read all 7,641 bytes. THE RESOLVER DRAWS NOTHING ITSELF - exactly three sites in its subtree, each behind a function whose callerCount is 1 (the resolver), so nothing else in the image can trigger them: R1 NextInt @0x007bb69b (node cannon, once per battle, gated on something having been flung AND a non-empty candidate list) mean 4/3 words; R2 an INLINED NextFloat @0x007a84bd, one per back-engineering candidate per combatant with a non-zero salvage slot, exactly 1 each; R3 NextInt @0x00852ec7, one per successful R2 roll if the project list is non-empty, mean 2^ceil(log2 n)/n. Structure: only EIGHT non-stack stores in the whole function and EXACTLY ONE indirect call (in a _CxxThrowException path) - it composes and posts events and delegates every mutation to its 70 callees. Also: the subtree DOES write the turn-results accumulator at S->+0x2f4[PlyrIdx*0x11c]+0x90 on the surrender arm (a DIFFERENT member from the +0x24 lane K found, and that writer has SEVEN callers); 23 EVENT_* keys in the closure vs lane K's 7, five new, INCLUDING PLAGUE BEING DECIDED INSIDE COMBAT; sizeof(Game::TacReport)=0x94 enumerated twice | | resolver formula is UNTESTED (stated) | verify | backlog | — | 0% | 2026-09-08 | Lane J's honest limit: every encounter in lane Z's workload had res->+0x4 set, so ApplyEncounterResult was a no-op and THE RESOLVER HAS NEVER EXECUTED UNDER AN INSTRUMENT. The formula is untested. combat-resolver.md 10.3 specifies the workload; the first cheap PREDICTION is that a plain fleet battle with no node cannon and no salvage should cost the SAME 18-20 words as a peaceful turn, because the resolver has no unconditional draw | | lane J errors caught before publishing | meta | verified | high | 100% | 2026-09-08 | Two, both corrected pre-publication and both worth keeping: (1) read a callee as straight-line from a truncated Ghidra range, hiding the outer loop that sets R2's count - now rule 17; (2) drafted a 0x11c site as a false positive after converting its address BY HAND, wrongly - re-disassembling from a known boundary showed a real SETurnResults write. THE SCANNER WAS RIGHT AND THE READER WAS NOT | +| inlined-draw inventory COMPLETE | verify | verified | high | 100% | 2026-09-08 | Lane I re-ran the tempering-immediate scan ITSELF at real instruction boundaries (all 41,089 functions decoded TO THE NEXT FUNCTION START, never to Ghidra's size). SIXTEEN functions carry the masks inside a decoded instruction, 67 occurrences - and RECALL IS PROVED COMPLETE: a brute byte scan over the executable sections finds the SAME 67 and ZERO ORPHANS. 70 functions desync mid-decode, every one inside int3 padding, none hiding a site. **LANE J'S 14 WAS WRONG IN TWO WAYS**: one is a FALSE POSITIVE (FUN_008cca30's four bytes are the rel32 displacement of a call, not a temper chain) and FOUR are RNG ENTRY POINTS, not game code - including 0x004f7670 which lane J counted as a game function with an inlined draw and which is actually Mars::RNG::NextUInt, a primitive. CORRECTED FIGURE: **eleven game functions, 28 sites**. Only TWO are reachable from the turn drivers by direct edges: FUN_007aa240 (StrategyServer::ProcessTurn, depth 4) and FUN_007a7f30 (OnAllCombatDone_Tail depth 3 / CombatResolver_Run depth 1). The nine others top out at vtable slots with no direct caller - and lane I states AS LOUDLY AS THE RESULT that "not in the closure" is proved FOR DIRECT EDGES ONLY | +| Mars::RNG::NextUInt read completely | objects | verified | high | 100% | 2026-09-08 | 0x004f7670, 84 bytes, EVERY INSTRUCTION READ: `if (left==0) Twist(&mt); y=*next++; --left; temper(y); return y`. EXACTLY ONE WORD, unconditional, no loop, no branch but the lazy twist, no scaling with game state. Ghidra's 84 is CORRECT here (body ends 0x004f76c3, 12 int3 to 0x004f76d0) - worth stating, since rule 17 is about not TRUSTING the size, not about it always being wrong. CONVENTION TRAP: ECX is the RNG OBJECT, not &mt - it does `lea esi,[ecx+4]` itself, where NextFloat/NextInt are entered at object+4, and ProbabilisticJump uses BOTH conventions 0x6b bytes apart | +| EncounterDetect_AssignContacts + "a doomed roll still moves the generator" | verify | verified | high | 95% | 2026-09-08 | FUN_007aa240 (Ghidra says 944; REAL BODY IS 953 - Ghidra's end lands inside a `push` at 0x007aa5ee, real end 0x007aa5f9; nine hidden bytes, only a throw stub, no draw-count change, but the range is wrong). Draws ONE INLINED NextFloat PER (contact, detector) TRIAL, BEFORE THE ACCEPT TEST. Threshold 0.25f if the detector's TechTree has tech 0x2728/0x2729 else 0.0f; accept iff thresh >= r, derived from fcompp/test ah,5/jp - EQUALITY ACCEPTS. words = sum over contacts of min(trials-to-first-accept, |detectors|); with no detector teched that is |contacts| x |detectors| EXACTLY and NOTHING IS EVER ASSIGNED - **a roll that can never succeed still advances the generator**. The outer repeat-until-no-progress back-edge (which a truncated dump HIDES) cannot redraw a pair: the tried-bitset is filled ABOVE the back-edge target and never cleared, so at most two passes | +| RNG entry points: SEVEN, not three | objects | verified | high | 100% | 2026-09-08 | Lane I: NextFloat, NextInt, Chance, plus NextUInt, FloatRange 0x0047d8a0 (1 word, NARROWS TWICE, in ProcessTurn's closure at depth 3 with two call sites), IntRangeBell 0x008e6d80 (triangular, >=2 words) and GaussianRange 0x008e6e30 (2 words PER ATTEMPT, UNBOUNDED, both draws inlined). AND IT SCALES BY 2^-32 WHERE NextFloat SCALES BY 1/(2^32-1) - TWO DIVISORS IN ONE IMAGE. GaussianRange documented but deliberately NOT modelled | +| RNG residual: 18-20 words STILL UNEXPLAINED | verify | backlog | — | 0% | 2026-09-08 | THE HONEST HEADLINE. Lane J predicted the two inlined functions would explain lane Z's per-turn gap. **NOT CONFIRMED.** Both new sources are gated and neither has been measured: 0x007aa240 contributes 0..(|contacts|x|detectors|) and whether its +0xfc gate passed is not knowable statically; ProbabilisticJump's second word is 0 unless a type-5 waypoint fails its arrival test. RESIDUAL: 18-20 words, essentially ALL of it. WHAT IS NOW PROVABLE IS THE NEGATIVE: there is NO TWENTY-THIRD MECHANISM - the complete draw-site inventory of the ProcessTurn closure (1,426 functions) is 22 sites (21 entry-point calls + 1 inlined), so the 18-20 words are distributed among exactly those. THE SEARCH SPACE CLOSES; THE COUNT DOES NOT. One extra bracket on EncounterDetect_ProcessTeamRecord with |contacts|/|detectors| in the argument record turns the formula into a one-turn test | +| CORRECTION: no Mars MT19937 variant | objects | verified | high | 100% | 2026-09-08 | Lane I correcting ITSELF (rule 11). It had written into Ghidra that 0xff3a58ad/0xffffdf8c are a Mars variant of MT19937. THEY ARE NOT - they are the textbook masks applied BEFORE the shift: (y & 0xff3a58ad) << 7 == (y << 7) & 0x9d2c5680, verified over 200k words. `mars::rng` in sots-engine was NEVER WRONG. Corrected in place in Ghidra and in the fragment. NOTE FOR FUTURE SCANS: a scan for the TEXTBOOK constants finds NOTHING in this image, which is exactly why rule 16's scan must use these pre-shift values |