findings/subsystems/players-residual.md is the deliverable: every one of the 54
leaves on the reference pair and the 24 on pair 2 attributed to a named
mechanism, with the rung split. 53 of 54 are pass-through -- the block is
unmodelled, not mismodelled, and only player 32's Sav is a number we compute and
get wrong.
Three corrections to the record:
- T31's self-check compared its post-turn result against the pre-turn stored
value, so its 6-of-8 covered only the players whose limit never moves.
- the AI difficulty column IS recoverable from the save, by recomputing BnkEl
under both columns against the value the save carries; 1 AI + 1 non-AI on all
eleven corpus saves.
- BANKRUPTCY_PROTECTION_LIMIT_FACTOR is read fmul dword ptr, so it is a float32
in the image; the decimal disagrees at every max income divisible by ten and
the corpus' seven records land where the two agree (rule 23). formula-gaps Q1
addendum + ghidra/addresses.d/lane-pl.json (3 data entries, fragment validated
to a scratch path, no duplicate names).
verify/results/standalone/{status.json,report.txt} refreshed from a build of main
aabd8a3: the committed copy was stale at 131/55, the real number is 128/54.
T34 RecordObservedDesigns is now readable off the corpus and is NOT an intel
pass: a design's creator registers the design in its own odes, its weapons in
owep, and re-stamps otnL on its techs in otch -- odes on build, otch on creation.
Both are hypotheses until a save exists where a player observes someone else's
design.
Status stays open and is flagged as NOT identifiable: eighteen fields of the
player record split the roster the same way and only two saves carry a non-zero
value. The probe is an entry hook on W2's writer.
6.9 KiB
The /Sim/players residual, decomposed
- Type: subsystem
- Address / RVA: n/a (a measurement over the save corpus; the one binary read is
ServerPlayer::UpdateBankruptcyLimits0x00818600) - Status: verified
- Confidence: high for the decomposition and the leaf counts; the
Statuspredicate is explicitly not identifiable and is flagged as such - Owner / date: lane PL · 2026-09-08
Purpose
Every lane so far attacked a named subsystem and reported which leaves fell out. This one runs the other way: take the largest remaining block of the standalone's residual and say what the leaves in it actually are, grouped by mechanism, so that no later lane has to re-derive it.
The full write-up, with the predictions written before the build and where they were wrong,
is sots-engine/docs/PL-players-residual.md. This note is the evidence and the numbers.
The numbers (re-measured; the published breakdown was stale)
sots_turn from sots-engine main aabd8a3, tools/standalone_report.py --binary …:
| run | pair 1 | pair 2 | /Sim/players |
|---|---|---|---|
| main, no options | 128 | 69 | 54 / 24 |
main, --commit-blocked=T31 --ai-player 1 |
126 | 67 | 52 / 22 |
| lane PL, no options | 126 | 65 | 52 / 20 |
lane PL, --data ROOT --commit-blocked=P11 --ai-player 1..3 |
122 | 62 | 48 / 17 |
status.json said 55. It is 54.
53 of the 54 leaves on pair 1, and 23 of the 24 on pair 2, are values our turn writes back
unchanged from the input save. Exactly one leaf in the whole block — player 32's Sav — is a
number we computed and got wrong. The block is unmodelled, not mismodelled, and a lane
looking for a formula error here will not find one.
The decomposition
| # | mechanism | p1 | p2 | rung |
|---|---|---|---|---|
| M1 | AI research orders: ResRate, ResTNm, ResErrRoll, and the tech-tree state they move |
12 | 2 | B |
| M2 | AI ship construction: ShipRecs/*, Maint, FNG/FNGNum, NumDes + the new Des[…], lboid, player 32's Sav/PvSav |
15 | 9 | B |
| M3 | Observation records refreshed by the AI's own new design: odes, owep, otch/*/otnL (T34, stub) |
9 | 1 | B |
| M4 | Events | 8 | 6 | 4/3 A + 4/3 B |
| M5 | BnkEl, BnkPr |
4 | 4 | A |
| M6 | Status |
4 | 0 | not identifiable |
| M7 | PvSav for the players M2 does not touch |
0 | 2 | A |
| M8 | player 528's one-off design Des[1712 "Refugee Trade Ship"] + NumDes |
2 | 0 | ? |
The finding that matters most
M3 is not an intel pass. RecordObservedDesigns (T34) reads, on the reference workload, as
a self-registration triggered by design creation, not as a sweep over enemy fleets:
- player 32's new
odesentry isodid=18, opid=32— design 18 owned by player 32, which is the design that player itself created this turn (Des[18 "Honor Lance"],NumDes5→6); - its new
owepentry isbal_gauss, that design's weapon; - the three
otchentries whoseotnLmoves 1→2 areDRV_Fissn,WEP_GsDrvr,DRV_Hyper— that design's technologies.
And the two lists refresh on different events: on pair 2 the same design is built again and
odes/.[1]/otnL moves 2→3 while every otch entry stays at 2. So odes is re-stamped on
build and otch on creation. One workload, two rules — both are hypotheses (rule 6)
until a save exists where a player observes someone else's design.
Corrections to the record (rule 11)
- T31's self-check was not a self-check. It reported "BnkEl reproduced for 6 of 8" on
turn1-stateregardless of the AI roster, because it compared its post-turn computation against the pre-turn stored value. The six it "reproduced" were exactly the six players whose limit does not move (five with no colonies, plus the independent colony). The phase's catalog text claimed "8 of 8 with the AI flag supplied", which the binary never printed. Rule 15, in the wild. The check is now taken at load, from the colony state the save was written from, and passes for every live player of all eleven corpus saves. - The AI flag is recoverable from the save.
ServerPlayer+0xf9is not serialised, but the save's ownBnkElwas written by the original from the colony state the save holds, and the AI difficulty column is worth 10% of the max income against aBnkElslope of 6.67 — far more than a truncation. Recomputing under both columns and comparing identifies the column: 1 AI and 1 non-AI on every one of the eleven saves, the rest ambiguous. T31 therefore no longer needs--ai-player, and closes 2 leaves per reference pair with no operator input. Ambiguous means both columns give the same limit, which happens for two reasons and not one: zero max income, or an NPC — the difficulty table's AI row is gated onisAI && !npc, so "Independent Colony" is ambiguous despite owning a colony and carrying a non-zero limit. - The protection factor is a float32. See
formula-gaps.mdQ1 addendum (lane PL) andghidra/addresses.d/lane-pl.json. - The published residual overstates what is unmodelled. 4 + 3 of the leaves belong to P11,
which is written and only wants
--data.tools/standalone_report.pyinvokes the binary with no data root, so the headline number measures the engine plus a missing argument. Reproduced independently on CT111 against/srv/re-lab/gob-extract: 87 closed / 0 regressed on pair 1 and 46 / 0 on pair 2.
Status — a negative result worth keeping
Status goes 0 → 4 on players 16, 32, 496, 512 and stays 0 on 528, 544, 560, 576, then stays 4
on the next turn. The two sets are perfectly correlated with species (0/2 against 4), and
sweeping every scalar field of the player record on turn1-state finds eighteen that split
the roster identically: ReqCL, AMine, CnTrd, CnVItl, cdp, hadvs, harcc, hgs,
PvMA, CstE, CstR, CstT, MinRate, MaxOH, NPTrk, PrGtTrf, TerraMod, pddm.
Only two corpus saves carry a non-zero Status at all.
So this is one observation of one transition on one roster, and any predicate chosen from it
is an eighteen-way coin flip. ReqCL is the most plausible on name and on meaning — W2 places
the writer at OnMessage+0xa15 and MarkPlayerTurnEnded writes 4 from the End-Turn submission
paths — but it is a hypothesis with a workload attached.
The probe that settles it (rules 18 and 20): an entry probe on that writer, capturing the player index on every call for one turn. It names the set directly. Instrument the entry.
Cross-refs
- Related: formula-gaps Q1 + the lane PL addendum, ship-construction (M2's 11,900),
population-growth (the
PvSavprediction it held), strategic-turn-internals - Engine:
sots-engine/docs/PL-players-residual.md,src/game/sim/player_turn.{h,cpp},tests/game_sim/test_player_turn.cpp - Addresses:
ghidra/addresses.d/lane-pl.json