az-turn23-tarka-comraid.sav is 89,285 B not 73,385 (that number was the
turn-11 file's). Both saves are now on the guest as well as in the repo.
az-turn11-16destroyers.sav is the same game with the 16-destroyer fleet
still alive and Commerce Raiding already in hand - it saves ten turns of
play for anyone retrying the player's own raider, though the AI's first
routes do not appear until turn 16.
Static half, verified and correcting two published sentences:
FUN_00820af0 short-circuits on the ROUTE OWNER being Hiver (0x00820bc2),
on the RAIDER's crew species being Hiver (0x00820bd7), and only then on
SpeciesDef+0x144 (Human, Zuul). The campaign's 'Human or Zuul draws
nothing, every other species draws' is incomplete on both sides. And the
short-circuit sets frac = 0.0, not 1.0 - corrected in ag.json in place.
Live half NOT taken. hooks=off throughout; no instrument was armed. The
workload was built to within one conjunct: az-turn23-tarka-comraid.sav is
the corpus's first Tarka empire, has Commerce Raiding, three AI routes and
five AI Tarka fleets parked on sector nodes with Pos bit-equal, two of
which clear G0-G4. The failed conjunct is G_B3 - the qualifying raiders
own every route, so relation-to-self is 3 and B empties its candidate
list. The next workload is named from that: give player 0 a route sourced
in sector 768, where the AI raider already stands.
tscr reads 127 on this map, not 252/253 - the mask is not a corpus
constant and my own P3 gave the right count for the wrong reason.
BP has a per-PROCESS input (two pinned processes disagree); this lane has a
per-HISTORY input (two load processes agree with each other and disagree
with the continuation). One datum against the ASLR reading as stated: the
exe base was identical in all five VM145 processes (0x00f40000, delta
+11796480), so any image-base hypothesis has to name which allocation it
means. Probe 3's ledger taken twice on ar-oracle-A-pre - once by
continuation, once by load - now speaks to both.
Both committed predictions are wrong, in different and useful ways.
1b FALSIFIED: ar-oracle-A-pre.sav + load + one End Turn gives 4c356f59...,
not 7a8b3d5e... - and gives it byte-for-byte in two fresh hooks=off
processes. 23 leaves apart from AR's continuation, 17 of them one AI fleet
(Zeta Fleet V moved in the continuation and stayed put after a load). So
AR's second hash may NOT enter determinism-hashes.txt as a load->turn pair;
the load form is certified instead. The resave canonicalisation is
exonerated: the pre-turn round trip differs in exactly one leaf,
/CD[1]/NPrvVa, +5.00 per load - which also corrects determinism-oracle.md's
"nothing else varies".
Probe 1: the two files DIFFER, and /Sim/RNG, /Sim/trdmgr and every player-0
fleet are identical as predicted - but ZERO TechTree and ZERO ResTNm leaves
differ. Both processes picked BIO_GrvAdpt. The 144 leaves are a combat at
Venkman: Auto Resolve Peacefully means the human commits no ships, not that
no combat occurs, and the planet's defences killed two raiders in one
process and one in the other.
The producer run (config already on the guest, no build) names why the pick
agreed: producers A and B both returned null, Player[32] reached the
candidate walk, and candidatesTried=1. The outcome set is a singleton by
COUNT, not by producer gate - so ResTNm == '' is necessary for research
exposure, not sufficient.
VM145 restored byte-for-byte and released.
shim.cfg back to hooks=off, binkw32.dll back to the pre-BP file, both autosaves
restored byte-identical, game process left not running as found.
ad-turn27-two-raiders.sav is deliberately left on disk, so the Load-dialog rows
have moved on this guest.
P1 falsified. Two processes with identical pinned AI client seeds produced
different turn-28 autosaves from ad-turn27-two-raiders.sav, so a per-process
input other than the seed reaches a rich turn and C-exact is false on this
state. Resolution trigger, handed back rather than chased.
But the pin did almost all the work. AD's unpinned pair differed in 94 leaves
across designs, ship records, money, both build queues, ModCount, NumFlts,
DesignIDs[] and FleetIDs[]. This pinned pair differs in 35 of 61147, and all 35
are one transposition: fleets 1970 and 1986 exchange their entire contents.
Every decision leaf is identical.
The command block says the same from the input side. Runs L (unpinned) and A
(pinned to L's observed seeds) wrote BYTE-IDENTICAL autosaves, which gives a
noise mask no previous lane could build for this state; against it, A and B
differ in exactly three words - one fleet-move fleetId and two systemIds - and
list 10's fleet ids are in the same order in both. The id counter is
deterministic; what varies is which newly formed ship group each id lands on.
Also measured: the save has seven non-human players and the engine builds THREE
AI clients (RunAI fires 3x, ids 32/496/512); the four NPC factions get no client
and no seed.
Probe 2 of the 2026-09-09 resolution: pin the AI client seeds on
ad-turn27-two-raiders.sav -- the richest AI turn the campaign owns, and the one
whose hooks=off control varies in 94 leaves -- and run two fresh processes.
Committed prediction: the two autosaves are byte-identical and state_checksum
prints IDENTICAL over ~67k leaves. If they differ, reading 3 is alive on rich
states, C-exact is false on the turns that matter, and that is a resolution
trigger rather than a lane result.
Also records, before any run: the exposure facts for all eight players read off
the input save, and the observation that four AI players enter this turn with
ResTNm == '' while contributing none of AD's 94 leaves -- all four have
NumOwn == 0, so 'ResTNm == emptystring names a pick turn' is necessary and not
sufficient.
Lane BQ (VM145) takes the seed-exposure predicate's first forward test.
Part 1b: ar-oracle-A-pre.sav + one End Turn from a LOAD must reproduce
7a8b3d5e... (it has only ever been reached by continuation). Part 1:
ar-oracle-A-post.sav in two fresh hooks=off processes must DIFFER, confined
to Player[32]'s research leaves, because that save carries ResTNm == '' for
the one player that can research.
Also records the exposure facts (ResTNm / NumDes / NumOwn / completions) for
every player on both inputs, and names a false-positive class in the predicate
before the run: four NPC players carry ResTNm == '' on the very turn whose
control agreed, and all four have NumOwn == 0.
The brief's gate ('Human or Zuul draws nothing, every other species draws')
is incomplete in two places, found by reading FUN_00820af0 before touching a
guest: species 1 (Hiver) short-circuits too, and the ROUTE OWNER's species is
a gate of its own. The species enum is read from the InitTable jump table as
bytes: 0 Human, 1 Hiver, 2 Tarkas, 3 Liir, 4 _NPC, 5 Zuul, 6 Morrigi.
Corpus counted against the predicate (rule 28 practice 5): 0 of 30. 24 saves
hold a Tarka AI - a drawing species - but tscr is 253 in every one, so the AI
has no Commerce Raiding and never rolls. The workload has to be built.
Tarkas chosen: the tech data gives it a 100% path to CCC_ComRaid via
CCC_FTLEcon, where Hiver has 0% on both edges.
Decides lane AG's section 3.3 final paragraph, which lane AD correctly reported
it could not separate on sector 832.
STATIC: FUN_00841700 is the writer AG's section 7 lists as not found. It resizes
the +0x0c vector to the length of the systems vector -- the container trfr indexes
-- and fills it with containingSector[system.Idx] = sector. It runs from the
deserialiser, so a loaded save has it populated. G_B1a cannot reject a valid trfr.
LIVE: four raiders parked (2 CR on sector 832, 2 DE on sector 816), probes=8.
Slot13RngCalleeA entered 4x/turn at 4 words every turn; on turn 42 the pick fired
at 0x0088b613, calls=1 words=1, and the tail cost 5 words against 4 elsewhere.
The same run carries its own control: B entered on 832 twice at 0 words.
B costs ONE word, not two: FUN_00820af0's third short-circuit is a flag word
InitTable sets only for Human and Zuul, so AG's committed 3-words-on-success is 2.
Two hooks=off processes gave byte-identical autosaves, and so did a probes=8
process -- an oracle pair and a whole-save byte-neutrality proof for the
instrument, both firsts on this lineage.
Also first firings of 0x008939ee (rule 20's case study) and 0x00820c1b.
Includes the static decode of FUN_00841700 -- the writer of TradeManager+0x0c that
lane AG's section 7 lists as not found -- stated as a falsifiable prediction (P0)
with the symptom of each way it could be wrong. Also corrects the route count in
ad-turn27-two-raiders.sav: five rt records, trfr 18/16/14/20/14, not three.
One row per statically reachable draw site in BeginProcessTurn u ProcessTurn u
OnAllCombatDone_Tail, each carrying a measured firing or a decoded predicate on
save fields plus the 22-save corpus count. No total is reported: 31 rows (30
distinct sites plus one grouped SVSO row) are unread gates, and that visible
blank column is the deliverable.
Three gates decoded:
0x0082cdb8 = the addiction spread roll (Chance ADDICTION_SPREAD_ODDS 0.2f);
corpus 0 of 22 -- nadct is 0 on all 28 systems of all 22 saves. Writer found:
ServerSystem_BeginAddiction has exactly two callers, both inside vslot15, so
an INTER-EMPIRE route plus a species addict tech must seed it first.
0x0088dc43 = the spy counter-mission roll, p = cmo accumulating 0.2f per turn,
so Chance costs 0 words from the fifth turn on; corpus 0 of 22 by deat/cm.
0x0088b613 = the raid target pick. RNG_NextInt has no zero-bound early-out, so
a single-candidate list still draws -- which proves lane AC's B returned
before the draw. Failed conjunct named: the raider sat on sector 832 while
all four rt records source in 816/816/768/752. Next workload: sector 816.
Six draw sites no firing-indexed table could hold: five behind
CreateRaidEncounter's empty-candidate-list gate, and 0x00820c1b again from
Slot13RngCalleeB -- so B costs 2 words on a success, not 0.
SVSO generator closed: all 17 drawing overrides use GetGame()->+0x16c.
Rule 17 again: ServerSpyManager_vslot14 really ends at 0x0088dd2b, not 0x0088dd03.
TradeManager::Slot13RngCalleeA, entered 0 times in every measurement any lane
had taken, is entered 1x/turn once a player fleet is parked on a trade-sector
node, and OnAllCombatDone_Tail's RNG word delta goes 0 -> 1. Lane W3's open
risk is realised, not retired: the RNG ledger and the standalone's generator
model need extending by one draw site.
The gate is neither freighters nor a deployed spy. It is a fleet whose LocID
resolves to a node of kind 2 (TradeSector), positioned bit-equal to the
sector, whose owner's bit is set in the sector's tscr mask -- which the
CCC_ComRaid tech sets, taking tscr from 252 to 253 and setting ServerPlayer
CnRad. Before the tech the game refuses in as many words: 'Alpha Fleet cannot
raid trade sector.'
Control: same guest, same build, same shim.cfg.l3probe, one End Turn on lane
L3's turn-15 save -> all three inner callees 0, tail delta 0. Guest adoption:
ref-turn2 hooks=off reproduces the published determinism oracle exactly.
Corrects lane L3 §3.5 (the turn-15 save already had a freighter in service --
the AI's -- so 'freighters' was falsified before this lane started) and lane
V2's 'trade slot 13 is draw-free'. The spy half is decoded but NOT reached:
no AI system in that game has an asteroid belt, so a deploy was impossible,
and spies2 is therefore still untested and stays a rule-6 hypothesis.
Two new corpus saves; expect the coverage ratchet to break on CnRad,
tscr=253, three rt records and a fleet whose LocID is a TradeSector.
Two results from the same VM session, both of which needed a workload the
corpus cannot supply.
CDiff, played forward. ref-turn2 loaded and 49 End Turns driven through the UI
helper to frame 51, with the entry hook emitting one record per turn:
50 calls, frames 2..51, EXACTLY TWO STORES
frame 2 -1 -> 0
frame 50 0 -> 1 <- the modelled tier transition, at the modelled frame
and 47 turns between them on which the writer ran and wrote NOTHING
predict_path was computed at entry from the transcribed threshold table on all
fifty turns and agreed with the cdiff region on every one. Frame >= 100 is still
a code read and is not claimed as a measurement.
The AI seed probe (asked for by the coordinator, ranked above CDiff). Hooks on
Mars::RNG::Seed and StrategyApp::RunAI, two launches from turn1-state, load
only. Every AI client seed differs between processes -- net 32, 496 and 512 all
move -- while the record structure is identical and one Seed call with seed=0
produces a byte-identical state in both runs.
So the turn1-state -> turn2 nondeterminism is a SEED effect, not the ordering
effect predicted, and the 'one of three varies' observation is explained by two
of the three empires having a research pick that is robust to the stream. This
falsifies lane AI1's 'every draw from the static generator returns 0', which
that lane had explicitly flagged as arithmetic rather than measurement.
Where the seed comes from is NOT established; the finding names the one hook
that would settle it and the six values it must reproduce.
VM140 left as found: the 8-file save set with its oracle bytes intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
Lane SV recovered the script-object subsystem statically and predicted that
SVSOSwarmQueen::RegisterHives takes one strategic-generator word per new hive
inside StrategyServer::BeginProcessTurn, which runs inside lane Z's autosave
bracket and outside both turn drivers. Nothing had ever been hooked in that
interval. Measured on VM140, and it is right.
turn 1 -> 2 (hives created) BeginProcessTurn 2 ProcessTurn 20 residual 2
turn 2 -> 3 (hives exist) BeginProcessTurn 0 ProcessTurn 19 residual 0
Two instruments that share no code path agree: the region ledger reads
170 -> 172 across RegisterHives, and the return-address draw-site table gains a
row at 0x00527714 (NextInt, 2 calls / 2 words) which is absent on the next turn.
The draws produce NextQ 30/28, TickHives' slip takes them to 31/29 -- exactly
turn2-state.sav -- and the next turn's slip gives 32/30, exactly turn3-state.sav.
So tail-rng-ledger.md's "all of it inside ProcessTurn, residual exactly zero" is
correct for every turn it measured and false as a statement about the code. The
interval a standalone must reproduce starts at BeginProcessTurn.
Also closed or corrected:
* LO/HI read live rather than fitted: NextQ = frame + 20 + NextInt(10),
inclusive; the TickHives gates are 10 (a frame floor), 5 and 3, so no queen can
spawn before frame 11 on any save.
* CDiff's threshold scan re-read independently: the tier can only ever be 0 or 1,
and "entered and stored nothing" was observed live and distinguished from
"did not run" (method rule 20).
* SnLv measured for the first time: Spica reads AFlags 0 with SnLv 0x200, so the
sensor branch is what refreshes it -- nvo-tshn-visible-owner.md §6 goes [H] to
[V]. ComputeContactLevel's documented "else 1" names one of four return tails;
the observed non-visible level is 2.
* Rule 19: the oracle reproduced byte for byte with all five new detours live,
and again with four watchpoints armed on top.
* turn1-state -> turn2 is not deterministic (lane L5 owns this). Two more runs
here make it four distinct outcomes over six runs, including two DIFFERENT
hooks=off results, and the strategic generator is provably not what varies.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
TShn is refreshed to Frame for every (system, player) that satisfies ServerSystem::IsKnownTo
0x00746390 -- IsVisibleTo OR (2-bit sensor contact at ServerSystem+0x24 AND CCC_AdvSens). Not
AFlags alone, which is why lane E3 could not fit it. The writer chain was confirmed live, frame by
frame, from a hardware watchpoint on Spica's NVO node; the refresh runs twice per End Turn (driver
phase 24 and combat-done phase 25).
Also: the trade and spy containers read out of a live game for the first time (both empty, zero
traps -- the workload confirmation two lanes lacked); Player.Status's predicate named as
ReqCL != 0 && Elim == 0, with Species != 4 shown to be a corpus coincidence; and all ten command
ModCount handlers named, two of them only reachable on a turn-1 workload.
Corrects objects/layouts.md's ServerSystem PID@0x274 row and closes system-visibility-record.md 7.
Runs: build w3tshn-bb81d3d-20260908T2007Z, configs shim.cfg.w3tshn / w3mod / w3control.
Rule 19 control passed -- the armed run reproduced the determinism oracle byte for byte.
TShn: the brief's address was wrong -- player+0x274 is observed_techs; the TShn map is
ServerSystem+0x274. The real obstacle is that a std::map's nodes are heap-allocated, so there is
no fixed leaf to arm; arm the map header to trap the insertion instead.
rcex: closed from the corpus, no VM time needed (see rcex-explained.md).
Trade/spy workload: not attempted, and lane H's reasons still hold. What is new is that the
watchpoint module is the instrument for confirming the workload BEFORE spending a turn on it --
both containers are one add from the arming point, and for the spy vector the trapped return
addresses would name the UI nobody has found.
Multiplayer: two clients on one guest, joined by typed IP and by LAN browse, launched a
2-player game and played two turns in lockstep. 44,319 packets captured outside the guest,
ZERO to any GameSpy port. Availability check fails open exactly as lane G2 read it; Join
Manually is enabled with every gamespy.com name dead, falsifying G2's own caveat (a). The
shipped MOTD is fetched live from kerberos-productions.com and tells players to host in LAN
mode -- the developer's own statement of the same finding.
Watchpoints: a new shim module arms DR0-DR3 from StrategyServer::ApplyAllTurnCommands.
ModCount takes exactly 12 writes per End Turn (lane A2's prediction, confirmed twice, both
predicted addresses exact); Frame takes exactly one, from BeginProcessTurn+0x2a, settling the
ModCount-vs-Frame naming in A2's favour. The Player.Status writer between tail phase 31 and
the autosave EXISTS and is StrategyNetworkClient::OnMessage+0xa15 -- correcting lane T2.
Rule 19 control: the armed run reproduced the determinism oracle byte for byte.
rcex: closed from the corpus alone. It is sixteen 4-bit per-player counters; nibble p is set
to 1 on the turn the system enters player p's AFlags and ticked to 0 the next turn. 7/7
across two different games.
- determinism oracle regenerated and byte-identical (bb4fd9ac / 978041ac)
- the four phase-23/33 draw-bearing tail callees run EVERY turn; the three inner
functions holding the draws run zero times -- the gate is inside each outer body
- CreateRaidEncounter is called (2 on one turn) and draws nothing: candidate list empty
- Zuul: 7 calls / 7 words per trade-raid Chance site, 14 not 16, as predicted
- EncounterDetect_Run receives an EMPTY record vector, so ProcessTeamRecord and
AssignContacts never run; the 2-word detection residual is in 0x007d5150's subtree
- a MinHook detour on 0x00893290 changes the game's output; bisected over six runs.
The un-instrumented game and lane Z's instrument agree, so lane Z's numbers stand
- lane AI1 insert: P2 held across two fresh processes, Rung B stays as written
Spine phase 4, byte for byte at 0x007dc871-0x007dc8c7. Three separate stores to
the same word -- clear, OR the self bit, then conditionally OR the alliance
mask -- which is what makes the alliance term an OR and is invisible in any
summary of the phase. The bit is the player's POSITION IN THE VECTOR.
Checked against the almem bytes the game archived: 72 of 80 player-records
agree, and the other 8 are predicted by the same model (FinalizeTurnRecords also
runs on load, and the load path does not run the spine, so every save's earliest
archived turn carries a zero mask -- true on all 11).
Reported as loudly: the corpus cannot separate `1 << vectorIndex` from
`1 << PlyrIdx` (0 of 80 records differ), cannot separate the OR from a plain
assignment (every observed alliance mask already contains its member's own bit),
and cannot separate the ALid guard from an AL != 0 guard. Those three are
instruction-stream readings only.
ModCount, named and enumerated. StrategyServer::Write tags both words itself, so
S+0x8 is the wire's ModCount and S+0xc is Frame -- addresses.json has the name
on the wrong word, and turn-driver.md's "they stay in lockstep" is corrected in
place. RTTI gives the reason for the two bases: Game::StrategySim is a base
sub-object at offset 4.
29 writer sites: 20 command handlers plus 6 inlined in the command-batch applier
(each an unconditional bump on ENTRY, before validation), the two turn drivers,
and the abandon/chaos check -- which is gated on Abdn, false on all 28 systems
of all 11 saves. So the per-turn delta is 2 + one per command applied out of
every player's TurnCommands block; the 0x1b4 container stride independently
confirms lane Q's block layout. It is not derivable from the pre-turn save.
The residual is a watchpoint, specified with its own written prediction (exactly
12 hits on turn1-state) and four falsifiers. Direct-call reachability is stated
as the lower bound it is, per lane V2's indirect-edge measurement.
Engine side: sots-engine wip/alliance 5e409cf.
Every call-graph result in this repo was computed over direct (E8) edges.
5,045 of the 5,207 functions named by a vftable slot have zero direct call
sites, so all of those results were lower bounds. Lane Z's dominant RNG
consumer hung off exactly such an edge.
tools/vtable_map.py builds, from the RTTI walk plus a full sweep to the next
function start (rule 17):
* vftable -> class -> sub-object offset -> slot -> target, and its inverse
* the class hierarchy from the RTTI base lists, so an abstract interface
with one concrete override resolves uniquely
* constructor-derived member typing (ctor result -> [this+d])
* the slot index at every indirect call site, with a backward register
resolver that refuses to cross a branch target rather than guess
* `this`-carrier spans and this/member call-graph propagation of class
Validation (12/12): rediscovers ServerTradeManagerImpl slot 10 ->
GenerateTradeRaidEncounters from the dispatch at 0x007d8469 with nothing
hand-fed, and re-derives the *Impl rule for both managers. Receiver-class
pinning reaches only 2.5% of the 6,398 virtual sites, at 0.6% out-of-range
against a 70% chance baseline; the displacement-only route measured worse
than random (81% vs 58%) and is rejected outright.
Closes lane K's tier-4 blind spot: all nine phase-23 calls and both phase-33
calls named. Four of the eleven reach a draw on the strategic generator
(StrategyServer+0x16c) at eight instruction-verified sites, none ever
observed firing — so "the tail draws nothing" is a property of eight turns,
not of the code. Also resolves the nine parked inlined-draw functions to
their vtable roots (correcting how that was recorded: none is itself in a
vftable; their topmost ancestors are), and finds 14,958 inter-function tail
jump edges without which three of them look like dead code.
saved-state writer under combat
FUN_007d5a00's six callees, read from the instruction stream to the next
function start, plus 18 helpers. Lane J named this "the real per-phase combat
pipeline"; the identification of it as the most tractable target was right and
the characterisation was not. It is one subsystem -- destinations, group,
classify, split, execute, destroy -- and it is StarFleet retreat.
Seven saved fields or containers move on this path: ship->fleet membership,
StarFleet objects created and destroyed in the master list, the system fleet
lists, fleet position/location/flight plan/flags, the ServerSystem presence and
explored masks, the per-player PlayerView, and ServerPlayer.GTraf.
Closes struct-recovery.md section 7's open "OID allocation (R2's x16)": the
allocator is at 0x008b8ae0, the counter lives per network node at
IDMap->+0x08 + node*0x14 + 0x10, it is pre-incremented and never issues 0, and
an id is (counter << 4) | (node & 0xF). R2's x16 is the shl 4. A partial
retreat mints one id per group, and that counter is almost certainly the save's
NMnx tag -- labelled hypothesis, with a falsifiable prediction written down
before any run.
New rule: retreating from a system you had not explored marks it explored for
you, writing EFlags and refreshing the PlayerView. New diplomacy rule: a system
whose owner captured it on the current turn loses that owner's ceasefire cover
in the hostile-presence test.
RNG recomputed independently from a different root and with a different tool:
327 functions in the closure once E9 tail-call thunks are followed (the first
pass missed an 8-byte jmp thunk and with it the whole id-allocator chain), zero
calls to the four primitives, zero inlined MT tempering immediates, and one
false-positive 0x11c stride that is StarFleet.FtMS being initialised to -1.
Bound, not proof: 152 indirect call sites in 91 of the 327, four of them on the
main line and flagged for the vtable lane.
Corrects my own first reading of the fleet-creation call: a ret-N tail-call
thunk in the middle of an argument list makes three pushes look like they
belong to the wrong call. Checking ret N against the push count catches it.
The honest limit stands: none of this has ever executed under an instrument.
combat-resolver.md section 10.3's workload now needs a second condition -- the
battle must produce a retreat, or all six phases are no-ops.
The standalone now models the turn's dominant generator cost -- 16 of a measured 18-22
words -- and lands 4 and 2 short of the two calibrated pairs, which is exactly the
per-call-site ledger's split for those turns. The state block is byte-identical; only
left differs. The answer to 'does it match the oracle' is no, by a stated amount, and
tools/rng_oracle_check.py is the instrument that says so.
The tail's last phase is modelled for the six turn-record fields recoverable from the
wire and checked against the record the game itself archived: 480 fields over 80
player-records, 0 mismatches. It stays blocked; --commit-blocked shows exactly which
five fields are missing and what they cost.
By-product, and probably worth more than the phase: the stored bankruptcy elimination
limit is injective in the maximum-income sum it is built from, so every save states the
per-system output term that blocks ComputeBudget. tools/max_income_oracle.py inverts it
-- 25 player-records over the corpus -- and recovers the protection factor as 3.3 from
the saves rather than from the data files. It also shows the engine's -0.15 divisor
disagrees with the game on 6 of those 25.
divergence unchanged: 209->204 and 108->103, 5 closed / 0 regressed on both pairs.
FUN_00893290 is ServerTradeManager::GenerateTradeRaidEncounters --
ServerTradeManagerImpl vftable slot 10 -- rolling Chance(TRADE_RAID_ODDS_PLAYER
= 0.2) and Chance(TRADE_RAID_ODDS_NPC = 0.05) once per player. Both are
strictly inside (0,1) so each is exactly one word, and no back-edge contains
either site, so one word per player per site is a hard bound.
Why no sweep found it: zero direct calls to it exist in the image and its only
reference is a vtable slot. The dispatch is a "call edx" through slot 10 at
0x007d8469 inside DetectEncounters -- one instruction before the DIRECT call
that lane I's closure did follow. Lane I's inventory is not wrong; its stated
caveat about indirect edges was load-bearing, and this is what it was hiding.
strategic-turn-internals.md line 153 had already named 0x00893290 "raid
encounter generation" against these exact StrategyVars. What was missing was
that it is where a turn's RNG goes.
Ghidra's size is wrong again: real body 1546 bytes ending 0x0089389a, reported
1532, ending mid-instruction. Rule 17, third time.
Seven entry points detoured, each call keyed by __builtin_return_address(0)
with the word cost from left before/after. Three consecutive turns on
ref-turn2: site sums 19/18/20 against independently measured ProcessTurn totals
of 19/18/20, residual 0 every time. The 18-20 spread is now explained rather
than reported -- it is the two gated research draws.
The dominant consumer is FUN_00893290: two Chance calls per player across all
eight player-vector entries, 16 of every turn's 18-20 words, and it is NOT one
of lane I's 22 sites. Lane I said its closure covered direct edges only and
that indirect reachability was unsettled; this is that gap, measured. The
function is unidentified and is the highest-value target left.
Two bookkeeping corrections are in the report tool, not the shim: helper-
internal rows (Chance's own NextFloat) double-count, and 8 calls per turn are
on the StrategyClient's generator, not the strategic one. The first build did
not distinguish generators and reported 44 words against a bracket of 18 --
which is what caught it. A per-site ledger that cannot say which generator a
draw came from is not a ledger.
tools/standalone_report.py drives sots-engine's sots_turn over each
consecutive-turn save pair and diffs the result against the game's own
post-turn save with state_checksum.py, which localises to named leaves and
proves its own coverage by re-serialisation.
turn1-state -> turn2-state baseline 209 diverging, after 204, closed 5
turn2-state -> turn3-state baseline 108 diverging, after 103, closed 5
regressed 0 on both
`regressed` is reported next to `closed` and never netted off. It earned its
place immediately: committing the phase-31 player-status restore turned two
agreeing leaves into disagreeing ones, because the phase writes 1 and the file
carries 4.
The stable-system stand-in feeding the colony pass is a labelled hypothesis and
it survived a changed workload -- the same 3 ntdev leaves closed on both pairs,
six agreements, zero disagreements.
Two things deliberately NOT implemented: the TShn/ltis counters (18 leaves, a
`+1` would close them, but "+1 across one observed turn" is a hypothesis, not a
reading), and the RNG state write-back (an advanced-but-incomplete generator is
wrong in a different way from an untouched one).
dashboard.py gains section 6, reading verify/results/standalone/status.json:
phases modelled/committed per driver, baseline vs after, closed vs regressed,
the subsystem breakdown of what still differs, and the RNG gap. Sections 6-8
renumbered to 7-9; the delta footer tracks the two new counts.
DASHBOARD_README.md documents every number.
findings/control-flow/standalone-scaffold.md has the ranked blocker list.
Predicted at turn 34 with the run in flight, from min_life falling by exactly
1 per turn: the first phase-11 draw on turn 64, exactly 1 word, tail total 1,
bracket = ProcessTurn + 1. The game was played to turn 64 and every clause
held. predict_words, computed before the original ran, said 1 and the
measurement said 1 -- a real check of the model, against 63 preceding turns
where 0 matched 0 and checked nothing.
So the defect lane K warned about is no longer latent: on that turn a
reimplementation modelling ProcessTurn perfectly would have written an autosave
one word out of step.
And the instrument's thinnest part ran live on the same turn -- ProcessTurn
crossed a block boundary (left 11 -> 615, one twist, 20 words) and the bracket
still reconciled to residual 0.
A Von Neumann encounter at Gallandro on turn 54 gave the workload the finding
said did not exist. Auto-resolved, with P10 committed before the click.
P10 predicted a non-zero tail cost and was wrong: res_no_battle flipped to 0
for the first time in 55 turns, the fleet was destroyed, and the generator
moved by zero. The bracket residual stayed 0, so combat proper drew nothing
either -- all 22 words were inside ProcessTurn, exactly as on a peaceful turn.
That is the strong form of lane J's static reading, and it means a
reimplementation can model a turn's RNG while modelling nothing about combat.
One auto-resolved encounter against an NPC is not combat in general, and 10.2
says so at length.
The primitive count went three -> four (this lane) -> seven (lane I) while this
measurement was being taken, and not one number in the ledger moved. That is
the argument for reading state instead of counting calls, stated where it is
now demonstrable rather than merely asserted.
StrategyServer::Write tags both words itself: S+0x8 is ModCount and S+0xc is
Frame. addresses.json has the name on the wrong word and lane T's
PhaseCounter is the one the wire calls ModCount. The saves confirm it
independently -- ModCount 0/12/24 across turn1/2/3-state, 241/412 across Zuul
16/23 -- and those deltas are exactly the 12-44 per turn measured live. So the
'writer nobody has identified' question dissolves: it is a modification
counter, it scales with the empire, and there is no single writer to find.
The players=8 flag is withdrawn. The offset is right, pinned by the ctor's
four-vector enumeration at 0x0085b120 with no frame arithmetic needed, and the
count is right: the vector is empires + one rebel-AI per empire species + four
NPC pseudo-players, so 8 on the Human saves and 7 on the Zuul ones against a
lobby that says 2 in both. My draft claimed the hook read 8 on both saves. It
read 7 on the Zuul one. I generalised from one run without re-reading the
other, and a check aimed at something else caught it.
Re-ran lane J's image-wide tempering-immediate scan at real instruction
boundaries and audited it site by site. Sixteen functions carry the two masks
inside a decoded instruction, 67 occurrences, and a brute byte scan finds zero
orphans -- recall is complete. Of the sixteen:
* ONE IS A FALSE POSITIVE. 0x008cca30 has no temper chain at all; the four
bytes read as the second mask are the rel32 displacement of a call.
* FOUR ARE RNG ENTRY POINTS, not game code -- the tempering there is the
primitive's own. Two were known; 0x004f7670 and 0x008e6e30 were not.
So the figure is ELEVEN game functions with inlined draws over 28 sites, not
fourteen. Exactly ONE of the eleven is reachable from StrategyServer::ProcessTurn
(0x007aa240, depth 4) and one more from OnAllCombatDone_Tail (lane J's R2). The
other nine are map setup, the lobby, the network layer and two scripted
encounters, all reached through vtable slots with no direct caller -- so their
absence from the turn closure is proved for direct edges only, and that caveat
is stated as loudly as the result.
Both functions lane J handed over are read completely from the instruction
stream:
0x004f7670 (84 B) is Mars::RNG::NextUInt -- ONE WORD, UNCONDITIONAL, no loop,
no branch but the lazy twist. ECX is the generator OBJECT, where NextFloat
and NextInt take the object PLUS FOUR; ProbabilisticJump uses both conventions
0x6b bytes apart. Ghidra's size is right here, which is worth saying.
0x007aa240 (Ghidra says 944; the body is 953 and ends past the reported range)
is the encounter-detection roll. ONE INLINED NextFloat PER (contact, detector)
TRIAL, drawn BEFORE the accept test, so a detector holding neither of two
specific techs still burns a word and can never succeed. The accept test is
`thresh >= r` -- equality accepts -- derived from the fcompp/test ah,5/jp
encoding rather than the mnemonic. The outer repeat-until-no-progress loop
cannot redraw a pair: the "tried" bitset is filled above the back-edge target
and never cleared, so the whole call is bounded by |contacts| x |detectors|.
Three more draw entry points nobody had listed: a float range (one word, and it
NARROWS TWICE), a triangular integer range (at least two words), and a
truncated-normal range whose two draws are BOTH inlined and which costs two
words per attempt with an unbounded attempt count -- and which scales by 2^-32
where NextFloat scales by 1/(2^32-1). Two divisors, one image.
RECONCILIATION, stated honestly. The complete draw-site inventory of the
ProcessTurn closure is 22 sites: 21 entry-point calls plus the one inlined site.
This lane adds two previously-uncounted sources to that list and accounts for
NONE of lane Z's 18-20 words per turn with certainty, because both new sources
are gated and neither has been measured. Lane J's prediction that these two
functions would explain the gap is NOT confirmed. What is now provable is the
negative: there is no twenty-third mechanism, so the 18-20 words are distributed
among exactly these 22 sites. The search space closes; the count does not.
Also corrects, in place: my own Ghidra comment claiming the masks are a Mars
variant of MT19937. They are the textbook masks applied before the shift
instead of after -- (y & 0xff3a58ad) << 7 == (y << 7) & 0x9d2c5680, verified
over 200k words -- so mars::rng was never wrong, but a scan for the textbook
constants finds nothing in this image.
Fragment validated by generating to a scratch path (797 entries, no duplicate
name); the tracked header is untouched while lane Z is in flight. Eight
prototypes, nine plate comments and eight pre-comments written back to Ghidra.
The second save's numbers were not in the first draft's table. Correcting my
own correction: on a turn-19 Zuul game the counter moves 16, 21 and 44 times
between ProcessTurn entries, against 12-14 on an early two-colony game. So it
is not a driver-invocation counter and not a constant either.
Lane J landed rule 16 (inlined draws are invisible to call-graph sweeps) while
this run was in flight, and it bears directly on three claims here that rested
on direct-call sweeps. Those claims now rest on the behavioural measurement
instead, which is immune to it: ProcessNodeSpaceTravel moved the generator by
0 words on 16 observations and node-line decay on 8. The instrument does not
ask which function drew, only whether the generator moved.
Lane J's resolver map and this ledger pair up: the resolver has no
unconditional draw, so its prediction -- a plain fleet battle costs the same
18-22 words as a peaceful turn -- is testable with these hooks as soon as
someone builds a save where two hostile fleets meet.