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.
118 lines
6.9 KiB
Markdown
118 lines
6.9 KiB
Markdown
# The `/Sim/players` residual, decomposed <!-- subsystem -->
|
|
|
|
- **Type:** subsystem
|
|
- **Address / RVA:** n/a (a measurement over the save corpus; the one binary read is
|
|
`ServerPlayer::UpdateBankruptcyLimits` 0x00818600)
|
|
- **Status:** verified
|
|
- **Confidence:** high for the decomposition and the leaf counts; the `Status` predicate 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 `odes` entry is `odid=18, opid=32` — design 18 owned by player 32, which is
|
|
the design *that player itself created this turn* (`Des[18 "Honor Lance"]`, `NumDes` 5→6);
|
|
* its new `owep` entry is `bal_gauss`, that design's weapon;
|
|
* the three `otch` entries whose `otnL` moves 1→2 are `DRV_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)
|
|
|
|
1. **T31's self-check was not a self-check.** It reported "BnkEl reproduced for 6 of 8" on
|
|
`turn1-state` *regardless 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**.
|
|
2. **The AI flag is recoverable from the save.** `ServerPlayer+0xf9` is not serialised, but the
|
|
save's own `BnkEl` was written by the original from the colony state the save holds, and the
|
|
AI difficulty column is worth 10% of the max income against a `BnkEl` slope 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 on
|
|
`isAI && !npc`, so "Independent Colony" is ambiguous despite owning a colony and carrying a
|
|
non-zero limit.
|
|
3. **The protection factor is a float32.** See `formula-gaps.md` Q1 addendum (lane PL) and
|
|
`ghidra/addresses.d/lane-pl.json`.
|
|
4. **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.py` invokes 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 `PvSav` prediction 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`
|