Compare commits
3 commits
6669c4ab1e
...
247285ac28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
247285ac28 | ||
|
|
49fa5501ba | ||
|
|
3b99e255c7 |
11 changed files with 1208 additions and 79 deletions
|
|
@ -267,3 +267,9 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| AI2 corrections to AI1 | meta | verified | high | 100% | 2026-09-08 | **31 task classes, not 34** - slot-1 x slot-8 across 31 vtables gives a gapless type enum 0..0x20 with two retired ids. **26 order methods, not 21.** **7 pure virtuals, not 8** - named: GetTypeId, GetTargetA/B, Execute(agent,pass), IsFinished(agent), GetTypeName, Describe. Also a test expectation was wrong on first run (ColonizeAt 1300 outranks NodeBore 1275) and lane AI2 FIXED THE TEST, NOT THE CODE |
|
||||
| AI2 boundary | meta | verified | high | 100% | 2026-09-08 | Slots 11/12/13 UNNAMED. The nine tasks with no reachable order method are INFERRED to be goal/planner tasks - bodies unread. The two-pass meaning is INFERENCE. The reachability table is a DIRECT-CALL CLOSURE = lower bound (rule 16 bites hard in a vtable system). **The StrategyApp+0x1c ENQUEUE SITE WAS NOT FOUND, so AI-stepping order is still open** - and that matters because ModCount is a count. The .data invade tunables 0x00a1795c/60 have no traced loader. NOTHING RAN UNDER AN INSTRUMENT; four falsifiable predictions with symptoms are in the finding |
|
||||
| INTEGRATOR NOTE: do not share a build dir with a background gate | meta | verified | high | 100% | 2026-09-08 | I read "98% tests passed, 1 failed" and started hunting a flaky test in a DETERMINISM project - a serious thing to chase. It was my own error: I ran an ad-hoc ctest against the same CT111 build directory while the BACKGROUND gate job was still writing binaries into it. The gate itself reported 47/47 and the shim built; 13 further clean runs (5 serial, 8 at -j4) confirm. RULE: one build directory per gate invocation, and do not query a dir while a background job owns it - the failure mode looks exactly like nondeterminism in the engine, which is the one thing this project must never misdiagnose |
|
||||
| ComputeOutput turn path modelled | subsystems | verified | high | 90% | 2026-09-08 | Lane C3. **+1 leaf closed, 0 regressed, on BOTH pairs** (reference 209 -> 157, closed 52; pair 2 108 -> 86, closed 22); the leaf is /Sim/players/Player[576 "Independent Colony"]/Sav both times. THE FUNCTION IS THIN: 0x00751fb0 zeroes 12 slots, calls ComputeOutputFromRates(out, &sys->Rts) with the system's OWN, UNNORMALISED sliders, then fills three reporting slots - and **out[3], the only slot ComputeBudget reads (confirmed at 0x008631fd), is written entirely inside ComputeOutputFromRates and never touched again**. THE LOAD-BEARING FACT NOBODY HAD: **construction points the build queue and the ship-repair pass do NOT spend are REDISTRIBUTED over trade/terraform/infra, and the trade share is added to the money channel** - and unspent infra cascades into the TERRAFORM pool, which cascades into money. TWO HOPS, NOT ONE. So a colony with an empty build queue earns the same money whichever way its sliders point. Which float decides an output value: **SRsc and the build-queue demand**; everything else in the corpus is suppressed by the normaliser or rounds to nothing |
|
||||
| the two-path cross-check decomposes TO THE UNIT | verify | verified | high | 100% | 2026-09-08 | Stronger than "the two paths agree". The engine now runs BOTH the projected path (BnkEl-constrained) and the turn path on the same colony state every load and reports the difference - and on all 11 saves **every delta decomposes to the unit into the build queue's points priced through the money chain**: 5 saves agree exactly (all queues empty); human-turn3 differs by 37 = one five-point block x 5 x IncMod 1.48; zuul-turn23 by 2035 = 370x5x1.1; zuul-turn5 by 39160 = 7120x5x1.1; zuul-turn17 by 21010; zuul-turn16 by 36438 (within the 1 unit two truncations of a .5 allow). The two paths differ by EXACTLY what the disassembly says |
|
||||
| S11 CIVILIAN GROWTH is the highest-value next item | verify | backlog | — | 0% | 2026-09-08 | Lane C3's headline for the roadmap: the residual on the metric is UPSTREAM, not in the output formula. ComputeBudget runs AFTER S11 SystemTurn (strategic phases 11 then 13), and our S11 DOES NOT COMMIT CIVILIAN GROWTH. The human's savings come out 289,092 against the oracle's 289,688 - the growth gap (597) minus one trade point. **ONE INPUT (imperial carrying capacity) GATES the human's Sav, PvSav, BnkEl AND BnkPr on both pairs.** The AI's are wrong for a different reason: its ResRate/ResTNm are its own orders, i.e. Rung B |
|
||||
| the last unmodelled output input: ship repair demand | verify | backlog | — | 0% | 2026-09-08 | Nine inputs to the turn path; EIGHT ARE ON THE WIRE (Rts, dsu, IdealSuit, TerraMod, Infra/ibon, BQ/ords/conleft, NumSnF, the output total). The ninth is **the repair demand of damaged ships in orbit**, needing Ship::RepairCost 0x00815180, which is UNREAD - it is taken as 0, so **a colony with a damaged fleet reads HIGH**. Also note this is the same repair pass that makes ComputeOutputFromRates unsafe to call for harvesting (the B1 replace double-run defect) |
|
||||
| the two RNG words: the blocker MOVED again | verify | mapped | high | 90% | 2026-09-08 | They still cannot enter the generator model and THE REASON HAS CHANGED AGAIN. The allocation is now computable - the money input was the whole gap. What blocks them is one step further out: **the only corpus player reaching ProcessResearch with a target is the AI**, whose research rate and target are generated during the same turn (Rung B). On turn1-state NO player has a target, so both words cost zero on the reference pair - **but rule 20 applies to that sentence**: a count cannot separate "not entered" from "entered and drew nothing". AN ENTRY PROBE ON ProcessResearch SETTLES IT IN MINUTES. Ledger unchanged at 16 words modelled |
|
||||
| two callees with NON-STANDARD conventions | objects | verified | high | 100% | 2026-09-08 | Recorded because they read WRONG as __thiscall and would silently mis-model: **0x0074c6f0 takes `this` in ESI**, and **0x007460b0 takes the system in EBX**. Same class of trap as lane P2's two helpers that read ebx/esi live-in without ever writing them |
|
||||
|
|
|
|||
|
|
@ -63,9 +63,17 @@ out[3] = _ftol2( TradePointsToMoney( [ebp-0x28] + [ebp-0x40] + [ebp-0x18] ) )
|
|||
with
|
||||
|
||||
* `[ebp-0x40]` = `round(round(ComputeTotalOutput(SRoh)) × SRt)` — the **trade points**;
|
||||
* `[ebp-0x28]` = leftover **science** points, non-zero only when the repair block ran;
|
||||
* `[ebp-0x28]` = the trade share of the leftover **ship-construction** points, non-zero only
|
||||
when the build queue and the repair pass left some over;
|
||||
* `[ebp-0x18]` = leftover **terraforming** points, itself fed by leftover **industry** points.
|
||||
|
||||
**Correction (lane C3).** This section originally called `[ebp-0x28]` "leftover **science**
|
||||
points". There is no science channel in this function: `OutputRates+4` is the ship-construction
|
||||
slider `SRsc`, 0x00746830 turns it into construction points, and the leftover is what the build
|
||||
queue (0x008251e0) and the ship-repair pass (0x00751590) did not spend. Research is bought with
|
||||
money at the empire level, not produced per system. `strategic-turn-internals.md` §3.3 had this
|
||||
right. See `output-turn-path.md`.
|
||||
|
||||
`round` is 0x008e5660 (`fistp`/`fild`, ties to even); the outer conversion is the truncating
|
||||
`_ftol2`. Under the max-income rate vector all three of science, terraform and industry points are
|
||||
`round(T × 0) = 0`, and §2.4 shows both cascade sources are then zero, so
|
||||
|
|
@ -112,9 +120,10 @@ Three corrections to `formula-gaps.md` Q3, which had this block as MEDIUM confid
|
|||
always `≥ 0`. `leftoverTerraform = 0 − min(0, ceil(that)) = 0`.
|
||||
(This corrects the implication in `output-term.md`'s "unspent terraforming points cascade into
|
||||
the money channel": they do cascade, but only when the terraform channel is actually funded.)
|
||||
* Science: `out[7] = 0x00746830(sys, 0.0)`; with zero science points the repair block's guard
|
||||
`esi > 0` fails and `[ebp-0x28]` keeps the `fldz` written at 0x00751d5f. *Inferred* — that
|
||||
0x00746830 returns 0 for an argument of 0 is read from its shape, not proven here.
|
||||
* Construction: `out[7] = 0x00746830(sys, 0.0)`; with zero construction points nothing is left
|
||||
over and `[ebp-0x28]` keeps the `fldz` written at 0x00751d5f. **Lane C3 upgraded this from
|
||||
inferred to instruction-verified**: 0x00746830 with an argument of 0 returns
|
||||
`ftol(k x (b x 0) + 0) = 0` on both its branches, so the guard really does fail.
|
||||
|
||||
### 2.5 `PopIncome(t)` — 0x0074d760 (instruction-verified)
|
||||
|
||||
|
|
@ -386,7 +395,15 @@ save's. Measured with `--commit-blocked=T31 --ai-player 1`: **0 closed, 0 regres
|
|||
and 108 -> 103, unchanged from the baseline). The same numbers hold with every other blocked phase
|
||||
committed alongside it.
|
||||
|
||||
### 6.2 P01 `ComputeBudget` -- NOT unblocked, and the roadmap's item 1 was wrong about this
|
||||
### 6.2 P01 `ComputeBudget` -- NOT unblocked by THIS lane (closed by lane C3)
|
||||
|
||||
> **Update, lane C3 (same day).** Everything this section says is right, and the turn path is
|
||||
> now read and modelled: see `output-turn-path.md`. The one thing it did not anticipate is that
|
||||
> the leftover construction points come back to the **trade** channel, so on a corpus where
|
||||
> every build queue is empty the two paths give the same money to within one trade point --
|
||||
> which makes the `BnkEl` oracle an indirect check on the turn path after all. P01/P02 are
|
||||
> committed and closed one leaf on each reference pair.
|
||||
|
||||
|
||||
`ComputeBudget` has two modes and they take their per-system money from **different functions**
|
||||
(0x008631fd, the `[ebp+0xc]` test):
|
||||
|
|
@ -400,7 +417,7 @@ else { OutputRates r; ComputeOutput(s, &r); money = r.out[3] } // 0x
|
|||
that path the science, construction and terraform channels are funded, which means:
|
||||
|
||||
* the repair pass inside `ComputeOutputFromRates` runs (it is **not** side-effect free -- it repairs
|
||||
ships in orbit), so its leftover science points can be non-zero;
|
||||
ships in orbit), so leftover construction points can be non-zero;
|
||||
* the unspent-industry and unspent-terraforming cascades into the money channel are live, and §2.4's
|
||||
proof that both are zero **does not apply**;
|
||||
* the build queue's consumption of construction points is in the same call.
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ The real population→output term is `PopOutput(0) + PopOutput(1) + SlaveOutput(
|
|||
|
||||
| addr | name | conv / boundary | note |
|
||||
|---|---|---|---|
|
||||
| 0x00751bb0 | `ServerSystem::ComputeOutputFromRates(int out[12], OutputRates* rates)` | thiscall, ends 0x00751fa8 | **repairs ships in orbit** — not compare-safe |
|
||||
| 0x00751bb0 | `ServerSystem::ComputeOutputFromRates(int out[12], OutputRates* rates)` | thiscall `ret 8`, ends 0x00751fa0 | **repairs ships in orbit** — not compare-safe |
|
||||
| 0x00750480 | `ServerSystem::ComputeTotalOutput(double SRoh)` | thiscall `ret 8`, ends 0x007505a5 | **side-effect free** — the compare target |
|
||||
| 0x00747d30 | `ServerSystem::StripMineFraction()` | thiscall, returns float, ends 0x00747ddd | |
|
||||
| 0x007483b0 | `ServerSystem::OverHarvestDemand(double SRoh)` | thiscall `ret 8`, ends 0x007484c4 (Ghidra size 276 = correct here) | |
|
||||
|
|
@ -49,6 +49,9 @@ The real population→output term is `PopOutput(0) + PopOutput(1) + SlaveOutput(
|
|||
| 0x00545cc0 | `SpeciesDef(int sp)` | cdecl | table at .bss 0x00b10a00, stride 0x184, 7 rows |
|
||||
| 0x008e5680 | `signed_cbrt(double)` | cdecl | `x>=0 ? pow(x,1/3) : -pow(-x,1/3)`; exponent is the double 1/3 at 0x00a3a7c8 |
|
||||
|
||||
(Boundary corrected by lane C3: the `ret 8` is at 0x00751fa0 and 0x00751fa3..0x00751faf is
|
||||
`int3` padding, so the 0x00751fa8 above was padding, not code.)
|
||||
|
||||
`ComputeOutputFromRates` is reached from `ComputeOutput` 0x00751fb0 (projected) and
|
||||
`ComputeMaxIncome` 0x007521c0 (max), which are what `ComputeBudget` 0x00863030 and
|
||||
`UpdateBankruptcyLimits` 0x00818600 call.
|
||||
|
|
|
|||
505
findings/subsystems/output-turn-path.md
Normal file
505
findings/subsystems/output-turn-path.md
Normal file
|
|
@ -0,0 +1,505 @@
|
|||
# `ComputeOutput` on the turn path (lane C3, 2026-09-08)
|
||||
|
||||
`ServerSystem::ComputeOutput` 0x00751fb0 and the whole of `ComputeOutputFromRates`
|
||||
0x00751bb0 — the money `ComputeBudget` actually sums during a turn, as opposed to the
|
||||
projected-mode `ComputeMaxIncome` that lane E1 closed at 25/25 on the `BnkEl` oracle.
|
||||
|
||||
Evidence: own `objdump -d` pass over `Sword of the Stars.exe`. Both ranges disassembled to
|
||||
the **next function start** and the real boundary found (rule 17). Field names per
|
||||
`findings/objects/struct-recovery.md`. Everything marked *instruction-verified* is read off
|
||||
the instruction stream; everything marked *inferred* is not.
|
||||
|
||||
This supersedes two labels in earlier notes:
|
||||
|
||||
* `income-term.md` §2.2 calls `[ebp-0x28]` "leftover **science** points". There is no science
|
||||
channel in this function. `OutputRates+4` is the **ship-construction** slider `SRsc`, the
|
||||
helper at 0x00746830 turns it into **construction points**, and the "leftover" is what the
|
||||
build queue and the ship-repair pass did not spend. `strategic-turn-internals.md` §3.3 had
|
||||
this right; the income lane's paraphrase did not.
|
||||
* `output-term.md` §1 gives `ComputeOutputFromRates`'s end as 0x00751fa8. The `ret 8` is at
|
||||
**0x00751fa0**; 0x00751fa3..0x00751faf is `int3` padding. E1's 0x00751fa0 is correct.
|
||||
|
||||
---
|
||||
|
||||
## 1. The call chain
|
||||
|
||||
| addr | name | conv / boundary | note |
|
||||
|---|---|---|---|
|
||||
| 0x00751fb0 | `ServerSystem::ComputeOutput(int out[12])` | thiscall `ret 4`, ends 0x00752070 | Ghidra size 192 = correct |
|
||||
| 0x00751bb0 | `ServerSystem::ComputeOutputFromRates(int out[12], OutputRates*)` | thiscall `ret 8`, ends 0x00751fa0 | **repairs ships in orbit** |
|
||||
| 0x00747390 | `NormaliseOutputRates(float rates[4], ServerSystem*, float* pinned)` | **cdecl** | pinned = null → trade |
|
||||
| 0x00750480 | `ServerSystem::ComputeTotalOutput(double SRoh)` | thiscall `ret 8` | lane N; side-effect free |
|
||||
| 0x00746830 | `ServerSystem::ConstructionPoints(double consShare)` → int | thiscall `ret 8`, ends 0x00746883 | shipyard-station bonus |
|
||||
| 0x00815c10 | `ServerSystem::StationCount(ServerPlayer*, int kind)` | cdecl-through-thiscall | **kind 1** here (shipyards) |
|
||||
| 0x008251e0 | `BuildQueue::TotalConstructionLeft()` → int | thiscall `ret 0`, 23 bytes | Σ `conleft` over the order list |
|
||||
| 0x00751590 | `ServerSystem::RepairShipsInOrbit(int points, bool estimateOnly)` → int | thiscall `ret 8`, ends 0x007517b5 | **the side effect**; returns points LEFT |
|
||||
| 0x00745d60 | `ServerSystem::IdealSuitability()` → double | thiscall `ret 0`, ends 0x00745dc0 | §2.6 |
|
||||
| 0x00746890 | `ServerSystem::TerraformPointsNeeded()` → double | thiscall `ret 0`, ends 0x0074690d | §2.5 |
|
||||
| 0x0074c6f0 | `ServerSystem::CivilianConsumption()` → int | **reads `esi` as `this`** | resource ledger only |
|
||||
| 0x007483b0 | `ServerSystem::OverHarvestDemand(double SRoh)` | thiscall `ret 8` | lane N |
|
||||
| 0x007505b0 | `ServerSystem::TradePointsToMoney(double trade)` → double | thiscall `ret 8` | lane E1 |
|
||||
| 0x008e5660 | `round(double)` | cdecl | `fistp`/`fild`, ties to **even** |
|
||||
| 0x00925220 | `_ftol2` | — | truncating |
|
||||
| 0x009dd1e4 | `ceil` (MSVCR100) | IAT | |
|
||||
| 0x0083a5b0 | `TradeManager::SystemRouteIncome(mgr, sys, ServerPlayer*, int* a, int* b)` | `ret 0x10` | fills `out[4]`,`out[5]` |
|
||||
| 0x007460b0 | `SystemRepairDemandForOwner(ServerPlayer*)` → int | **reads `ebx` as `sys`** | fills `out[6]` |
|
||||
|
||||
Two non-standard conventions worth flagging (both would be silently wrong if taken as
|
||||
`__thiscall`): 0x0074c6f0 takes its `this` in **ESI** and 0x007460b0 takes the system in
|
||||
**EBX**. Both are compiler-local helpers that inherit a register the caller happens to hold.
|
||||
|
||||
---
|
||||
|
||||
## 2. The formula
|
||||
|
||||
### 2.1 `ComputeOutput(out)` — 0x00751fb0 (instruction-verified)
|
||||
|
||||
```
|
||||
if (out == 0) return
|
||||
memset(out, 0, 48) // 12 dwords; slots 10 and 11 are FLOATS
|
||||
if (sys->PID /*+0x100*/ == 0) return
|
||||
ComputeOutputFromRates(out, &sys->Rts) // sys+0x88 -- the system's OWN sliders, unnormalised
|
||||
if (sys->[0x230] != 0) return // a byte; NOT on the wire
|
||||
mgr = server->vtbl[2]() // server = sys->+0x10, the RAW StrategyServer base
|
||||
TradeManager::SystemRouteIncome(mgr, sys, sys->PID, &a, &b)
|
||||
out[4] = a ; out[5] = b
|
||||
out[6] = SystemRepairDemandForOwner(sys->PID) // 0x007460b0, `sys` arrives in EBX
|
||||
```
|
||||
|
||||
**`out[3]` — the money — is written entirely inside `ComputeOutputFromRates`.** Nothing after
|
||||
that call touches it, so the trade-route and repair-demand slots are reporting only.
|
||||
|
||||
`ComputeBudget` 0x00863030 confirms this at 0x008631fd:
|
||||
|
||||
```
|
||||
for s in player->systems:
|
||||
if (s->Abdn /*+0xc4*/) continue
|
||||
if (projected) money = ComputeMaxIncome(s) // 0x007521c0
|
||||
else { int o[12]; ComputeOutput(s, o); money = o[3]; } // [ebp-0xe0 + 0xc]
|
||||
if (money > 0) income += money // budget+0x04
|
||||
if (money < 0) expense += -money // budget+0x1c
|
||||
```
|
||||
|
||||
### 2.2 The `OutputRates` memory layout (instruction-verified)
|
||||
|
||||
`ServerSystem+0x88`, 0x1c bytes, seven dwords. The **memory** order is not the wire order:
|
||||
|
||||
| offset | field | who reads it |
|
||||
|---|---|---|
|
||||
| +0x00 | `SRt` trade | trade points, and the leftover weight |
|
||||
| +0x04 | `SRsc` **ship construction** | construction points |
|
||||
| +0x08 | `SRtf` terraform | zeroed by normalisation when `Suit == IdealSuitability()` |
|
||||
| +0x0c | `SRi` infrastructure | zeroed by normalisation when `f32(Infra + ibon) >= 1` |
|
||||
| +0x10 | `SRoh` over-harvest | `ComputeTotalOutput` / `OverHarvestDemand` |
|
||||
| +0x14 | `SRs` | not read here (slave death rate) |
|
||||
| +0x18 | `SRnr` (int) | not read here |
|
||||
|
||||
The suppressions are read off `NormaliseOutputRates` 0x00747390: `[edi+0x8] := 0` under the
|
||||
`Suit == IdealSuitability()` branch and `[edi+0xc] := 0` under `1.0 <= f32(ibon + Infra)`.
|
||||
The wire order (`SRs, SRt, SRsc, SRtf, SRi, SRoh, SRnr`) is a serialiser choice.
|
||||
|
||||
### 2.3 `ComputeOutputFromRates(out, rates)` — 0x00751bb0 (instruction-verified)
|
||||
|
||||
```
|
||||
memset(out, 0, 48)
|
||||
if (sys->PID == 0) return
|
||||
r = *rates // 7 dwords copied to the stack: the caller's
|
||||
NormaliseOutputRates(&r, sys, 0) // struct is never modified
|
||||
|
||||
T = round( ComputeTotalOutput(r.SRoh) ) // ties to even, kept as a double
|
||||
tradePts = round( T * r.SRt )
|
||||
consPts = round( r.SRsc * T )
|
||||
terraPts = round( r.SRtf * T )
|
||||
infraPts = round( r.SRi * T )
|
||||
out[0] = ftol(T)
|
||||
|
||||
// --- resource ledger (independent of everything below) -------------------------
|
||||
need = ftol(OverHarvestDemand(r.SRoh)) + CivilianConsumption()
|
||||
resAvail = sys->Res + (PID->[0x138] ? sys->MRes + sys->ARes2 : 0)
|
||||
out[2] = min(resAvail, max(need, 0))
|
||||
out[1] = min(out[2], ftol(OverHarvestDemand(r.SRoh)))
|
||||
|
||||
// --- construction ---------------------------------------------------------------
|
||||
queueNeed = sys->BQ ? BuildQueue::TotalConstructionLeft(sys->BQ) : 0
|
||||
out[7] = ConstructionPoints(consPts) // ftol(c + stations*BONUS*c)
|
||||
out[8] = min(queueNeed, out[7])
|
||||
rem = max(out[7] - out[8], 0)
|
||||
out[9] = 0
|
||||
if (rem > 0):
|
||||
left = RepairShipsInOrbit(rem, /*estimateOnly=*/false)
|
||||
out[9] = rem - left
|
||||
rem = left
|
||||
|
||||
// --- the leftover redistribution -------------------------------------------------
|
||||
Ltrade = Lterra = Linfra = 0.0
|
||||
if (rem > 0):
|
||||
P = (double) rem
|
||||
if (r.SRsc == 1.0): // an exact ==
|
||||
wT = 1.0
|
||||
wF = (sys->Suit != IdealSuitability()) ? 1.0 : 0.0
|
||||
wI = (sys->Infra != 1.0) ? 1.0 : 0.0
|
||||
else:
|
||||
wT = r.SRt ; wF = r.SRtf ; wI = r.SRi
|
||||
S = wI + (wF + wT) // that association
|
||||
Ltrade = round( wT * P / S )
|
||||
Lterra = round( wF * P / S )
|
||||
Linfra = round( wI * P / S )
|
||||
|
||||
// --- infrastructure --------------------------------------------------------------
|
||||
infraNeed = ceil( (1.0 - sys->Infra) / 3.3e-5 ) // 0x00a1f930
|
||||
poolI = Linfra + infraPts
|
||||
spendI = min(poolI, infraNeed)
|
||||
leftI = max(poolI - spendI, 0)
|
||||
out[10] = (float) max( spendI / 500.0 * 0.01 * 1.65, 0 ) // three 80-bit steps
|
||||
|
||||
// --- terraforming ----------------------------------------------------------------
|
||||
terraNeed = ceil( TerraformPointsNeeded() )
|
||||
poolF = (Lterra + terraPts) + leftI // the infra cascade lands HERE
|
||||
spendF = min(poolF, terraNeed)
|
||||
leftF = max(poolF - spendF, 0)
|
||||
sign = (IdealSuitability() < sys->Suit) ? -1.0 : 1.0
|
||||
out[11] = (float) ( sign * (spendF * 1.5 * 1.2f * PID->TerraMod) / 20000.0 )
|
||||
|
||||
// --- money -----------------------------------------------------------------------
|
||||
out[3] = ftol( TradePointsToMoney( (Ltrade + tradePts) + leftF ) )
|
||||
```
|
||||
|
||||
Two things this makes explicit that no earlier note stated:
|
||||
|
||||
1. **The infrastructure leftover cascades into the terraform pool, and the terraform
|
||||
leftover cascades into money.** There is no direct industry→money edge; it is two hops.
|
||||
`income-term.md` §2.4's "both cascade sources are zero" is right for the max-income rate
|
||||
vector (where `infraPts = terraPts = 0` *and* both needs are `>= 0`) but says nothing
|
||||
about the turn path.
|
||||
2. **The construction leftover's trade share is added to the money channel.** On a colony
|
||||
whose build queue is empty, every construction point the system produced comes back as
|
||||
trade points. A system with `SRt = 0, SRsc = 1` and an empty queue therefore earns
|
||||
**exactly as much money** as one with `SRt = 1`. That is the single most consequential
|
||||
fact in this function and it is why the corpus's turn-path money is so close to its
|
||||
projected money (§4).
|
||||
|
||||
### 2.4 `RepairShipsInOrbit(points, estimateOnly)` — 0x00751590 (instruction-verified)
|
||||
|
||||
```
|
||||
if (points == 0) return 0
|
||||
need = 0 ; cand = vector<StarShip*>()
|
||||
for i in 0 .. sys->vtbl[2]()-1: // fleets at the system
|
||||
f = sys->vtbl[4](i)
|
||||
if (f->[0x58] != sys->PID) continue
|
||||
if (!Fleet::TestFlags(f, 0, 8)) continue
|
||||
for sh in f->ships (+0xa4 .. +0xa8):
|
||||
if (!Ship::TestFlags(sh, 0, 8)) continue
|
||||
c = Ship::RepairCost(sh, 1) // 0x00815180
|
||||
if (c <= 0) continue
|
||||
need += c
|
||||
if (!estimateOnly) cand.push_back(sh)
|
||||
if (estimateOnly) return max(points - need, 0)
|
||||
// round robin
|
||||
progress = true
|
||||
while (progress && points > 0):
|
||||
share = max(points / cand.size(), 1) // integer divide
|
||||
progress = false
|
||||
for sh in cand:
|
||||
if (points <= 0) return points
|
||||
take = min(Ship::RepairCost(sh, 1), share)
|
||||
if (take > 0) { Ship::ApplyRepair(sh, take); progress = true; points -= take }
|
||||
return points
|
||||
```
|
||||
|
||||
**The round robin is equivalent to `points - min(points, need)`** and this is a proof, not an
|
||||
observation: `share >= 1` always, so every ship with a positive remaining cost takes at least
|
||||
one point per pass; the loop can only stop early via `progress == false`, which requires every
|
||||
remaining cost to be zero. So the pass ends with `points == 0` (need was the binding
|
||||
constraint) or with every cost zero (points was). The engine models it as the `min` and pins
|
||||
the equivalence in a test rather than reproducing the round robin.
|
||||
|
||||
`ComputeOutputFromRates` passes `estimateOnly = 0`, so **this is the B1 replace double-run
|
||||
hazard**: calling the original to harvest a value repairs ships. The engine never does.
|
||||
|
||||
### 2.5 `TerraformPointsNeeded()` — 0x00746890 (instruction-verified)
|
||||
|
||||
```
|
||||
if (PID == 0) return 0.0
|
||||
sign = (IdealSuitability() < sys->Suit) ? -1.0 : 1.0 // 0x009e3348 = -1.0
|
||||
rate = f32(PID->TerraMod /*+0x134*/) * 1.8000000715255737 // 0x00a1f928 = (double)1.8f
|
||||
d = fabs( f32(IdealSuitability() - sys->Suit) )
|
||||
return d / fabs( rate * sign / 20000.0 )
|
||||
```
|
||||
|
||||
The `fabs` is after the sign multiply, so the sign cancels and the result is always `>= 0`
|
||||
(E1's §2.4 reading confirmed). Note `1.8f` here is the same number as `1.5 * 1.2f` in
|
||||
`out[11]` — `1.5 * 1.2000000476837158 == 1.8000000715255737` exactly — so the point count and
|
||||
the point value use one rate, folded differently.
|
||||
|
||||
`TerraMod == 0` makes this `+inf`, `ceil(+inf) = +inf`, and the terraform channel then absorbs
|
||||
its whole pool with nothing cascading to money. No corpus player has `TerraMod == 0` (Human
|
||||
and Tarkas carry 1.0, species 4 carries 3.7), so that branch is **unexercised**.
|
||||
|
||||
### 2.6 `IdealSuitability()` — 0x00745d60 (instruction-verified)
|
||||
|
||||
```
|
||||
if (sys->PID == 0) return (double) sys->Suit // an unowned system is "at its ideal"
|
||||
v = PID->IdealSuit /*+0xb0*/
|
||||
if (sys->indi /*+0x1c8*/) v = StrategyServer::IdealSuit(sys->server, sys->indi->indsp)
|
||||
if (sys->dsu /*+0x118*/ != SENTINEL) v = sys->dsu // SENTINEL is the float at 0x00aeca6c
|
||||
return v
|
||||
```
|
||||
|
||||
This is **not** the same source `CalcSuitMod` uses in the money cost (E1 §2.6 reads the
|
||||
server's per-species array there). Here the owner's own `IdealSuit` field is the default and
|
||||
the server array is only consulted for an independent colony. In the 11-save corpus the two
|
||||
agree, so the difference is instruction-verified only.
|
||||
|
||||
`dsu` is `3.4028235e38` (`FLT_MAX`) on **every system of every corpus save**, so the override
|
||||
never fires and the sentinel's identity is *inferred* from that (the value at 0x00aeca6c is a
|
||||
data-file-loaded `GlobalConst` slot, not a `.rdata` literal, so it was not read directly).
|
||||
|
||||
### 2.7 `ConstructionPoints(cons)` — 0x00746830 (instruction-verified)
|
||||
|
||||
```
|
||||
if (PID == 0) return ftol(cons)
|
||||
k = StationCount(sys, PID, 1) // 1 = shipyard
|
||||
b = (STATION_BONUS_SHIPCON > 0) ? STATION_BONUS_SHIPCON : 0
|
||||
return ftol( k * (b * cons) + cons )
|
||||
```
|
||||
|
||||
Note the association `((b * cons) * k) + cons`, and that the bonus is ignored unless strictly
|
||||
positive — the same "unloaded tuning table reads as 1.0, not 0.0" guard the output term has.
|
||||
|
||||
---
|
||||
|
||||
## 3. What this needs that the max-income path did not
|
||||
|
||||
| input | source | held? |
|
||||
|---|---|---|
|
||||
| `Rts` (5 floats) | wire, `Sys/Rts` | yes |
|
||||
| `IdealSuitability()` | `Player/IdealSuit`, `Sys/dsu`, `Sim/ISsu[]` | yes |
|
||||
| `Infra`, `ibon` | wire | yes |
|
||||
| shipyard station count | `Sys/NumSnF` | yes (0 on every corpus system) |
|
||||
| build-queue demand Σ`conleft` | wire, `Sys/BQ/ords` | yes |
|
||||
| **ship repair demand in orbit** | needs `Ship::RepairCost` 0x00815180 over the fleets at the system | **NO** |
|
||||
| `TerraMod` | wire, `Player/TerraMod` | yes |
|
||||
| the output total | lane N | yes |
|
||||
|
||||
**One input of nine is missing**, and it is the repair demand. It is zero whenever no ship in
|
||||
orbit is damaged. On the two reference pairs the human's and the AI's colonies carry no fleet
|
||||
at all in the input state except Ke'Dolarra (turn 2) and Koa'Vo, so the exposure is small but
|
||||
not provably nil; the engine takes it as an explicit input defaulting to 0 and reports it.
|
||||
|
||||
---
|
||||
|
||||
## 4. PREDICTION — written before the build
|
||||
|
||||
The corpus turns out to make this function much simpler than it looks, and the predictions
|
||||
below are the consequences.
|
||||
|
||||
Measured facts about the corpus, from the wire (`Sys/Rts`, `Sys/dsu`, `Sys/BQ`, `Sys/NumSnF`,
|
||||
`Player/IdealSuit`, `Player/TerraMod`), before any model was written:
|
||||
|
||||
* every owned colony in all 11 saves has `Suit == IdealSuit` **exactly** and `Infra == 1.0`
|
||||
exactly, so `NormaliseOutputRates` zeroes both `SRtf` and `SRi` on every one of them;
|
||||
* consequently `infraNeed = ceil(0/3.3e-5) = 0`, `terraNeed = ceil(0/rate) = 0`, and
|
||||
`infraPts = terraPts = 0`;
|
||||
* no system carries a station, so the shipyard bonus is 1;
|
||||
* `dsu` is `FLT_MAX` everywhere, so the ideal-suitability override never fires;
|
||||
* the sliders that occur are `{t .5, sc .5}`, `{t 0, sc 1}`, `{t .04, sc .96}` and
|
||||
`{.25,.25,.25,.25}`; after suppression they normalise to `{t, 1-t, 0, 0}`.
|
||||
|
||||
### 4.1 What I expect
|
||||
|
||||
1. **The leftover redistribution collapses to "all of it to trade" on every corpus colony.**
|
||||
In the `SRsc == 1` branch the weights are `(1, 0, 0)` because suit is at the ideal and
|
||||
infra is exactly 1; in the other branch they are `(SRt, 0, 0)`. Either way
|
||||
`Ltrade = round(P) = P` and `Lterra = Linfra = 0`.
|
||||
2. **Therefore `out[3] = ftol(TradePointsToMoney(round(T*SRt) + ftol(round(T*SRsc))))`**, and
|
||||
with an empty build queue and no repairs the argument is `2*round(T/2)` for a 50/50 colony
|
||||
and `T` for a pure-construction one — i.e. **within ±1 of `ComputeMaxIncome`'s `T`**.
|
||||
3. **The turn path's money therefore equals the projected path's money to ±1 trade point on
|
||||
this corpus.** If that is right, the `BnkEl` oracle — which constrains only the projected
|
||||
path — becomes an *indirect* check on the turn path here, and only here.
|
||||
4. Numerically, using the `BnkEl` the oracle saves already carry (inverted through
|
||||
`ftol(maxIncome / -0.15000000596046448)`):
|
||||
|
||||
| player | pair | oracle's `Sav` move | decomposition |
|
||||
|---|---|---|---|
|
||||
| 576 Independent Colony | 1→2 | `0 → 98871` | `99871 − 1000 maint + 0 interest` |
|
||||
| 576 | 2→3 | `98871 → 198730` | `99871 + 988 interest − 1000 maint` |
|
||||
| 16 human | 1→2 | `50000 → 289688` | `239188 + 500 interest`, and `239189` is the max income of the **post-growth** turn-2 colony |
|
||||
| 16 human | 2→3 | `289688 → 532369` | `239785 + 2896 interest`, again post-growth |
|
||||
|
||||
So `ComputeBudget` runs on the colony state **after** `S11 SystemTurn` — the strategic
|
||||
driver runs the system turn at index 11 and the player driver at index 13 — and the
|
||||
human's system income is the post-growth number.
|
||||
|
||||
5. **What will therefore close, and what will not:**
|
||||
* Player 576's income does **not** move with growth (Koa'Vo has no civilian population and
|
||||
its `BnkEl` is identical in all three saves), so its `Sav` should come out **exactly**
|
||||
right on both pairs. **+1 leaf per pair.**
|
||||
* Player 16's income **does** move with growth, and our `S11` does not commit civilian
|
||||
growth (it needs the carrying capacity). Our number will be short by **597** on pair 1
|
||||
and **596** on pair 2, so `Sav` stays wrong — but it was already wrong, so **0 regressed**.
|
||||
* Player 32 (the AI) picks a research target and changes `ResRate` during the turn; that is
|
||||
Rung B. Not closed.
|
||||
* The 24 `turnstats` leaves need the budget **and** ship construction (lane E2's finding).
|
||||
Not closed by this lane alone.
|
||||
* `PvSav` is `S00`'s snapshot, not `P02`'s write, and player 32's `PvSav` is not its
|
||||
pre-turn `Sav`, so it is left alone.
|
||||
|
||||
**Predicted total: pair 1 closed 1 regressed 0; pair 2 closed 1 regressed 0.**
|
||||
6. The two research RNG words stay out of the generator model, and for a **new** reason: the
|
||||
allocation is now computable, but no player in either input state carries a research
|
||||
target (`ResTNm` is empty on 16 and 32 in `turn1-state`), so `ProcessResearch` has nothing
|
||||
to fund. The blocker moves from "the money is unmodelled" to "the AI's target selection
|
||||
is unmodelled".
|
||||
|
||||
### 4.2 Falsification — how this could be wrong, and the symptom of each
|
||||
|
||||
| way it could be wrong | symptom |
|
||||
|---|---|
|
||||
| `OutputRates+4` is not construction but something else, so `SRsc == 1` is not what the leftover branch tests | player 576 (whose rates are `.25/.25/.25/.25`, the only four-way vector) would miss while the `{0,1}` colonies matched, or vice versa |
|
||||
| the leftover weights are the **raw** sliders, not the normalised ones | 576 misses by a clean ratio (`.25` vs `.75` denominators); the `{t,1-t}` colonies are unaffected because the ratio is the same |
|
||||
| `Ltrade` is *not* added to the money channel (i.e. I misread the slot reuse of `[ebp-0x28]`) | every colony's money collapses to `round(T*SRt)`, so the `{t 0, sc 1}` colonies earn **zero** and player 32's and the turn-2/3 colonies' income falls by 100 % — a very loud failure |
|
||||
| the budget runs **before** `S11`, and the human's +597 has another source | player 576, whose income does not move with growth, would still match, so this is NOT separable on 576 — it is separable only on 16, where a pre-growth budget predicts `289092` and a post-growth one `289688` |
|
||||
| the repair pass consumes points on a corpus colony | player 576 (Koa'Vo has one fleet in every save) misses low by the repair spend; 16's homeworld has no fleet in `turn1-state`, so a 576-only miss names this |
|
||||
| the round robin is not equivalent to the `min` | only visible where `points < need`; unexercised here |
|
||||
| `ConstructionPoints` truncates where I read a round | off-by-one on colonies whose `T*SRsc` is not integral — but `T` is already an integer and the sliders are exact binary fractions on most colonies, so this is nearly invisible; `{.04, .96}` (Downbelow) is the one colony that would show it, and it is not on either reference pair |
|
||||
| `sys->[0x230]` gates something that feeds money | it cannot: `out[3]` is written before that test and never again |
|
||||
|
||||
A run that closes player 576 and nothing else is a **weak** result and will be reported as
|
||||
one: it is one leaf per pair, and it is the leaf where the missing upstream input happens not
|
||||
to matter.
|
||||
|
||||
---
|
||||
|
||||
## 5. The result
|
||||
|
||||
Engine: `sim::ComputeSystemOutput` (`src/game/sim/colony.{h,cpp}`) plus
|
||||
`sim::IdealSuitability` and `sim::RepairShipsInOrbit`, wired into `P01`/`P02` through
|
||||
`BuildBudgetFeeds` in `src/app/turn.cpp`. Host `ctest` 46/46, clean-room OK, both run as
|
||||
separate commands.
|
||||
|
||||
```
|
||||
reference pair turn1-state -> turn2-state 209 -> 157 closed 52 regressed 0 (was 51 / 0)
|
||||
second pair turn2-state -> turn3-state 108 -> 86 closed 22 regressed 0 (was 21 / 0)
|
||||
```
|
||||
|
||||
**Closed 1 per pair, regressed 0 per pair.** The leaf is
|
||||
`/Sim/players/Player[576 "Independent Colony"]/Sav` on both.
|
||||
|
||||
### 5.1 Every prediction in §4.1, checked
|
||||
|
||||
| predicted | outcome | verdict |
|
||||
|---|---|---|
|
||||
| the leftover redistribution collapses to "all to trade" on every corpus colony | it does; `leftoverToTrade == leftover` on all of them | HELD |
|
||||
| the turn path equals the projected path to within the trade-point rounding when the queue is empty | **exactly equal on 5 of the 11 saves** — every save whose build queues are all empty | HELD |
|
||||
| player 576's `Sav` comes out exactly right on both pairs | `0 -> 98871` and `98871 -> 198730`, both exact | HELD |
|
||||
| player 16's `Sav` is short by the uncommitted civilian growth | `289092` against the oracle's `289688` | HELD (the amount was predicted as 597 and is **596** — see §5.2) |
|
||||
| player 32 is not closed | `297492` against `92651` | HELD |
|
||||
| the `turnstats` leaves are not closed | not closed | HELD |
|
||||
| pair 1 closed 1 regressed 0; pair 2 closed 1 regressed 0 | exactly that | HELD |
|
||||
| the two research RNG words stay out of the generator model | they do, for the predicted reason | HELD (§5.5) |
|
||||
|
||||
Nothing in §4.2's falsification table fired. In particular the loudest one — "the construction
|
||||
leftover does not reach money" — would have collapsed every `{SRt 0, SRsc 1}` colony's income
|
||||
to zero, and those colonies price correctly.
|
||||
|
||||
### 5.2 The prediction that was off, and why
|
||||
|
||||
§4.1 said player 16 would be short by **597** on pair 1. It is short by **596**. The
|
||||
arithmetic: our pre-growth output gives a max income of 238,592 and the post-growth one gives
|
||||
239,189, a gap of 597 — but the oracle's own turn-path number is 239,188, one *below* its
|
||||
projected 239,189, because `2 x round(T/2)` lost a point on the post-growth total and did not
|
||||
on the pre-growth one. So the two errors partly cancel, and the residual is the growth gap
|
||||
minus one trade point. The mechanism was predicted; the sign of its interaction was not.
|
||||
|
||||
### 5.3 The strongest check available without a VM
|
||||
|
||||
The engine now runs every owned system through **both** paths on the same colony state and
|
||||
reports the difference, every run. The projected sum is the number the save's own `BnkEl`
|
||||
states and that lane E1 scored 25/25 against, so a large delta is the new model failing and it
|
||||
is visible on load.
|
||||
|
||||
| save | landed players agreeing exactly | worst \|delta\| | explanation |
|
||||
|---|---|---|---|
|
||||
| `turn1-state`, `turn2-state`, `turn3-state` | 3 of 3 | 0 | every build queue empty |
|
||||
| `human-turn2-orders` | 2 of 2 | 0 | every build queue empty |
|
||||
| `zuul-turn15-orders` | 2 of 2 | 0 | every build queue empty |
|
||||
| `human-turn3-noderoute` | 1 of 2 | **37** | queues empty; one trade point crossed a five-point block: `5 money x 5 x IncMod 1.48 = 37` |
|
||||
| `zuul-turn23-fleet23` | 1 of 2 | **2035** | Gallandro's queue holds 370 points: `370 x 5 x 1.1 = 2035` |
|
||||
| `zuul-turn17-orders2`, `-rollpending` | 0 of 2 | **21010** | W'kanda's queue holds 3818: `3820 x 5 x 1.1 = 21010` (the block count moves by 3820, not 3818, because of the two `mod 5` offsets) |
|
||||
| `zuul-turn16-noderoute` | 0 of 2 | **36438** | W'kanda's queue holds 6626: `6625 x 5 x 1.1 = 36437.5`, and two independent truncations put it at 36438 |
|
||||
| `zuul-turn5-species5` | 1 of 2 | **39160** | W'kanda's queue holds 7120: `7120 x 5 x 1.1 = 39160` |
|
||||
|
||||
**Every delta decomposes, to the unit, into the build queue's construction points priced
|
||||
through the money chain** — three of the four exactly, and the fourth within the one unit that
|
||||
two independent truncations of a `.5` allow. That is the closest thing to an oracle this path
|
||||
has, and it is a much stronger statement than "the two agree": it says the two paths differ
|
||||
by *exactly the thing the disassembly says they differ by*.
|
||||
|
||||
### 5.4 What this run did NOT cover — read this before quoting the numbers
|
||||
|
||||
* **One leaf per pair is a weak result** and it is the one leaf where the missing upstream
|
||||
input does not bite. Player 576 owns one colony with no civilian population and an empty
|
||||
queue; it is the easy case.
|
||||
* **The ship-repair demand is taken as 0 everywhere.** `Ship::RepairCost` (0x00815180) is
|
||||
unread, so no save can supply it. Every colony with a damaged fleet in orbit therefore reads
|
||||
**high**, and the corpus cannot tell us whether any does.
|
||||
* **No corpus colony is off its ideal suitability, and none is below full infrastructure.** So
|
||||
the terraform channel, the infrastructure channel, `infraNeed`, `terraNeed`, the
|
||||
infra→terraform cascade, the terraform→money cascade and the non-trivial half of the
|
||||
`SRsc == 1` leftover branch are **all unexercised by the corpus**. They are covered by host
|
||||
tests written from the instruction stream, which is a much weaker claim.
|
||||
* **No corpus system has a station**, so the shipyard bonus in `ConstructionPoints` — including
|
||||
the "strictly positive" guard and the association this lane corrected — is unexercised.
|
||||
* **`SRoh` is 0 on every save**, so the over-harvest term is unexercised here too (lane N's
|
||||
finding, unchanged).
|
||||
* **`dsu` is FLT_MAX everywhere**, so the ideal-suitability override is unexercised and the
|
||||
sentinel's identity is inferred from that fact rather than read.
|
||||
* **The build queue is exercised only on Zuul saves that are not on either reference pair.**
|
||||
The two pairs the metric is measured on have empty queues throughout, so the queue edge is
|
||||
checked by the §5.3 self-check and not by the leaf count.
|
||||
* The `[0x230]` gate, `out[4]`, `out[5]` and `out[6]` are read but **not modelled**; nothing
|
||||
downstream of `out[3]` needs them.
|
||||
* **Neither `NormaliseOutputRates`' tail (past 0x007474d4) nor `CivilianConsumption`'s ratio
|
||||
was read this lane.** The first is B4's; the second feeds only the resource ledger.
|
||||
|
||||
### 5.5 The two budget-downstream RNG words
|
||||
|
||||
They still cannot enter the generator model, and the reason has changed again.
|
||||
|
||||
`income-term.md` §6.3 named them: `ProcessResearch`'s `RNG::Chance(odds)` on the funded node,
|
||||
and the draw inside `OnTechResearched`'s effect callback. Both are downstream of the research
|
||||
**allocation**, which is `ComputeBudget`'s research money — and that money is now computable,
|
||||
because its only missing input was the per-system money this lane closed.
|
||||
|
||||
What blocks them now is one step further out: **the only corpus player that reaches
|
||||
`ProcessResearch` with a research target is the AI**, and its research rate (`ResRate`
|
||||
0.25 → 0.8) and its target (`ResTNm` '' → 'IND_Waldo') are *its own orders, generated during
|
||||
the same turn*. Feeding our budget into `ProcessResearch` would allocate against the wrong
|
||||
rate. On `turn1-state` no player has a target at all, so the phase has nothing to fund and
|
||||
both words cost zero on the reference pair.
|
||||
|
||||
That last sentence is a claim about the **workload**, not about the phase, and rule 20 applies
|
||||
to it: a word count cannot separate "`ProcessResearch` was not entered" from "it was entered
|
||||
and drew nothing". An entry probe on `ProcessResearch` would settle it in minutes and is the
|
||||
cheap next step. Until then the two words are best described as *unblocked in formula, blocked
|
||||
in workload*, and the honest ledger entry is that the generator model is unchanged at 16 words
|
||||
modelled.
|
||||
|
||||
### 5.6 What is now blocked on what
|
||||
|
||||
| phase | was blocked on | is blocked on |
|
||||
|---|---|---|
|
||||
| P01 `ComputeBudget` | the per-system money output | nothing here; `S11`'s uncommitted civilian growth upstream, and the unread ship-repair demand |
|
||||
| P02 `ApplyNetToSavings` | P01 | committed; exact where the upstream is |
|
||||
| P03 `RecordBudgetDerivedFields` | the budget | two player words that are still not identified on the wire — **unchanged, this lane does not help it** |
|
||||
| P05 `ProcessResearch` | the allocation | the AI's own research rate and target (Rung B) |
|
||||
| P06 `ResearchRefund` | P01 and P05 | P05 |
|
||||
| T36 turn record | the budget **and** ship construction | ship construction (lane B6) plus the same growth gap; the money half is now available |
|
||||
|
||||
The single highest-value next item this lane can name is **committing `S11`'s civilian
|
||||
growth**: it is what stands between player 16's `Sav` and the oracle on both pairs, it is also
|
||||
what stands between `BnkEl`/`BnkPr` and the oracle (four more leaves), and it is the same
|
||||
`Pop2/PopG/PopC` and `PvPop2/PopG/PopC` leaves that are already on the remaining list. One
|
||||
input — the imperial carrying capacity — gates all of it.
|
||||
|
||||
|
|
@ -513,6 +513,19 @@ words are copied in. Nothing in `StrategyServer::ProcessTurn` itself is diplomac
|
|||
diplomacy upkeep" (0x0086b300 / 0x007adc80) is **trade** (§1.4).
|
||||
|
||||
### 5.2 `DiplomacyStats` (vector at `+0x230`, one entry per other player, 0x24 B)
|
||||
|
||||
> **CORRECTED by lane T2, 2026-09-08 — two of this paragraph's three facts.** The caller and the location are
|
||||
> right; the relation codes and the stamped value are not. (a) `GetRelation` 0x0080e050 forwards to the cdecl
|
||||
> 0x006d2050, which reads **`3 = ally` (and self), `2 = NAP`, `1 = cease-fire`, `0 = war`** — 1 and 3 the
|
||||
> other way round from the line below, which labelled itself MEDIUM confidence on exactly this mapping.
|
||||
> `combat-done-tail.md` §2A agrees independently (`ResupplyAlliedFleets` is gated on `GetRelation == 3`).
|
||||
> (b) The stamped value is **`S+0xc`, which lane Z named `Frame` — the turn — not `ModCount`**: the
|
||||
> instruction is `mov eax,[edi+0xc]` with `edi = S`, and across the corpus every stamp equals the save's own
|
||||
> `Frame` on ten of eleven saves while `ModCount` runs an order of magnitude higher (0/12/24 on the
|
||||
> reference family). Also: the entry is created on demand, a fresh one starts its three `last*` at **-1**,
|
||||
> and the relation's bit is the **`PlyrIdx` field**, not the position in the player vector.
|
||||
> See `findings/subsystems/treaty-turn-stamp.md`. Modelled in `sots-engine` as host phase H02.
|
||||
|
||||
Updated by 0x007898c0 right after the alliance diff (called from `ApplyTurnCommands`):
|
||||
* for every pair with a current relation (0x0080e050: 1 ally, 2 NAP, 3 cease-fire) the matching `last*` int16
|
||||
(`lastally` @+0x10, `lastnap` @+8, `lastcf` @+0x18) is set to the current `ModCount`;
|
||||
|
|
|
|||
350
findings/subsystems/treaty-turn-stamp.md
Normal file
350
findings/subsystems/treaty-turn-stamp.md
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
# The treaty-turn stamp, and where `Player.Status` really comes from
|
||||
|
||||
Lane T2, 2026-09-08. Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs.
|
||||
|
||||
**Method.** Every claim about control flow below is read from the instruction stream with
|
||||
`objdump -b binary -m i386 -M intel` over the raw image (PE section table mapping, call targets resolved
|
||||
against `dumps/functions.json`, callers found with a whole-image byte scan for `E8/E9 rel32`). Nothing here
|
||||
comes from the decompiler. Where a claim is an inference from save bytes rather than from instructions it
|
||||
says so in the sentence that makes it.
|
||||
|
||||
This lane was briefed on `OnAllCombatDone_Tail` and went looking for the tail phase with the best
|
||||
leaves-closed-per-effort. **The answer was not in the tail.** §5 says why, and lists what the tail's own
|
||||
phases are actually blocked on. §1–§3 are what the search found instead.
|
||||
|
||||
---
|
||||
|
||||
## 0. The prediction, written before the run
|
||||
|
||||
Two models, both derived statically, both with a falsification section. Recorded here **before** anything
|
||||
was built or measured, per rule 2.
|
||||
|
||||
### 0.1 H02 `StampTreatyTurns`
|
||||
|
||||
> Every turn, for every ordered pair of players `(A, B)` with a live treaty, `A`'s `DiplomacyStats` entry
|
||||
> for `B` has the matching `last*` field stamped with the **current turn** (`Frame`), the entry being
|
||||
> created on demand. Concretely, per player `A` in player-vector order, per player `B` in player-vector
|
||||
> order, `B != A`:
|
||||
>
|
||||
> ```
|
||||
> rel = 3 if A.plyrIdx == B.plyrIdx else
|
||||
> 3 if (A.AL & (1 << B.plyrIdx)) else
|
||||
> 2 if (A.NA & (1 << B.plyrIdx)) else
|
||||
> 1 if (A.CF & (1 << B.plyrIdx)) else 0
|
||||
> if rel == 0: continue
|
||||
> e = the first entry of A.dipstats with e.other == B.PlayerID,
|
||||
> else a NEW entry appended at the end with
|
||||
> {other = B.PlayerID, lastnap = lastally = lastcf = -1, every other field 0}
|
||||
> e.lastally = Frame if rel == 3
|
||||
> e.lastnap = Frame if rel == 2
|
||||
> e.lastcf = Frame if rel == 1
|
||||
> ```
|
||||
>
|
||||
> **Predicted leaves closed: 26 on the reference pair (turn1 -> turn2), 14 on pair 2 (turn2 -> turn3).
|
||||
> Predicted regressed: 0. Predicted RNG words: 0.**
|
||||
|
||||
The 26 are six players gaining a `dipstats` vector (2 entries for each of the two `Singularity` players,
|
||||
4 rows each — the container leaf plus its entries — and 4 entries each for the four species-4 players,
|
||||
5 rows each). The 14 on pair 2 are `lastnap`/`lastally` moving 2 -> 3 on the fourteen entries that already
|
||||
exist.
|
||||
|
||||
**How this could be wrong, and the symptom of each way.**
|
||||
|
||||
1. *The stamp is not `Frame`.* `strategic-turn-internals.md` §5.2 says it is `ModCount`. If §5.2 is right
|
||||
and I am wrong, the stamped value is 12 (pair 1) or 24 (pair 2), not 2 or 3 — **14 regressed leaves on
|
||||
pair 2 and 26 wrong values on pair 1**, and the two are trivially distinguishable because `ModCount` and
|
||||
`Frame` differ by an order of magnitude on both pairs.
|
||||
2. *The relation mapping is inverted.* If `1 = ally, 2 = NAP, 3 = ceasefire` (also §5.2) rather than
|
||||
`3 = ally, 2 = NAP, 1 = ceasefire`, the value lands in `lastcf` where the oracle has `lastally`:
|
||||
**pair 1 closes ~12 of 26 and regresses none** (the created entries are still created, with the right
|
||||
`other`, but two of their thirteen counters are wrong), and pair 2 **closes 0 and regresses 14**.
|
||||
3. *The shift is by vector position, not `plyrIdx`.* Undetectable in this corpus — every save has
|
||||
`plyrIdx == vector position`. The symptom would appear only on a save where a player was removed. Stated
|
||||
as a limitation, not tested.
|
||||
4. *Entries are ordered by something other than the stamping order.* Then the created vectors have the right
|
||||
contents in the wrong order and **pair 1 closes the container leaf and regresses the entry leaves**.
|
||||
5. *The phase is somewhere else in the turn and sees a different `Frame`.* If `Frame` were bumped after this
|
||||
ran, the stamp would be `Frame - 1` — 1 and 2 instead of 2 and 3 — i.e. **0 closed, 26 wrong on pair 1**.
|
||||
6. *Some other writer also touches these fields on a normal turn.* Then the closed count is short of 26/14
|
||||
by whatever that writer moves, and the residual is named rather than netted.
|
||||
|
||||
### 0.2 `Player.Status` — evaluated, not written
|
||||
|
||||
> `Status` is set to **4** by `FUN_00821a40` (§3), which runs on the **End Turn submission** path, before
|
||||
> the turn is processed — not between tail phase 31 and the autosave, which is where `backlog.md` item 6
|
||||
> looks for it. Which players get it is a property of who submits an End Turn, and the only two saves in
|
||||
> the corpus where the field is non-zero agree with `Species != 4`.
|
||||
>
|
||||
> **Predicted leaves: 4 on the reference pair, 0 on pair 2 — and NOT written**, because the gate is a
|
||||
> two-observation inference and the campaign's own rule 20 says a constant fitted to two observations is not
|
||||
> a constant.
|
||||
|
||||
---
|
||||
|
||||
## 1. `StampTreatyTurns` — 0x007898c0, and it is not in the tail
|
||||
|
||||
**One caller, whole-image byte scan for `E8/E9 rel32`:** `ApplyTurnCommands` 0x007b18b0 at **0x007b2461**
|
||||
(+0xbb1), which is `strategic-turn-internals.md` §5.2's "right after the alliance diff". So the phase runs
|
||||
**before** either turn driver, in the host's command-application step — the same bracket
|
||||
`phase_catalog.h` already calls `Driver::Host`.
|
||||
|
||||
`void __thiscall (StrategyServer* S, vector<uint>* allianceBroken, vector<uint>* napBroken,
|
||||
vector<uint>* cfBroken)`, `ret 0xc`, 700 B. `this` is the **`S` frame** — `[edi+0x54]`/`[edi+0x58]` is the
|
||||
`vector<ServerPlayer*> Players` and `[edi+0xc]` is `Frame`, both `S`-frame offsets lane T and lane Z
|
||||
established.
|
||||
|
||||
### 1.1 Pass A, the stamp — 0x00789920..0x007899c4, read as instructions
|
||||
|
||||
```
|
||||
007898fc mov eax,[edi+0x58] ; Players._Mylast
|
||||
007899 02 mov ecx,[edi+0x54] ; ... _Myfirst
|
||||
sar eax,2 ; nPlayers
|
||||
loop i:
|
||||
007d9920 mov ebx,[ecx+esi*4] ; A = Players[i]
|
||||
loop j:
|
||||
007d9930 mov esi,[ecx+edx*4] ; B = Players[j]
|
||||
007d9933 cmp ebx,esi
|
||||
007d9935 je 0x78999a ; A == B -> skip
|
||||
007d9937 push esi ; mov ecx,ebx
|
||||
007d993a call 0x80e050 ; rel = A->GetRelation(B)
|
||||
007d993f dec eax ; je 0x78995a ; rel == 1 -> slot 8
|
||||
007d9942 dec eax ; je 0x789951 ; rel == 2 -> slot 0
|
||||
007d9945 dec eax ; jne 0x78999a ; rel != 3 -> skip
|
||||
007d9948 mov [ebp-0x5c],4 ; rel == 3 -> slot 4
|
||||
007d9961 lea ecx,[ebp-0x58] ; call 0x80e7b0 ; DiplomacyStats ctor on the stack
|
||||
007d9969 push esi ; lea eax,[ebp-0x58] ; push eax
|
||||
mov ecx,ebx ; call 0x8180e0 ; A->GetDipStat(&local, B)
|
||||
007d997c mov eax,[edi+0xc] ; Frame
|
||||
007d9986 mov WORD PTR [ebp+ecx*2-0x50],ax ; local.<slot> = (short)Frame
|
||||
007d998e push esi ; lea edx,[ebp-0x58] ; push edx
|
||||
mov ecx,ebx ; call 0x863950 ; A->SetDipStat(&local, B)
|
||||
```
|
||||
|
||||
The stamped field is at `local + 8 + slot*2`, because the ctor's `this` is `[ebp-0x58]` and the store's base
|
||||
is `[ebp-0x50]`. Against `objects/layouts.md`'s `Game::DiplomacyStats` (`+8 lastnap`, `+0x10 lastally`,
|
||||
`+0x18 lastcf`), that gives:
|
||||
|
||||
| `GetRelation` | slot | field |
|
||||
|---|---|---|
|
||||
| 1 | 8 | `lastcf` |
|
||||
| 2 | 0 | `lastnap` |
|
||||
| 3 | 4 | `lastally` |
|
||||
|
||||
### 1.2 `GetRelation` 0x0080e050 — 33 bytes, fully resolved
|
||||
|
||||
```c
|
||||
// 0x0080e050: thiscall, ret 4 -> tail-calls the cdecl 0x006d2050 with
|
||||
// (this->PlyrIdx(+0x28), &this->Alliances(+0x168), other->PlyrIdx(+0x28))
|
||||
int Relation(int myIdx, PlayerAlliances* a, int otherIdx) { // 0x006d2050, 58 bytes
|
||||
if (myIdx == otherIdx) return 3;
|
||||
uint32_t bit = 1u << otherIdx; // shl edx,cl -- x86 masks the count to 5 bits
|
||||
if (a->AL(+0x04) & bit) return 3;
|
||||
if (a->NA(+0x08) & bit) return 2;
|
||||
return (a->CF(+0x0c) & bit) ? 1 : 0;
|
||||
}
|
||||
```
|
||||
|
||||
**So the relation codes are `3 = ally (and self)`, `2 = NAP`, `1 = ceasefire`, `0 = war`.**
|
||||
`strategic-turn-internals.md` §5.2 has 1 and 3 the wrong way round; it labelled itself MEDIUM confidence on
|
||||
exactly this mapping, and it was the half that was wrong. The corrected mapping is independently confirmed by
|
||||
`combat-done-tail.md` §2A, which reads `Node::ResupplyAlliedFleets` as gated on `GetRelation == 3 (allied/self)`
|
||||
and `UpdateDiplomacyStatsFromCombat` as gated on `GetRelation < 1` (war).
|
||||
|
||||
Two further notes on this function, both places a reimplementation goes quietly wrong:
|
||||
|
||||
* The shift count is the **`PlyrIdx` field** (`ServerPlayer+0x28`), *not* the player's position in the player
|
||||
vector. This is the exact opposite of the alliance/shared-vision mask (`app/alliance.h` §1), which uses the
|
||||
vector position and never loads the index field. Both are in the same subsystem and they disagree. No save
|
||||
in the corpus separates them — `plyrIdx == vector position` on all eleven — so this is an
|
||||
instruction-stream reading with no behavioural evidence behind it.
|
||||
* `AL` is consulted **unconditionally**. It is *not* gated on `ALid != -1` the way the shared-vision mask's
|
||||
alliance term is.
|
||||
|
||||
### 1.3 The three helpers
|
||||
|
||||
**`DiplomacyStats::DiplomacyStats()` 0x0080e7b0** (55 B): vptr `0x00a21430`, everything zeroed, then
|
||||
`lastcf(+0x18) = lastnap(+8) = lastally(+0x10) = -1`. So a fresh entry is
|
||||
`{other 0, lastnap -1, lastnapbty 0, bknnap 0, btynap 0, lastally -1, lastallybty 0, bknally 0, btyally 0,
|
||||
lastcf -1, lastcfbty 0, bkncf 0, btycf 0, deadhome 0}`.
|
||||
|
||||
**`ServerPlayer::GetDipStat(out, other)` 0x008180e0** (294 B, `ret 8`): if `out == 0` return; re-initialise
|
||||
`*out` to those defaults; `out->other = other->+0x4` (the handle id — the wire's `PlayerID`); if `other == 0`
|
||||
return; then a **linear, first-match** scan of the `0x24`-stride vector at `ServerPlayer+0x230` for
|
||||
`entry.other == GetId(other)` and, on a hit, copy the entry's thirteen `int16` fields into `out`.
|
||||
|
||||
**`ServerPlayer::SetDipStat(src, other)` 0x00863950** (328 B, `ret 8`): if `src == 0 || other == 0` return;
|
||||
the same linear first-match scan; **on a miss, default-construct and `push_back`**, then set
|
||||
`back().other = other->+0x4`; finally copy `src`'s thirteen `int16` fields into the entry and re-write
|
||||
`other`. So a new entry is appended **at the end**, and the append order is the order in which pairs are
|
||||
first stamped — i.e. the `(i, j)` double-loop order, which is player-vector order.
|
||||
|
||||
The `0x24` stride is read twice, once in each helper, from `0x38e38e39 / sar 3` over
|
||||
`[p+0x234] - [p+0x230]`. That is `objects/layouts.md`'s `dipstats` vector at `ServerPlayer+0x230` and
|
||||
`sizeof(Game::DiplomacyStats) == 0x24`, agreeing with the serializer's own enumeration.
|
||||
|
||||
### 1.4 The value is `Frame`, not `ModCount`
|
||||
|
||||
`mov eax,[edi+0xc]` with `edi = S`, and lane Z's `StrategyServer::Write` tags `S+0x8` as `ModCount` and
|
||||
`S+0xc` as `Frame`. The save bytes settle it independently and are worth stating because they are the
|
||||
cheaper check: across `turn1/2/3-state.sav` `ModCount` runs 0 -> 12 -> 24 while `Frame` runs 1 -> 2 -> 3, and
|
||||
every stamped `last*` in the corpus is 2 or 3. **`strategic-turn-internals.md` §5.2's "set to the current
|
||||
`ModCount`" is wrong**; it is the turn.
|
||||
|
||||
`Frame` is already the *new* turn when this runs: `StrategyServer::BeginProcessTurn` 0x007d990a does
|
||||
`inc [esi+0xc]` and the host order is `BeginProcessTurn -> ApplyTurnCommands -> ... -> ProcessTurn`.
|
||||
|
||||
### 1.5 Pass B is the betrayal counter, and it needs a command stream
|
||||
|
||||
0x007899ca onward is a second double loop, gated on `GetRelation(A,B) <= 0` (`jg` skips), that indexes the
|
||||
three `vector<uint32>` arguments by the inner loop index — with a size check against `nPlayers` that
|
||||
substitutes a zero local when the vector is the wrong length. Those three vectors are the alliance-broken /
|
||||
NAP-broken / CF-broken masks the alliance diff in `ApplyTurnCommands` builds from the turn's `SNMUpdate`
|
||||
commands. **With no commands they are empty, the size check fails, the zero local is used, and pass B is a
|
||||
no-op.** It is not modelled: the standalone has no command stream, and a betrayal cannot occur without one.
|
||||
|
||||
---
|
||||
|
||||
## 2. What the save bytes say, and they say the same thing
|
||||
|
||||
`dipstats` across the reference family, every non-zero field shown:
|
||||
|
||||
| save | `Frame` | `ModCount` | P496 | P512 | P528 / P544 / P560 / P576 |
|
||||
|---|---|---|---|---|---|
|
||||
| turn1-state | 1 | 0 | — | — | — (all six vectors empty) |
|
||||
| turn2-state | 2 | 12 | `{other 512, lastally 2}` | `{other 496, lastally 2}` | three entries each, `lastnap 2`, `other` = the other three in vector order |
|
||||
| turn3-state | 3 | 24 | `lastally 3` | `lastally 3` | `lastnap 3` |
|
||||
|
||||
Every created entry carries `lastnap = -1, lastcf = -1` beside the stamped `lastally`, or `lastally = -1,
|
||||
lastcf = -1` beside the stamped `lastnap` — which is the 0x0080e7b0 constructor, byte for byte. The two
|
||||
`Singularity` players are allied to each other (relation 3); the four species-4 players are mutually
|
||||
non-aggressive (relation 2); the two real empires have no treaty with anyone and their vectors stay empty on
|
||||
all three turns. Nothing else in the vector ever moves in this corpus, so `lastnapbty`, `bkn*`, `bty*` and
|
||||
`deadhome` are **unexercised** and are hypotheses, per rule 6.
|
||||
|
||||
---
|
||||
|
||||
## 3. `Player.Status` — the writer, and a correction to the roadmap
|
||||
|
||||
`Status` is `ServerPlayer+0x164` (`objects/layouts.json` grades it `off_abs 356`). A whole-image scan for
|
||||
`C7 8x 64 01 00 00 imm32` — a `mov dword [reg+0x164], imm` — finds every immediate store to that
|
||||
displacement in the image. Three matter:
|
||||
|
||||
| VA | value | containing function |
|
||||
|---|---|---|
|
||||
| 0x007dcc8a | **1** | `ProcessTurn` +0x5ca, inside a `0x44`-stride loop (the encounter-member stride), gated on two bytes at `member+0xf9`/`+0xfa` |
|
||||
| 0x00821a6b | **4** | `FUN_00821a40`, 60 B |
|
||||
| 0x007ddd41 | **0** | `ResumePlaying` 0x007ddc90 +0xb1 |
|
||||
|
||||
`FUN_00821a40(this, playerId)`, `ret 4`, in full:
|
||||
|
||||
```
|
||||
p = HandleMap::Resolve(this + 0x80, playerId) ; sub ecx,0xffffff80 is add ecx,0x80
|
||||
if (!p) { Log(2, <0x00a2fb30>, playerId); return false; }
|
||||
p->Status(+0x164) = 4
|
||||
return true
|
||||
```
|
||||
|
||||
Its three callers, from the same byte scan: `EndTurn` 0x00783be0 (+0x70), `EndTurnForced` 0x00783d30
|
||||
(+0x7b), `OnPlayerEndTurn` 0x007d9af0 (+0x35). In `EndTurn` the id passed is `this->+0x148`, the client's own
|
||||
player id, and the call is immediately followed by `[this+0x15c] = 1` and `[this+0x4c8] = 1` and a
|
||||
`RaiseEvent(0x21)` — this is the **End Turn submission** path, which runs before the turn is processed.
|
||||
|
||||
**This corrects `backlog.md` item 6.** The roadmap has `Player.Status` blocked on "the writer between tail
|
||||
31 and the autosave — watchpoint". There is no such writer. `Status = 4` is set when a player submits an End
|
||||
Turn; `ProcessTurn`'s encounter loop can set 1; `ResumePlaying` sets 0 on load, which is exactly the
|
||||
"`Status` resetting 4 -> 0 on load" that `determinism-oracle.md` recorded. Item 6 does not need a VM
|
||||
watchpoint; it needs one predicate — *which players submit an End Turn* — and that is a question about the
|
||||
client/host controller layer, not about the turn.
|
||||
|
||||
What the corpus can say about that predicate, and it is not much: of the eleven saves, **only
|
||||
`turn2-state.sav` and `turn3-state.sav` carry a non-zero `Status` at all** (every other save was written
|
||||
through a load, and `ResumePlaying` had zeroed it). On those two, the four players with `Status = 4` are
|
||||
exactly the four with `Species != 4`, and the four with `Status = 0` are exactly the four species-4
|
||||
monster/neutral players. That is 8 observations on 2 saves of a 1-bit predicate, on a field that no other
|
||||
save exercises. Rule 20 applies. **Evaluated, reported, not written.**
|
||||
|
||||
The cheap way to settle it is not a watchpoint either: an entry probe on 0x00821a40 recording its `playerId`
|
||||
argument over one End Turn names the set directly, and a probe on `OnPlayerEndTurn` separates "the host ends
|
||||
the AI's turn" from "the AI player never ends a turn".
|
||||
|
||||
---
|
||||
|
||||
## 3A. The measurement
|
||||
|
||||
`sots-engine` host phase **H02 `StampTreatyTurns`** (`src/app/treaty.{h,cpp}`), run through
|
||||
`tools/standalone_report.py`. `closed` and `regressed` are separate columns and are never netted. The
|
||||
"before" column is `main` at `0f1c007` **rebuilt on the same host** — the checked-in `build-host` was stale
|
||||
and reported `closed 5` on the reference pair, which would have made this table look four times better than
|
||||
it is.
|
||||
|
||||
| pair | baseline | before | after | closed | regressed |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| `turn1-state -> turn2-state` (the reference End Turn) | 209 | 158 | **132** | 51 -> **77** | **0** |
|
||||
| `turn2-state -> turn3-state` (real End Turn) | 108 | 87 | **73** | 21 -> **35** | **0** |
|
||||
| `human-turn2-orders -> human-turn3-noderoute` | 375 | 311 | **299** | 64 -> **76** | **0** |
|
||||
| `zuul-turn15-orders -> zuul-turn16-noderoute` | 282 | 264 | **252** | 18 -> **30** | **0** |
|
||||
| `zuul-turn16-noderoute -> zuul-turn17-orders2` | 345 | 328 | **316** | 17 -> **29** | **0** |
|
||||
|
||||
**+26 and +14 on the two reference pairs, exactly the predicted numbers, and +12 on each of three pairs the
|
||||
model was never fitted to** — a different game, species 5, turns 2, 15 and 16. Twelve is precisely the number
|
||||
of ordered treaty pairs each of those saves holds. Total **76 leaves closed, 0 regressed**, 0 RNG words.
|
||||
|
||||
`zuul-turn17-orders2 -> zuul-turn23-fleet23` is a six-turn gap rather than a pair; it reports `closed 0,
|
||||
regressed 1` **both before and after**, so that one regression is not this phase's and is untouched by it.
|
||||
|
||||
The measurement was taken on the WSL host with a WSL-built `sots_turn`, and the `before` column was taken
|
||||
with the same tool on the same host from a freshly rebuilt `main`. `verify/results/standalone/report.txt`
|
||||
and `status.json` are **deliberately not committed by this lane**: lane C3 published its own numbers into
|
||||
those two files minutes before this run, and regenerating them from a branch that does not carry C3's work
|
||||
would silently replace a better result with a worse one. They are the integrator's to regenerate after the
|
||||
merge.
|
||||
|
||||
All eleven corpus saves run clean end to end. On ten of them the phase creates **zero** entries and only
|
||||
re-stamps, which is the check that matters: the entry set the rule derives is already the entry set the
|
||||
game wrote. Only `turn1-state.sav` creates any (14), and that is the save on which no turn has ever run.
|
||||
|
||||
**What this did not cover, stated as loudly as the divergences.** The cease-fire arm has never executed —
|
||||
no save in the corpus contains a cease-fire — so `Relation::CeaseFire` and the `lastcf` store are held by a
|
||||
unit test and by the instruction stream, and by nothing else. The betrayal counters (`lastnapbty`, `bkn*`,
|
||||
`bty*`) and `deadhome` are likewise untouched by any save; pass B is not modelled at all. Nine of the eleven
|
||||
saves have exactly one relation kind (NAP) and eight players, so the corpus tests one shape hard rather than
|
||||
many shapes at all. And `plyrIdx == vector position` on every save, so the index-vs-position reading of §1.2
|
||||
has no behavioural evidence behind it whatsoever.
|
||||
|
||||
---
|
||||
|
||||
## 4. Two names that should be in Ghidra
|
||||
|
||||
| VA | proposed name | evidence |
|
||||
|---|---|---|
|
||||
| 0x007898c0 | `StrategyServer::StampTreatyTurns` | §1.1 |
|
||||
| 0x006d2050 | `PlayerAlliances::Relation` | §1.2 |
|
||||
| 0x0080e050 | `ServerPlayer::GetRelation` | §1.2 |
|
||||
| 0x0080e7b0 | `Game::DiplomacyStats::DiplomacyStats` | §1.3 |
|
||||
| 0x008180e0 | `ServerPlayer::GetDipStat` | §1.3 |
|
||||
| 0x00863950 | `ServerPlayer::SetDipStat` | §1.3 |
|
||||
| 0x00821a40 | `StrategyServer::MarkPlayerTurnEnded` | §3 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Why the answer was not in the tail
|
||||
|
||||
The lane was briefed on the post-combat tail and ranked its phases by leaves-closed-per-effort against the
|
||||
standalone's current 158-leaf residual on the reference pair. Every tail phase that moves a leaf in this
|
||||
corpus is blocked on something another lane owns, or on a blind spot:
|
||||
|
||||
| tail phase | leaves it would move (pair 1 / pair 2) | why not now |
|
||||
|---|---|---|
|
||||
| T24 maintenance + research bonus + `ShipRecs` | 9 / 5 | `Maint` moves 0 -> 500 -> 1000 in lockstep with one ship completing per turn, and every `ShipRecs` leaf is a **new** ship's record. Blocked on ship construction, which is lane B6's |
|
||||
| T31 `UpdateBankruptcyLimits` | 4 / 4 | already `Blocked` in the phase table, on `ComputeOutput`'s turn path (lane C3) |
|
||||
| T36 `FinalizeTurnRecords` | 24 / 8 | already `Blocked`, on the budget and ship construction |
|
||||
| T02 first contact, T30 comms mask | 0 / 0 | `HasEnc`/`HasDiscCl` and `ServerPlayer+0x198` move no leaf in this corpus |
|
||||
| T05 `UpdateDiplomacyStatsFromCombat` | 0 / 0 | writes `deadhome` and `bty*`; **no battle occurs on either pair** — every encounter result seen has `res->+0x4 != 0` (lane Z). A path no save exercises is a hypothesis |
|
||||
| T11 node-line decay | ? | draws RNG; the corpus turn cost is measured at 0 words from this phase, so no line expires |
|
||||
| T08 / T20 / T26 script hooks, T23 / T33 trade vtables | 8 / 2 (`SvSctOb`) | the `SVScriptObject` encounter state (`EncObj[*]/Hives`, `did`, `didc`, `ini`, `CDiff`) is the tail's tier-4 blind spot: eleven virtual slots, none identified |
|
||||
|
||||
The remaining large clusters — `TShn` (10/10) and `rcex` (6/6) — are lane E3's named residual with a
|
||||
watchpoint already specified, and `Events` (6/6) needs the whole turn's event inventory, not one phase.
|
||||
|
||||
So the tail's own ranked list is, right now, correct and unactionable: it is waiting on C3, B6 and a
|
||||
vtable sweep. The two items above are what a lane holding no VM and no data-file dependency can close today.
|
||||
93
ghidra/addresses.d/lane-c3.json
Normal file
93
ghidra/addresses.d/lane-c3.json
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"_note": "Lane C3, ComputeOutput on the turn path. Five addresses this lane read are already held elsewhere and AGREE, so they are dropped from this fragment rather than duplicated (rule 14): ServerSystem_ComputeOutput 0x00751fb0 and ServerSystem_TerraformPointsNeeded 0x00746890 (lane-e1.json), ServerSystem_ComputeOutputFromRates 0x00751bb0 and ServerSystem_NormaliseOutputRates 0x00747390 (addresses.json), Ship_TestFlags 0x00814da0 (lane-z.json). Every one was re-disassembled to the next function start this lane and the address, convention and boundary matched. One CORRECTION to a boundary that is recorded here rather than by editing another lane's row: ComputeOutputFromRates ends at its `ret 8` at 0x00751fa0 and 0x00751fa3..0x00751faf is int3 padding, so output-term.md's 0x00751fa8 is padding, not code; income-term.md's 0x00751fa0 is right. See findings/subsystems/output-turn-path.md.",
|
||||
"entries": [
|
||||
{
|
||||
"name": "ServerSystem_RepairShipsInOrbit",
|
||||
"addr": "0x00751590",
|
||||
"convention": "thiscall",
|
||||
"prototype": "int (ServerSystem* sys, int points, bool estimateOnly) // `ret 8`, real end 0x007517b5 (SEH frame, /GS cookie). Returns the points NOT consumed. Walks the system's fleets through the system's own vtable (slot 2 = count, slot 4 = element), keeps those whose +0x58 equals sys->PID and that pass 0x00813ab0(0,8), and inside each keeps the ships that pass 0x00814da0(0,8) with 0x00815180(ship,1) > 0. With estimateOnly it returns max(points - totalCost, 0) and touches nothing; without it, it distributes round-robin -- share = max(points / shipCount, 1) per pass, take = min(cost, share), applied by 0x008151c0 -- until no ship takes anything or the points run out. THE ROUND ROBIN IS EQUIVALENT TO points - min(points, totalCost): share is at least 1, so every ship with a positive cost takes at least one point per pass, and the only early exit needs every remaining cost to be zero. THIS IS THE SIDE EFFECT that makes ComputeOutputFromRates unsafe to call for its value (the B1 replace double-run defect): ComputeOutputFromRates passes estimateOnly = 0",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "ServerSystem_ConstructionPoints",
|
||||
"addr": "0x00746830",
|
||||
"convention": "thiscall",
|
||||
"prototype": "int (ServerSystem* sys, double constructionShare) // `ret 8`, real end 0x00746883. Turns the ship-construction channel's rounded share into out[7]: with no owner it is ftol(share), else k = StationCount(sys, PID, 1) and b = STATION_BONUS_SHIPCON (slot 0x00af08ec) taken as 0 unless STRICTLY positive, returning ftol( k x (b x share) + share ). TRUNCATING, not rounding, and note the association -- neither is `share x (1 + b x k)`",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "BuildQueue_TotalConstructionLeft",
|
||||
"addr": "0x008251e0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "int (BuildQueue* q) // 23 bytes, plain `ret`, no frame. Walks the std::list at q+0x10 from its sentinel and sums the dword at +0x18 of each node -- the order's `conleft`. This is the queue demand ComputeOutputFromRates charges against out[7] before anything cascades back to the money channel, and the ONE input of that function that a save can supply in full (Sys/BQ/ords/conleft is on the wire)",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "ServerSystem_IdealSuitability",
|
||||
"addr": "0x00745d60",
|
||||
"convention": "thiscall",
|
||||
"prototype": "double (ServerSystem* sys) // real end 0x00745dc0. The suitability the terraform channel aims at, and the value NormaliseOutputRates compares sys->Suit against with an exact ==. With no owner it returns sys->Suit itself, so an unowned system is always 'at its ideal'. Otherwise it starts from owner->IdealSuit (+0xb0), replaces that with StrategyServer::IdealSuit(sys->server, sys->indi->indsp) when the system carries an independence record (+0x1c8), and finally overrides both with sys->dsu (+0x118) whenever dsu differs from the float behind 0x00aeca6c. NOT the same source as CalcSuitMod's ideal, which is the server's per-species array unconditionally -- the two agree on every corpus save, so the difference is instruction-verified only. Every corpus system carries dsu = FLT_MAX, which is why the sentinel is READ AS FLT_MAX (inferred from the corpus, not from the data files)",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "ServerSystem_CivilianConsumption",
|
||||
"addr": "0x0074c6f0",
|
||||
"convention": "custom",
|
||||
"prototype": "int (/* ESI = ServerSystem* sys */) // real end 0x0074c80f. TAKES ITS `this` IN ESI, not ECX -- a compiler-local helper that inherits the register its caller holds; hooking or calling it as a __thiscall reads the wrong object. Returns 0 with no owner and 0 on an independent colony (+0x1c8), else derives a civilian-share ratio from three int64 population helpers (0x0074a870, 0x0074a8c0, 0x0074a920), scales CIVILIAN_RESOURCES_CONSUMED (slot 0x00ae2ea4) by it and floors the result at 1. Feeds only the resource ledger (out[1], out[2]); it is NOT on the path to out[3]",
|
||||
"status": "unverified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08) -- the ESI convention and the two early returns are read from the instruction stream; the ratio's exact shape is not"
|
||||
},
|
||||
{
|
||||
"name": "SystemRepairDemandForOwner",
|
||||
"addr": "0x007460b0",
|
||||
"convention": "custom",
|
||||
"prototype": "int (/* EBX = ServerSystem* sys */ ServerPlayer* owner) // real end 0x0074615a. TAKES THE SYSTEM IN EBX and the player on the stack -- the second compiler-local helper in this call graph with an inherited register. Returns 0 when either is null. Walks the system's fleets through the system's vtable (slot 2 = count, slot 3 = element), keeps those whose owner (0x0071e280) is the argument and whose +0x78 byte is set, and sums 0x00829180 over the ship vector at +0xa4..+0xa8. ComputeOutput stores the result in out[6]; nothing downstream of out[3] reads it",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "TradeManager_SystemRouteIncome",
|
||||
"addr": "0x0083a5b0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void (TradeManager* mgr, ServerSystem* sys, ServerPlayer* owner, int* a, int* b) // `ret 0x10`. Returns immediately when `sys` is null, else forwards to 0x00833a10(owner, a, b, 0, sys) with the manager still in ECX. ComputeOutput reaches it as `mgr = server->vtbl[2]()` -- a zero-argument getter whose four argument pushes were scheduled BEFORE the call, which reads as a five-argument virtual call and is not one. Fills out[4] and out[5]; nothing downstream of out[3] reads them. The name is INFERRED from the callee's neighbourhood (the trade manager's difficulty multiplier lives at 0x00833938), not from a symbol",
|
||||
"status": "unverified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "ServerSystem_StationCount",
|
||||
"addr": "0x00815c10",
|
||||
"convention": "cdecl",
|
||||
"prototype": "int (ServerSystem* sys, ServerPlayer* owner, int kind) // the station count both station bonuses read. ConstructionPoints 0x00746830 passes kind = 1 (shipyards); GroupOutput 0x0074b7a0 passes kind = 0 (the imperial output bonus). Body not read this lane; the argument order and the two kinds are read off the two call sites",
|
||||
"status": "unverified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "Ship_RepairCost",
|
||||
"addr": "0x00815180",
|
||||
"convention": "thiscall",
|
||||
"prototype": "int (StarShip* ship, int kind) // the per-ship repair demand the orbit repair pass sums and then spends against, always called with kind = 1. Body not read; this is the ONE input of ComputeOutput that no save can currently supply, and until it is read the engine takes the demand as 0 -- which reads a colony with a damaged fleet HIGH, because every point the repair pass would have taken is a point that comes back to the money channel instead",
|
||||
"status": "unverified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08) -- reached from 0x00751590, body not read"
|
||||
},
|
||||
{
|
||||
"name": "Ship_ApplyRepair",
|
||||
"addr": "0x008151c0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void (StarShip* ship, int points) // the write half of the orbit repair pass. Body not read",
|
||||
"status": "unverified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08) -- reached from 0x00751590, body not read"
|
||||
},
|
||||
{
|
||||
"name": "Fleet_TestFlags",
|
||||
"addr": "0x00813ab0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "bool (StarFleet* fleet, int a, int b) // the fleet-level gate of the orbit repair pass, called as (0, 8); the ship-level counterpart is 0x00814da0 with the same arguments. Body not read",
|
||||
"status": "unverified",
|
||||
"source": "findings/subsystems/output-turn-path.md (lane C3 2026-09-08) -- reached from 0x00751590, body not read"
|
||||
}
|
||||
]
|
||||
}
|
||||
60
ghidra/addresses.d/lane-t2.json
Normal file
60
ghidra/addresses.d/lane-t2.json
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"entries": [
|
||||
{
|
||||
"name": "StrategyServer_StampTreatyTurns",
|
||||
"addr": "0x007898c0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void (StrategyServer* S, vector<uint32>* allianceBroken, vector<uint32>* napBroken, vector<uint32>* cfBroken) // 700 B, ret 0xc. THE DIPLOMACY LEDGER'S PER-TURN STAMP, and the only writer of DiplomacyStats on a turn with no combat and no diplomatic command. Pass A (0x00789920..0x007899c4): over every ORDERED pair (A,B) of the S-frame player vector at S+0x54/+0x58, skipping A==B by POINTER, rel = A->GetRelation(B) 0x0080e050; rel 1 -> slot 8, rel 2 -> slot 0, rel 3 -> slot 4, else skip; then ctor a DiplomacyStats on the stack, GetDipStat(&local,B) 0x008180e0, store (int16)S->Frame(+0xc) at local+8+slot*2, SetDipStat(&local,B) 0x00863950. Field mapping: rel 3 -> lastally(+0x10), rel 2 -> lastnap(+8), rel 1 -> lastcf(+0x18). Pass B (0x007899ca..) is the betrayal counter and indexes the three broken-mask arguments by the inner loop index, substituting a zero local when a vector's length != nPlayers -- with no command stream it is a no-op. SOLE CALLER: ApplyTurnCommands 0x007b18b0 at 0x007b2461, so this runs BEFORE both turn drivers and AFTER BeginProcessTurn's frame bump",
|
||||
"status": "mapped",
|
||||
"source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08); closes 26 leaves on the reference pair and 14 on pair 2 in sots-engine, 0 regressed, and reproduces the ledger of 10 of 11 corpus saves exactly"
|
||||
},
|
||||
{
|
||||
"name": "PlayerAlliances_Relation",
|
||||
"addr": "0x006d2050",
|
||||
"convention": "cdecl",
|
||||
"prototype": "int (int myPlyrIdx, PlayerAlliances* a, int otherPlyrIdx) // 58 B. if (myPlyrIdx == otherPlyrIdx) return 3; bit = 1 << otherPlyrIdx (shl by cl, so masked to 5 bits); if (a->AL(+4) & bit) return 3; if (a->NA(+8) & bit) return 2; return (a->CF(+0xc) & bit) ? 1 : 0. THE RELATION CODES ARE 3 = ALLIED (and self), 2 = NON-AGGRESSION, 1 = CEASE-FIRE, 0 = WAR -- strategic-turn-internals.md section 5.2 had 1 and 3 the other way round. The bit is the INDEX FIELD, not the position in the player vector (the opposite of the shared-vision mask), and AL is tested with NO alliance-id guard",
|
||||
"status": "mapped",
|
||||
"source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08); agrees with combat-done-tail.md section 2A, which reads Node::ResupplyAlliedFleets as gated on GetRelation == 3 and UpdateDiplomacyStatsFromCombat as gated on GetRelation < 1"
|
||||
},
|
||||
{
|
||||
"name": "ServerPlayer_GetRelation",
|
||||
"addr": "0x0080e050",
|
||||
"convention": "thiscall",
|
||||
"prototype": "int (ServerPlayer* this, ServerPlayer* other) // 33 B, ret 4. A thin forwarder: tail-calls the cdecl PlayerAlliances_Relation 0x006d2050 with (this->PlyrIdx(+0x28), &this->Alliances(+0x168), other->PlyrIdx(+0x28)). 60+ call sites across the image",
|
||||
"status": "mapped",
|
||||
"source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "DiplomacyStats_ctor",
|
||||
"addr": "0x0080e7b0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "DiplomacyStats* (DiplomacyStats* this) // 55 B. vptr = 0x00a21430; every field zeroed; then lastcf(+0x18) = lastnap(+8) = lastally(+0x10) = -1. So a fresh entry's three 'last in force' fields are -1, NOT 0, and every counter (lastnapbty/bkn*/bty*/deadhome) is 0. This is what distinguishes 'never' from 'on turn 0' in the ledger",
|
||||
"status": "mapped",
|
||||
"source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "ServerPlayer_GetDipStat",
|
||||
"addr": "0x008180e0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void (ServerPlayer* this, DiplomacyStats* out, ServerPlayer* other) // 294 B, ret 8. if (!out) return; re-initialise *out to the ctor's defaults IN PLACE (the vptr is not touched); out->other(+4) = other->+0x4 (the handle id, i.e. the wire's PlayerID); if (!other) return; then a LINEAR FIRST-MATCH scan of the 0x24-stride vector at this->dipstats(+0x230/+0x234) for entry.other == GetId(other) 0x0042bfb0, copying the entry's thirteen int16 fields (out+8..out+0x21) on a hit. Stride read as 0x38e38e39 / sar 3",
|
||||
"status": "mapped",
|
||||
"source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "ServerPlayer_SetDipStat",
|
||||
"addr": "0x00863950",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void (ServerPlayer* this, const DiplomacyStats* src, ServerPlayer* other) // 328 B, ret 8. if (!src || !other) return; the same linear first-match scan; ON A MISS default-construct a DiplomacyStats on the stack and push_back it (0x0085bc40) so a NEW ENTRY IS APPENDED AT THE END, then back().other = other->+0x4; finally copy src's thirteen int16 fields into the entry and re-write other. The append order is therefore the order in which pairs are first stamped, which is player-vector order",
|
||||
"status": "mapped",
|
||||
"source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "StrategyServer_MarkPlayerTurnEnded",
|
||||
"addr": "0x00821a40",
|
||||
"convention": "thiscall",
|
||||
"prototype": "bool (StrategyServer* this, int playerId) // 60 B, ret 4. p = HandleMap::Resolve(this + 0x80, playerId) 0x008b9240; if (!p) { Log(2, <0x00a2fb30>, playerId); return false; } p->Status(+0x164) = 4; return true. THE ONLY WRITER OF Player.Status = 4 IN THE IMAGE. Three callers, all End Turn SUBMISSION paths that run before the turn is processed: EndTurn 0x00783be0 (+0x70, passes the client's own id at client+0x148), EndTurnForced 0x00783d30 (+0x7b), OnPlayerEndTurn 0x007d9af0 (+0x35). The other two immediate stores to +0x164 in the image are ProcessTurn +0x5ca (value 1, inside the 0x44-stride encounter-member loop) and ResumePlaying +0xb1 (value 0, the load-path normalisation determinism-oracle.md recorded as 'Status resets 4 -> 0 on load'). There is NO writer between tail phase 31 and the autosave; backlog.md item 6 looks in the wrong place",
|
||||
"status": "mapped",
|
||||
"source": "findings/subsystems/treaty-turn-stamp.md (lane T2 2026-09-08) section 3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
||||
// Source: sots-re ghidra/addresses.json @ 232397a, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ 3b99e25, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
|
@ -1481,6 +1481,70 @@ constexpr uint32_t StrategyServer_DestroyFleet = 0x0048b980;
|
|||
constexpr uint32_t StrategyServer_OrderFleetMove = 0x004653c0;
|
||||
// thiscall void** (std::map<int, void*>* this, const int* key) // 125 B, ret 4. MSVC std::map<int,T*>::operator[]: _Lbound over the tree from this->_Myhead(+0x04)->_Parent, testing _Isnil at node+0x15 and the key at node+0x0c; if found returns &node->_Myval.second (node+0x10), else default-inserts the pair {key, 0} via _Buynode 0x008b91d0 + _Insert 0x0072b400 and returns the same. NODE IS 0x18 BY ENUMERATION from _Buynode's operator new(0x18): _Left +0x00, _Parent +0x04, _Right +0x08, pair<int,void*> at +0x0c/+0x10, and _Color/_Isnil written as ONE 16-bit store at +0x14/+0x15, plus 2 bytes padding. DELEGATED instruction-level read [verified]
|
||||
constexpr uint32_t Map_IntPtr_Subscript = 0x0036bce0;
|
||||
// site The byte AFTER Game::BuildQueue::ProcessTurn's last instruction (`ret 0x8` at 0x00891226, then int3 padding). Ghidra reports the function as 1230 bytes from 0x00890d50, i.e. ending at 0x0089121e -- INSIDE the epilogue, before the security cookie check. The body is 1264 bytes. Earned rule 17. Note that this address is ALSO the entry of SystemBuildQueue_AttachBuiltShip, the function's own slot-10 callee, which is why the fleet half of construction looked absent from the pass [verified]
|
||||
constexpr uint32_t BuildQueue_ProcessTurn_RealEnd = 0x00491240;
|
||||
// field ServerPlayer* -- the player that owns the queue. Read at 0x00890de9 (passed to the ship factory), 0x00890ec9 (passed to the system's post-build hook), 0x00890ef4 (the base of the ShipRecords update) and 0x00890f75 (the build-completed event's owner field) [verified]
|
||||
constexpr uint32_t BuildQueue_off_Owner = 0x0000000c;
|
||||
// field std::list<ShipBuildOrder> head sentinel. The pass walks it as `node = *(head); while (node != head) node = *node`, so it is the MSVC circular list. Read at 0x00890d9a and re-read every iteration at 0x00890d9d / 0x00891020 [verified]
|
||||
constexpr uint32_t BuildQueue_off_OrderList = 0x00000010;
|
||||
// field int -- the list's element count. Used as the reserve hint for the removal sweep's scratch vector at 0x0089104a and decremented once per unlinked order at 0x008911b0 [verified]
|
||||
constexpr uint32_t BuildQueue_off_OrderCount = 0x00000014;
|
||||
// field int conleft, measured from the std::list NODE base (node+0x0 next, +0x4 prev, +0x8 the order's own vptr -- the list is polymorphic -- then +0xc desID, +0x10 con, +0x14 sav, +0x18 conleft, +0x1c ordID, +0x20 ShipDesign*). Compared against the remaining points at 0x00890db5, decremented at 0x008910c4, zeroed at 0x0089100c, and it is the removal sweep's predicate at 0x00891070 [verified]
|
||||
constexpr uint32_t ShipBuildOrder_off_ConLeft = 0x00000018;
|
||||
// field ShipDesign* -- from the same std::list node base as ShipBuildOrder_off_ConLeft. Loaded at 0x00890dbe and is the source of the money cost (+0xc0), the role flags (+0xb8) and the hull class (+0x12c) the completion reads [verified]
|
||||
constexpr uint32_t ShipBuildOrder_off_Design = 0x00000020;
|
||||
// vslot bool (StrategySim* sim, int64 cost) -- vtable slot 9. Called at 0x00890ddc only when design->+0xc0 > 0; a FALSE return SKIPS that order and the pass continues with the next one rather than stopping. Pure in Game::BuildQueue's own vtable (0x00a31328); Game::SystemBuildQueue (0x00a31358) binds 0x00809910 and Game::ShipBuildQueue (0x00a31388) binds 0x0091e480 [verified]
|
||||
constexpr uint32_t BuildQueue_vslot_ChargeMoney = 0x00000024;
|
||||
// vslot void (StrategySim* sim, StarShip* ship) -- vtable slot 10, called at 0x00890e20 immediately after the ship is created. Pure in the base vtable; Game::SystemBuildQueue binds 0x00891240, Game::ShipBuildQueue binds 0x0081c000. This is where join-an-existing-fleet vs create-a-new-one is decided [verified]
|
||||
constexpr uint32_t BuildQueue_vslot_AttachBuiltShip = 0x00000028;
|
||||
// __thiscall void (SystemBuildQueue* this, StrategySim* sim, StarShip* ship) /* RET 8. Game::SystemBuildQueue vtable slot 10. Routes the newly built hull by design role flags -- design->+0xb8 & 0x80000, design->+0xbc & 0x4, design->+0xb8 & 0x800000, design->+0xb8 & 0x400 each take their own handler -- and FALLS THROUGH at 0x00891328 to `if (ship->FltID == 0) ServerSystem_AttachShipToHomeFleet(sys, ship)`. NOTE the 0x400 tested at 0x00891311 is a DESIGN role bit and is NOT the fleet's FtFlg 0x400 */ [verified]
|
||||
constexpr uint32_t SystemBuildQueue_AttachBuiltShip = 0x00491240;
|
||||
// __thiscall void (ServerSystem* this, StarShip* ship) /* The join-or-create step. If this->+0x238 is non-null the cached home fleet is reused; otherwise an id is drawn from the object-id allocator (0x0074f500) and StrategyServer_CreateFleet (lane B5, 0x0085b340) builds a fleet at the system's position with a NULL name override, the result is cached in this->+0x238 (0x0074f513), FtFlg |= 0x20 (0x0074f519), and StarFleet_AddShip links the hull. ONE home fleet per system: every hull built at that system in later turns joins it */ [verified]
|
||||
constexpr uint32_t ServerSystem_AttachShipToHomeFleet = 0x0034f4d0;
|
||||
// field StarFleet* -- the cached fleet newly built hulls join. Tested at 0x0074f4d6 and written at 0x0074f513. NEW OFFSET: not in struct-recovery.md's ServerSystem table [verified]
|
||||
constexpr uint32_t ServerSystem_off_HomeFleet = 0x00000238;
|
||||
// __thiscall StarShip* (ObjectHost* this, int id, ServerPlayer* owner, ShipDesign* design) /* RET 0xc. operator new(0xb0) at 0x0086571c, ctor 0x00861280, then IDMap_Insert at 0x00865754 which is where the object id lands in ship->+0x4. Reached from exactly two places in the image: the build queue (through the 0x004f41a0 thunk) and the trade manager's encounter spawner */ [verified]
|
||||
constexpr uint32_t StarShip_Create = 0x004656f0;
|
||||
// __thiscall void (StarShip* this, ObjectHost* host, ShipDesign* design, ServerPlayer* owner) /* Zeroes the object through 0x0080c960 (which leaves +0x48, +0x60 and +0xac at -1), then writes +0xc host, +0x14 design (DesID), +0x10 owner (PlrID); allocates the ship-borne BuildQueue into +0x98 when design->+0xb8 & 0x400000, and three Population objects into +0x9c/+0xa0/+0xa4 when design->+0xb8 & 0x4000000; finally 0x00854680 copies the cached design stats -- Range +0x20 from design+0xe8, Health +0x24..+0x30 from design+0xec.., RefCap +0x6c, RepCap +0x70, ConCap +0x68 from design+0xd0. FltID (+0x64) is born NULL and is set by StarFleet_AddShip */ [verified]
|
||||
constexpr uint32_t StarShip_Ctor = 0x00461280;
|
||||
// field int tblt -- the turn the hull was completed. Born -1 in the constructor's default sweep (0x0080c9fb) and overwritten by the build queue at 0x00890e02 with the StrategySim's Frame word. MEASURED: the six hulls the zuul turn-16 -> turn-17 pair adds all carry tblt equal to the NEW turn number, which independently confirms that BeginProcessTurn's Frame increment happens BEFORE the spine, so a phase reading Frame during a turn sees the turn it is producing, not the one it started from [verified]
|
||||
constexpr uint32_t StarShip_off_TurnBuilt = 0x000000ac;
|
||||
// site `inc DWORD PTR [esi+edx*4+0x1b4]` with esi = the queue's owner and edx = design->+0x12c (the hull class). THE ONLY WRITER OF THE PER-CLASS BUILT COUNTER IN THE WHOLE IMAGE: a byte scan for the indexed-increment form at that displacement over all executable sections returns this one site. A hull that reaches the wire with the counter bumped came through the build queue and through nothing else [verified]
|
||||
constexpr uint32_t ShipRecords_BuiltCounterSite = 0x00490ef7;
|
||||
// field int built[3] -- the first of Game::ShipRecords' four parallel per-hull-class arrays (built, lost, killed, inService; wire tags srb/srl/srk/sri under the srnc count). SIZED BY ENUMERATION, not by what the code touches: three classes x four arrays x 4 bytes from 0x1b4 lands exactly on 0x1e4, the per-design vector, which is the next thing the same function reads. struct-recovery.md places the ShipRecords sub-object at 0x1b0, so 0x1b0 is its vptr [verified]
|
||||
constexpr uint32_t ServerPlayer_off_ShipRecordsBuilt = 0x000001b4;
|
||||
// field std::vector<{int srd; int src; int srb; int srl; int sri}> at +0x1e4/+0x1e8/+0x1ec, stride 0x14 (the wire's srbd section). The completion scans it linearly for a record whose first word equals the design's object id (0x00890f10), appends one when there is no hit (0x00890f4c) and increments the record's third word (0x00890f61). MEASURED: srd really is the design's save id -- the zuul corpus shows records keyed 608/576/114/816/18/34/130 against build orders naming exactly those desIDs [verified]
|
||||
constexpr uint32_t ServerPlayer_off_ShipRecordsByDesign = 0x000001e4;
|
||||
// __thiscall int (ServerTradeManagerImpl* this, int* spec) /* THE SECOND AND ONLY OTHER ROOT THAT CREATES SHIPS. Draws two ids from the object-id allocator, creates a StarFleet through StrategyServer_CreateFleet (lane B5, 0x0085b340) (0x0088f314) and then loops StarShip creation (0x0088f375) + StarFleet_AddShip (0x0088f381). Reached only from 0x008926ce, itself reached only from ServerTradeManagerImpl vtable slot 17 (0x008938a0), which picks its target with an RNG draw. It does NOT touch ModCount, and it does NOT touch ShipRecords -- so an encounter squadron is invisible to the per-class built counter, which is why that counter is a clean discriminator for player-built hulls */ [verified]
|
||||
constexpr uint32_t TradeManager_SpawnEncounterSquadron = 0x0048f070;
|
||||
// __thiscall void (ShipAction* this, ...) /* The construction-ship wrapper around BuildQueue::ProcessTurn (calls it at 0x00789551 with the ship's own queue at ship->+0x98 and its ConCap at ship->+0x68 as the point budget). REACHABILITY NOTE, and it is a new indirection class for the campaign: this function has ZERO call sites and is in NO vtable. Its address is written into a STACK-BUILT function-pointer table by the ship-action dispatcher (0x007b9c4b `mov eax,0x789500`, stored at 0x007b9c50), alongside five siblings. tools/vtable_map.py cannot see edges of this shape, so `no caller` and `no vtable caller` are BOTH lower bounds */ [verified]
|
||||
constexpr uint32_t ShipBorneBuildQueue_ProcessTurn = 0x00389500;
|
||||
// site CORRECTION to addresses.json's BuildQueue_ProcessTurn prototype, which reads `int (BuildQueue* this, ServerSystem* sys, int points)`. The FIRST STACK ARGUMENT IS NOT THE SYSTEM. At the only real call site the pushed value is `[sys+0x10] - 4` (0x0075257c `lea edi,[eax-0x4]` with eax = [esi+0x10], esi = the ServerSystem, ecx = [esi+0xa4] = the queue), i.e. the StrategyServer `S` frame -- the same object lane B5's StrategyServer_CreateFleet takes at S+4, one word higher. Two consequences the old prototype hides: the build-completed event is pushed onto a list at S+0x2b0, NOT onto the system; and the turn stamp written into the new hull at 0x00890e02 is S+0xc, which StrategyServer::Write tags `Frame`. AGREEMENT with lane T section 0 (the two bases four bytes apart) and with lane A2 (S+0xc is Frame, not ModCount) [verified]
|
||||
constexpr uint32_t BuildQueue_ProcessTurn_Arg0Correction = 0x00352589;
|
||||
// field ServerSystem* -- the system a SystemBuildQueue belongs to, read by the slot-10 attach handler at 0x00891246 as `(this->+0x4 == 0) ? this->+0x8 : 0`. Also read by the pass itself at 0x00890f89 for the build-completed event [verified]
|
||||
constexpr uint32_t BuildQueue_off_OwningSystem = 0x00000008;
|
||||
// thiscall int (ServerSystem* sys, int points, bool estimateOnly) // `ret 8`, real end 0x007517b5 (SEH frame, /GS cookie). Returns the points NOT consumed. Walks the system's fleets through the system's own vtable (slot 2 = count, slot 4 = element), keeps those whose +0x58 equals sys->PID and that pass 0x00813ab0(0,8), and inside each keeps the ships that pass 0x00814da0(0,8) with 0x00815180(ship,1) > 0. With estimateOnly it returns max(points - totalCost, 0) and touches nothing; without it, it distributes round-robin -- share = max(points / shipCount, 1) per pass, take = min(cost, share), applied by 0x008151c0 -- until no ship takes anything or the points run out. THE ROUND ROBIN IS EQUIVALENT TO points - min(points, totalCost): share is at least 1, so every ship with a positive cost takes at least one point per pass, and the only early exit needs every remaining cost to be zero. THIS IS THE SIDE EFFECT that makes ComputeOutputFromRates unsafe to call for its value (the B1 replace double-run defect): ComputeOutputFromRates passes estimateOnly = 0 [verified]
|
||||
constexpr uint32_t ServerSystem_RepairShipsInOrbit = 0x00351590;
|
||||
// thiscall int (ServerSystem* sys, double constructionShare) // `ret 8`, real end 0x00746883. Turns the ship-construction channel's rounded share into out[7]: with no owner it is ftol(share), else k = StationCount(sys, PID, 1) and b = STATION_BONUS_SHIPCON (slot 0x00af08ec) taken as 0 unless STRICTLY positive, returning ftol( k x (b x share) + share ). TRUNCATING, not rounding, and note the association -- neither is `share x (1 + b x k)` [verified]
|
||||
constexpr uint32_t ServerSystem_ConstructionPoints = 0x00346830;
|
||||
// thiscall int (BuildQueue* q) // 23 bytes, plain `ret`, no frame. Walks the std::list at q+0x10 from its sentinel and sums the dword at +0x18 of each node -- the order's `conleft`. This is the queue demand ComputeOutputFromRates charges against out[7] before anything cascades back to the money channel, and the ONE input of that function that a save can supply in full (Sys/BQ/ords/conleft is on the wire) [verified]
|
||||
constexpr uint32_t BuildQueue_TotalConstructionLeft = 0x004251e0;
|
||||
// thiscall double (ServerSystem* sys) // real end 0x00745dc0. The suitability the terraform channel aims at, and the value NormaliseOutputRates compares sys->Suit against with an exact ==. With no owner it returns sys->Suit itself, so an unowned system is always 'at its ideal'. Otherwise it starts from owner->IdealSuit (+0xb0), replaces that with StrategyServer::IdealSuit(sys->server, sys->indi->indsp) when the system carries an independence record (+0x1c8), and finally overrides both with sys->dsu (+0x118) whenever dsu differs from the float behind 0x00aeca6c. NOT the same source as CalcSuitMod's ideal, which is the server's per-species array unconditionally -- the two agree on every corpus save, so the difference is instruction-verified only. Every corpus system carries dsu = FLT_MAX, which is why the sentinel is READ AS FLT_MAX (inferred from the corpus, not from the data files) [verified]
|
||||
constexpr uint32_t ServerSystem_IdealSuitability = 0x00345d60;
|
||||
// custom int (/* ESI = ServerSystem* sys */) // real end 0x0074c80f. TAKES ITS `this` IN ESI, not ECX -- a compiler-local helper that inherits the register its caller holds; hooking or calling it as a __thiscall reads the wrong object. Returns 0 with no owner and 0 on an independent colony (+0x1c8), else derives a civilian-share ratio from three int64 population helpers (0x0074a870, 0x0074a8c0, 0x0074a920), scales CIVILIAN_RESOURCES_CONSUMED (slot 0x00ae2ea4) by it and floors the result at 1. Feeds only the resource ledger (out[1], out[2]); it is NOT on the path to out[3] [unverified]
|
||||
constexpr uint32_t ServerSystem_CivilianConsumption = 0x0034c6f0;
|
||||
// custom int (/* EBX = ServerSystem* sys */ ServerPlayer* owner) // real end 0x0074615a. TAKES THE SYSTEM IN EBX and the player on the stack -- the second compiler-local helper in this call graph with an inherited register. Returns 0 when either is null. Walks the system's fleets through the system's vtable (slot 2 = count, slot 3 = element), keeps those whose owner (0x0071e280) is the argument and whose +0x78 byte is set, and sums 0x00829180 over the ship vector at +0xa4..+0xa8. ComputeOutput stores the result in out[6]; nothing downstream of out[3] reads it [verified]
|
||||
constexpr uint32_t SystemRepairDemandForOwner = 0x003460b0;
|
||||
// thiscall void (TradeManager* mgr, ServerSystem* sys, ServerPlayer* owner, int* a, int* b) // `ret 0x10`. Returns immediately when `sys` is null, else forwards to 0x00833a10(owner, a, b, 0, sys) with the manager still in ECX. ComputeOutput reaches it as `mgr = server->vtbl[2]()` -- a zero-argument getter whose four argument pushes were scheduled BEFORE the call, which reads as a five-argument virtual call and is not one. Fills out[4] and out[5]; nothing downstream of out[3] reads them. The name is INFERRED from the callee's neighbourhood (the trade manager's difficulty multiplier lives at 0x00833938), not from a symbol [unverified]
|
||||
constexpr uint32_t TradeManager_SystemRouteIncome = 0x0043a5b0;
|
||||
// cdecl int (ServerSystem* sys, ServerPlayer* owner, int kind) // the station count both station bonuses read. ConstructionPoints 0x00746830 passes kind = 1 (shipyards); GroupOutput 0x0074b7a0 passes kind = 0 (the imperial output bonus). Body not read this lane; the argument order and the two kinds are read off the two call sites [unverified]
|
||||
constexpr uint32_t ServerSystem_StationCount = 0x00415c10;
|
||||
// thiscall int (StarShip* ship, int kind) // the per-ship repair demand the orbit repair pass sums and then spends against, always called with kind = 1. Body not read; this is the ONE input of ComputeOutput that no save can currently supply, and until it is read the engine takes the demand as 0 -- which reads a colony with a damaged fleet HIGH, because every point the repair pass would have taken is a point that comes back to the money channel instead [unverified]
|
||||
constexpr uint32_t Ship_RepairCost = 0x00415180;
|
||||
// thiscall void (StarShip* ship, int points) // the write half of the orbit repair pass. Body not read [unverified]
|
||||
constexpr uint32_t Ship_ApplyRepair = 0x004151c0;
|
||||
// thiscall bool (StarFleet* fleet, int a, int b) // the fleet-level gate of the orbit repair pass, called as (0, 8); the ship-level counterpart is 0x00814da0 with the same arguments. Body not read [unverified]
|
||||
constexpr uint32_t Fleet_TestFlags = 0x00413ab0;
|
||||
// thiscall void (Game_ShipDesignDef* this, Mars::Stream* s) // THE DESIGN SERIALIZER LANE D SAID DID NOT EXIST. Slot 1 of the ShipDesignDef vftable 0x009fef64. Writes, in DISK order: WriteBool 'FAIDes' this+0x4, WriteBool 'DHide' this+0x5, WriteBool 'DWep' this+0x6 (a BOOL, not an int -- the campaign schema had it as int; byte-neutral because a 4-char tag makes both items 12 bytes), WriteString 'DName' this+0x8, then THREE 'DSec' frames through StreamableHelper<ShipDesignDef::Section> at this+0x4c, this+0x24, this+0x74 in that order. THREE sections, not five: the ctor 0x00874c70 runs eh_vector_constructor_iterator(this+0x24, stride 0x28, count 3). MEMORY ORDER != WRITE ORDER: the array is [+0x24, +0x4c, +0x74] and the wire is [+0x4c (command), +0x24 (mission), +0x74 (engine)] [verified]
|
||||
constexpr uint32_t Game_ShipDesignDef_Write = 0x00427390;
|
||||
// thiscall void (Game_ShipDesignDef* this, Mars::Stream* s) // slot 0 of vftable 0x009fef64. Mirrors Write field for field, same tags, same three DSec frames in the same order [verified]
|
||||
|
|
@ -2005,6 +2069,20 @@ constexpr uint32_t g_flt_RebOutModDecay = 0x00617870;
|
|||
constexpr uint32_t g_flt_RebOutModMin = 0x00617868;
|
||||
// data const float = 2.0f, the upper clamp of RebOutMod [verified]
|
||||
constexpr uint32_t g_flt_RebOutModMax = 0x0061786c;
|
||||
// thiscall void (StrategyServer* S, vector<uint32>* allianceBroken, vector<uint32>* napBroken, vector<uint32>* cfBroken) // 700 B, ret 0xc. THE DIPLOMACY LEDGER'S PER-TURN STAMP, and the only writer of DiplomacyStats on a turn with no combat and no diplomatic command. Pass A (0x00789920..0x007899c4): over every ORDERED pair (A,B) of the S-frame player vector at S+0x54/+0x58, skipping A==B by POINTER, rel = A->GetRelation(B) 0x0080e050; rel 1 -> slot 8, rel 2 -> slot 0, rel 3 -> slot 4, else skip; then ctor a DiplomacyStats on the stack, GetDipStat(&local,B) 0x008180e0, store (int16)S->Frame(+0xc) at local+8+slot*2, SetDipStat(&local,B) 0x00863950. Field mapping: rel 3 -> lastally(+0x10), rel 2 -> lastnap(+8), rel 1 -> lastcf(+0x18). Pass B (0x007899ca..) is the betrayal counter and indexes the three broken-mask arguments by the inner loop index, substituting a zero local when a vector's length != nPlayers -- with no command stream it is a no-op. SOLE CALLER: ApplyTurnCommands 0x007b18b0 at 0x007b2461, so this runs BEFORE both turn drivers and AFTER BeginProcessTurn's frame bump [mapped]
|
||||
constexpr uint32_t StrategyServer_StampTreatyTurns = 0x003898c0;
|
||||
// cdecl int (int myPlyrIdx, PlayerAlliances* a, int otherPlyrIdx) // 58 B. if (myPlyrIdx == otherPlyrIdx) return 3; bit = 1 << otherPlyrIdx (shl by cl, so masked to 5 bits); if (a->AL(+4) & bit) return 3; if (a->NA(+8) & bit) return 2; return (a->CF(+0xc) & bit) ? 1 : 0. THE RELATION CODES ARE 3 = ALLIED (and self), 2 = NON-AGGRESSION, 1 = CEASE-FIRE, 0 = WAR -- strategic-turn-internals.md section 5.2 had 1 and 3 the other way round. The bit is the INDEX FIELD, not the position in the player vector (the opposite of the shared-vision mask), and AL is tested with NO alliance-id guard [mapped]
|
||||
constexpr uint32_t PlayerAlliances_Relation = 0x002d2050;
|
||||
// thiscall int (ServerPlayer* this, ServerPlayer* other) // 33 B, ret 4. A thin forwarder: tail-calls the cdecl PlayerAlliances_Relation 0x006d2050 with (this->PlyrIdx(+0x28), &this->Alliances(+0x168), other->PlyrIdx(+0x28)). 60+ call sites across the image [mapped]
|
||||
constexpr uint32_t ServerPlayer_GetRelation = 0x0040e050;
|
||||
// thiscall DiplomacyStats* (DiplomacyStats* this) // 55 B. vptr = 0x00a21430; every field zeroed; then lastcf(+0x18) = lastnap(+8) = lastally(+0x10) = -1. So a fresh entry's three 'last in force' fields are -1, NOT 0, and every counter (lastnapbty/bkn*/bty*/deadhome) is 0. This is what distinguishes 'never' from 'on turn 0' in the ledger [mapped]
|
||||
constexpr uint32_t DiplomacyStats_ctor = 0x0040e7b0;
|
||||
// thiscall void (ServerPlayer* this, DiplomacyStats* out, ServerPlayer* other) // 294 B, ret 8. if (!out) return; re-initialise *out to the ctor's defaults IN PLACE (the vptr is not touched); out->other(+4) = other->+0x4 (the handle id, i.e. the wire's PlayerID); if (!other) return; then a LINEAR FIRST-MATCH scan of the 0x24-stride vector at this->dipstats(+0x230/+0x234) for entry.other == GetId(other) 0x0042bfb0, copying the entry's thirteen int16 fields (out+8..out+0x21) on a hit. Stride read as 0x38e38e39 / sar 3 [mapped]
|
||||
constexpr uint32_t ServerPlayer_GetDipStat = 0x004180e0;
|
||||
// thiscall void (ServerPlayer* this, const DiplomacyStats* src, ServerPlayer* other) // 328 B, ret 8. if (!src || !other) return; the same linear first-match scan; ON A MISS default-construct a DiplomacyStats on the stack and push_back it (0x0085bc40) so a NEW ENTRY IS APPENDED AT THE END, then back().other = other->+0x4; finally copy src's thirteen int16 fields into the entry and re-write other. The append order is therefore the order in which pairs are first stamped, which is player-vector order [mapped]
|
||||
constexpr uint32_t ServerPlayer_SetDipStat = 0x00463950;
|
||||
// thiscall bool (StrategyServer* this, int playerId) // 60 B, ret 4. p = HandleMap::Resolve(this + 0x80, playerId) 0x008b9240; if (!p) { Log(2, <0x00a2fb30>, playerId); return false; } p->Status(+0x164) = 4; return true. THE ONLY WRITER OF Player.Status = 4 IN THE IMAGE. Three callers, all End Turn SUBMISSION paths that run before the turn is processed: EndTurn 0x00783be0 (+0x70, passes the client's own id at client+0x148), EndTurnForced 0x00783d30 (+0x7b), OnPlayerEndTurn 0x007d9af0 (+0x35). The other two immediate stores to +0x164 in the image are ProcessTurn +0x5ca (value 1, inside the 0x44-stride encounter-member loop) and ResumePlaying +0xb1 (value 0, the load-path normalisation determinism-oracle.md recorded as 'Status resets 4 -> 0 on load'). There is NO writer between tail phase 31 and the autosave; backlog.md item 6 looks in the wrong place [mapped]
|
||||
constexpr uint32_t StrategyServer_MarkPlayerTurnEnded = 0x00421a40;
|
||||
// thiscall bool (TechTree* this, TechPrereqs* prereqs /* = TechDef + 0x88 */) // RET 4. An AND over groups, each group an OR over techs: a group is satisfied by any listed tech whose node exists in this->nodes AND whose state (+0x14) is 4. Zero groups -> TRUE (the function returns satisfied==total with both 0); a group with ZERO entries -> FALSE and the whole test fails, because the inner loop cannot break and the outer one then exits with that group uncounted. Reads only. Called from SetResearched twice: the unforced completion gate on the argument def, and the availability sweep on each node's self-resolved def [verified]
|
||||
constexpr uint32_t TechTree_PrereqsMet = 0x0017d8e0;
|
||||
// field TechPrereqs prereqs -- the block TechTree::PrereqsMet is called on. Two MSVC vectors back to back: the flat entry array at +0x00 and the group array at +0x10. SetResearched passes `def + 0x88` at both call sites [verified]
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
# standalone vs the oracle
|
||||
|
||||
generated 2026-09-08T17:46:02Z binary /srv/re-lab/build/eng-rep/build-host/src/app/sots_turn
|
||||
generated 2026-09-08T18:47:54Z binary /home/alex/sots-engine-c3/build-host/src/app/sots_turn
|
||||
|
||||
phases: 16/44 of the two turn drivers modelled, 10 committed (implemented 4, partial 6, blocked 6, stub 28)
|
||||
phases: 16/44 of the two turn drivers modelled, 12 committed (implemented 4, partial 8, blocked 4, stub 28)
|
||||
5/37 of the post-combat tail modelled
|
||||
|
||||
## turn1-state.sav -> turn2-state.sav (real End Turn)
|
||||
baseline (do nothing) 209 leaves diverge
|
||||
after one standalone turn 158 leaves diverge
|
||||
closed 51, regressed 0, byte match: no
|
||||
after one standalone turn 157 leaves diverge
|
||||
closed 52, regressed 0, byte match: no
|
||||
coverage proved on all three saves: {'input': True, 'oracle': True, 'ours': True}
|
||||
closed:
|
||||
+ /Sim/Frame
|
||||
+ /Sim/players/Player[576 "Independent Colony"]/Sav
|
||||
+ /Sim/systems/Sys[112 "Gamma Cephei"]/EPid
|
||||
+ /Sim/systems/Sys[112 "Gamma Cephei"]/ETS
|
||||
+ /Sim/systems/Sys[112 "Gamma Cephei"]/Eid
|
||||
|
|
@ -65,11 +66,12 @@ phases: 16/44 of the two turn drivers modelled, 10 committed (implemented 4, par
|
|||
|
||||
## turn2-state.sav -> turn3-state.sav (real End Turn)
|
||||
baseline (do nothing) 108 leaves diverge
|
||||
after one standalone turn 87 leaves diverge
|
||||
closed 21, regressed 0, byte match: no
|
||||
after one standalone turn 86 leaves diverge
|
||||
closed 22, regressed 0, byte match: no
|
||||
coverage proved on all three saves: {'input': True, 'oracle': True, 'ours': True}
|
||||
closed:
|
||||
+ /Sim/Frame
|
||||
+ /Sim/players/Player[576 "Independent Colony"]/Sav
|
||||
+ /Sim/systems/Sys[112 "Gamma Cephei"]/ETS
|
||||
+ /Sim/systems/Sys[112 "Gamma Cephei"]/ltis
|
||||
+ /Sim/systems/Sys[112 "Gamma Cephei"]/ntdev
|
||||
|
|
@ -92,7 +94,7 @@ phases: 16/44 of the two turn drivers modelled, 10 committed (implemented 4, par
|
|||
+ /Summary/Turn
|
||||
|
||||
## what still differs on the reference pair, by subsystem
|
||||
82 /Sim/players
|
||||
81 /Sim/players
|
||||
34 /Sim/systems
|
||||
24 /Sim/turnstats
|
||||
8 /Sim/SvSctOb
|
||||
|
|
@ -131,7 +133,7 @@ phases: 16/44 of the two turn drivers modelled, 10 committed (implemented 4, par
|
|||
/Sim/players/Player[16 "re"]/Events/Events/.: only-in-A
|
||||
/Sim/players/Player[16 "re"]/Events/Events/.[0]: only-in-B
|
||||
/Sim/players/Player[16 "re"]/Events/Events/.[EvTurn=2]: only-in-B
|
||||
/Sim/players/Player[16 "re"]/Sav: 50000 -> 289688
|
||||
/Sim/players/Player[16 "re"]/Sav: 289092 -> 289688
|
||||
/Sim/players/Player[16 "re"]/Status: 0 -> 4
|
||||
/Sim/players/Player[32 "Fane Lao"]/BnkEl: -1811273 -> -1815833
|
||||
/Sim/players/Player[32 "Fane Lao"]/BnkPr: -896580 -> -898837
|
||||
|
|
@ -145,6 +147,6 @@ phases: 16/44 of the two turn drivers modelled, 10 committed (implemented 4, par
|
|||
/Sim/players/Player[32 "Fane Lao"]/PvSav: 50000 -> 38100
|
||||
/Sim/players/Player[32 "Fane Lao"]/ResRate: 0.25 -> 0.800000011920929 [1.34218e+07 ulp]
|
||||
/Sim/players/Player[32 "Fane Lao"]/ResTNm: '' -> 'IND_Waldo'
|
||||
/Sim/players/Player[32 "Fane Lao"]/Sav: 50000 -> 92651
|
||||
/Sim/players/Player[32 "Fane Lao"]/Sav: 297492 -> 92651
|
||||
/Sim/players/Player[32 "Fane Lao"]/ShipRecs/srb[0]: 0 -> 1
|
||||
/Sim/players/Player[32 "Fane Lao"]/ShipRecs/srb[3]: only-in-B
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"schema": "sots-standalone-status/1",
|
||||
"generated": "2026-09-08T17:46:02Z",
|
||||
"binary": "/srv/re-lab/build/eng-rep/build-host/src/app/sots_turn",
|
||||
"generated": "2026-09-08T18:47:54Z",
|
||||
"binary": "/home/alex/sots-engine-c3/build-host/src/app/sots_turn",
|
||||
"reference": {
|
||||
"input": "turn1-state.sav",
|
||||
"oracle": "turn2-state.sav",
|
||||
"baselineDiverging": 209,
|
||||
"divergingAfterTurn": 158,
|
||||
"closed": 51,
|
||||
"divergingAfterTurn": 157,
|
||||
"closed": 52,
|
||||
"regressed": 0,
|
||||
"byteMatch": false,
|
||||
"subsystems": {
|
||||
"/Sim/players": 82,
|
||||
"/Sim/players": 81,
|
||||
"/Sim/systems": 34,
|
||||
"/Sim/turnstats": 24,
|
||||
"/Sim/SvSctOb": 8,
|
||||
|
|
@ -31,11 +31,11 @@
|
|||
"total": 44,
|
||||
"verified": 0,
|
||||
"implemented": 4,
|
||||
"partial": 6,
|
||||
"blocked": 6,
|
||||
"partial": 8,
|
||||
"blocked": 4,
|
||||
"stub": 28,
|
||||
"modelled": 16,
|
||||
"committed": 10
|
||||
"committed": 12
|
||||
},
|
||||
"tailPhases": {
|
||||
"total": 37,
|
||||
|
|
@ -58,31 +58,31 @@
|
|||
"note": "real End Turn",
|
||||
"exit": 0,
|
||||
"stdout": [
|
||||
"load: /srv/re-lab/build/sre/verify/results/saves/turn1-state.sav",
|
||||
"load: /home/alex/sots-re/verify/results/saves/turn1-state.sav",
|
||||
" 591376 inflated bytes, 0 error(s), 0 warning(s)",
|
||||
" turn 1, frame 1, modCount 0, 8 player(s), 28 system(s), 6 fleet(s)",
|
||||
"roundtrip (untouched): byte-identical (591376 bytes)",
|
||||
"data: no root given (--data DIR / $SOTS_DATA_DIR); the tail's ship census stays unmodelled",
|
||||
"",
|
||||
"phases",
|
||||
" turn drivers (the milestone's denominator): 16 of 44 modelled, 10 committed",
|
||||
" verified 0 implemented 4 partial 6 blocked 6 stub 28",
|
||||
" turn drivers (the milestone's denominator): 16 of 44 modelled, 12 committed",
|
||||
" verified 0 implemented 4 partial 8 blocked 4 stub 28",
|
||||
" post-combat tail (written to the autosave, tracked separately): 5 of 37 modelled",
|
||||
" verified 0 implemented 1 partial 2 blocked 2 stub 32",
|
||||
"",
|
||||
"this run",
|
||||
" leaves written 53",
|
||||
" leaves NOT written by a blocked phase 72",
|
||||
" leaves written 59",
|
||||
" leaves NOT written by a blocked phase 73",
|
||||
" generator words consumed 16 (state loaded, left untouched)",
|
||||
" generator words NOT accounted (never netted off the above):",
|
||||
" - encounter detection draws one unit value and one bounded integer per turn on every turn measured (2 words), with no derived rule behind the count -- its bound is the product of the contact and detector counts, so it is left unmodelled",
|
||||
" - the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)",
|
||||
" - two draws are downstream of the budget's research allocation -- ProcessResearch's completion Chance and the tech-effect callback's own roll (0 or 1 word each). The allocation needs ComputeBudget's per-system money, which is ComputeOutput with the system's OWN rate sliders; the max-income form of that money is now modelled and self-checked (see T31), but it is NOT the one this path takes",
|
||||
" - a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined",
|
||||
" ! the generator advanced during this run but the save keeps its original state (--commit-rng to write it)",
|
||||
" ! the modelled words are a LOWER BOUND on the turn's cost, so a committed generator is short by the unaccounted sites below and its drawn VALUES are not the game's",
|
||||
"",
|
||||
"wrote /tmp/tmpezqlouzs/post-turn1-state.sav (65208 bytes gzipped, 591664 inflated)",
|
||||
"metric -> /tmp/tmpezqlouzs/metric-turn1-state.sav.json"
|
||||
"wrote /tmp/tmp3mqpqe52/post-turn1-state.sav (65218 bytes gzipped, 591664 inflated)",
|
||||
"metric -> /tmp/tmp3mqpqe52/metric-turn1-state.sav.json"
|
||||
],
|
||||
"coverage": {
|
||||
"input": true,
|
||||
|
|
@ -92,14 +92,15 @@
|
|||
"roots": {
|
||||
"input": "64b836b966299d9b72fc02244ed31e31",
|
||||
"oracle": "4bed514f853332b9fa35b388c3aa5558",
|
||||
"ours": "d77168aa0a5bd1b8890d8c0f994aed3f"
|
||||
"ours": "436ff5f87928966e36bb61318bd0b35f"
|
||||
},
|
||||
"baselineDiverging": 209,
|
||||
"divergingAfterTurn": 158,
|
||||
"closed": 51,
|
||||
"divergingAfterTurn": 157,
|
||||
"closed": 52,
|
||||
"regressed": 0,
|
||||
"closedPaths": [
|
||||
"/Sim/Frame",
|
||||
"/Sim/players/Player[576 \"Independent Colony\"]/Sav",
|
||||
"/Sim/systems/Sys[112 \"Gamma Cephei\"]/EPid",
|
||||
"/Sim/systems/Sys[112 \"Gamma Cephei\"]/ETS",
|
||||
"/Sim/systems/Sys[112 \"Gamma Cephei\"]/Eid",
|
||||
|
|
@ -176,7 +177,7 @@
|
|||
"/Sim/players/Player[16 \"re\"]/Events/Events/.: only-in-A",
|
||||
"/Sim/players/Player[16 \"re\"]/Events/Events/.[0]: only-in-B",
|
||||
"/Sim/players/Player[16 \"re\"]/Events/Events/.[EvTurn=2]: only-in-B",
|
||||
"/Sim/players/Player[16 \"re\"]/Sav: 50000 -> 289688",
|
||||
"/Sim/players/Player[16 \"re\"]/Sav: 289092 -> 289688",
|
||||
"/Sim/players/Player[16 \"re\"]/Status: 0 -> 4",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/BnkEl: -1811273 -> -1815833",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/BnkPr: -896580 -> -898837",
|
||||
|
|
@ -190,24 +191,24 @@
|
|||
"/Sim/players/Player[32 \"Fane Lao\"]/PvSav: 50000 -> 38100",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/ResRate: 0.25 -> 0.800000011920929 [1.34218e+07 ulp]",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/ResTNm: '' -> 'IND_Waldo'",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/Sav: 50000 -> 92651",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/Sav: 297492 -> 92651",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/ShipRecs/srb[0]: 0 -> 1",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/ShipRecs/srb[3]: only-in-B"
|
||||
],
|
||||
"byteMatch": false,
|
||||
"standalone": {
|
||||
"schema": "sots-standalone-metric/1",
|
||||
"input": "/srv/re-lab/build/sre/verify/results/saves/turn1-state.sav",
|
||||
"output": "/tmp/tmpezqlouzs/post-turn1-state.sav",
|
||||
"input": "/home/alex/sots-re/verify/results/saves/turn1-state.sav",
|
||||
"output": "/tmp/tmp3mqpqe52/post-turn1-state.sav",
|
||||
"spine": {
|
||||
"total": 44,
|
||||
"verified": 0,
|
||||
"implemented": 4,
|
||||
"partial": 6,
|
||||
"blocked": 6,
|
||||
"partial": 8,
|
||||
"blocked": 4,
|
||||
"stub": 28,
|
||||
"modelled": 16,
|
||||
"committed": 10
|
||||
"committed": 12
|
||||
},
|
||||
"tail": {
|
||||
"total": 37,
|
||||
|
|
@ -220,14 +221,14 @@
|
|||
"committed": 3
|
||||
},
|
||||
"run": {
|
||||
"leafWrites": 53,
|
||||
"blockedLeafWrites": 72,
|
||||
"leafWrites": 59,
|
||||
"blockedLeafWrites": 73,
|
||||
"rngWords": 16,
|
||||
"rngLoaded": true,
|
||||
"rngCommitted": false,
|
||||
"rngUnaccounted": [
|
||||
"encounter detection draws one unit value and one bounded integer per turn on every turn measured (2 words), with no derived rule behind the count -- its bound is the product of the contact and detector counts, so it is left unmodelled",
|
||||
"the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)",
|
||||
"two draws are downstream of the budget's research allocation -- ProcessResearch's completion Chance and the tech-effect callback's own roll (0 or 1 word each). The allocation needs ComputeBudget's per-system money, which is ComputeOutput with the system's OWN rate sliders; the max-income form of that money is now modelled and self-checked (see T31), but it is NOT the one this path takes",
|
||||
"a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined"
|
||||
]
|
||||
},
|
||||
|
|
@ -378,7 +379,7 @@
|
|||
"name": "PlayerTurn",
|
||||
"status": "partial",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"writes": 3,
|
||||
"blockedWrites": 7,
|
||||
"rng": 0
|
||||
},
|
||||
|
|
@ -386,7 +387,7 @@
|
|||
"driver": "ServerPlayer::ProcessTurn",
|
||||
"id": "P01",
|
||||
"name": "ComputeBudget",
|
||||
"status": "blocked",
|
||||
"status": "partial",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 0,
|
||||
|
|
@ -396,9 +397,9 @@
|
|||
"driver": "ServerPlayer::ProcessTurn",
|
||||
"id": "P02",
|
||||
"name": "ApplyNetToSavings",
|
||||
"status": "blocked",
|
||||
"status": "partial",
|
||||
"ran": 0,
|
||||
"writes": 0,
|
||||
"writes": 3,
|
||||
"blockedWrites": 3,
|
||||
"rng": 0
|
||||
},
|
||||
|
|
@ -997,9 +998,9 @@
|
|||
"id": "T31",
|
||||
"name": "UpdateBankruptcyLimits",
|
||||
"status": "blocked",
|
||||
"ran": 0,
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 0,
|
||||
"blockedWrites": 1,
|
||||
"rng": 0
|
||||
},
|
||||
{
|
||||
|
|
@ -1071,31 +1072,31 @@
|
|||
"note": "real End Turn",
|
||||
"exit": 0,
|
||||
"stdout": [
|
||||
"load: /srv/re-lab/build/sre/verify/results/saves/turn2-state.sav",
|
||||
"load: /home/alex/sots-re/verify/results/saves/turn2-state.sav",
|
||||
" 603360 inflated bytes, 0 error(s), 0 warning(s)",
|
||||
" turn 2, frame 2, modCount 12, 8 player(s), 28 system(s), 7 fleet(s)",
|
||||
"roundtrip (untouched): byte-identical (603360 bytes)",
|
||||
"data: no root given (--data DIR / $SOTS_DATA_DIR); the tail's ship census stays unmodelled",
|
||||
"",
|
||||
"phases",
|
||||
" turn drivers (the milestone's denominator): 16 of 44 modelled, 10 committed",
|
||||
" verified 0 implemented 4 partial 6 blocked 6 stub 28",
|
||||
" turn drivers (the milestone's denominator): 16 of 44 modelled, 12 committed",
|
||||
" verified 0 implemented 4 partial 8 blocked 4 stub 28",
|
||||
" post-combat tail (written to the autosave, tracked separately): 5 of 37 modelled",
|
||||
" verified 0 implemented 1 partial 2 blocked 2 stub 32",
|
||||
"",
|
||||
"this run",
|
||||
" leaves written 23",
|
||||
" leaves NOT written by a blocked phase 66",
|
||||
" leaves written 29",
|
||||
" leaves NOT written by a blocked phase 67",
|
||||
" generator words consumed 16 (state loaded, left untouched)",
|
||||
" generator words NOT accounted (never netted off the above):",
|
||||
" - encounter detection draws one unit value and one bounded integer per turn on every turn measured (2 words), with no derived rule behind the count -- its bound is the product of the contact and detector counts, so it is left unmodelled",
|
||||
" - the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)",
|
||||
" - two draws are downstream of the budget's research allocation -- ProcessResearch's completion Chance and the tech-effect callback's own roll (0 or 1 word each). The allocation needs ComputeBudget's per-system money, which is ComputeOutput with the system's OWN rate sliders; the max-income form of that money is now modelled and self-checked (see T31), but it is NOT the one this path takes",
|
||||
" - a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined",
|
||||
" ! the generator advanced during this run but the save keeps its original state (--commit-rng to write it)",
|
||||
" ! the modelled words are a LOWER BOUND on the turn's cost, so a committed generator is short by the unaccounted sites below and its drawn VALUES are not the game's",
|
||||
"",
|
||||
"wrote /tmp/tmpezqlouzs/post-turn2-state.sav (66882 bytes gzipped, 603360 inflated)",
|
||||
"metric -> /tmp/tmpezqlouzs/metric-turn2-state.sav.json"
|
||||
"wrote /tmp/tmp3mqpqe52/post-turn2-state.sav (66882 bytes gzipped, 603360 inflated)",
|
||||
"metric -> /tmp/tmp3mqpqe52/metric-turn2-state.sav.json"
|
||||
],
|
||||
"coverage": {
|
||||
"input": true,
|
||||
|
|
@ -1105,14 +1106,15 @@
|
|||
"roots": {
|
||||
"input": "4bed514f853332b9fa35b388c3aa5558",
|
||||
"oracle": "e9c161e311f8ef8fad6f1aa1903dcf3f",
|
||||
"ours": "987447a85f9fcded1afd868fca112ef6"
|
||||
"ours": "5c5eb87437866b1563ff19ba4702ef32"
|
||||
},
|
||||
"baselineDiverging": 108,
|
||||
"divergingAfterTurn": 87,
|
||||
"closed": 21,
|
||||
"divergingAfterTurn": 86,
|
||||
"closed": 22,
|
||||
"regressed": 0,
|
||||
"closedPaths": [
|
||||
"/Sim/Frame",
|
||||
"/Sim/players/Player[576 \"Independent Colony\"]/Sav",
|
||||
"/Sim/systems/Sys[112 \"Gamma Cephei\"]/ETS",
|
||||
"/Sim/systems/Sys[112 \"Gamma Cephei\"]/ltis",
|
||||
"/Sim/systems/Sys[112 \"Gamma Cephei\"]/ntdev",
|
||||
|
|
@ -1154,7 +1156,7 @@
|
|||
"/Sim/players/Player[16 \"re\"]/Events/Events/.[0]: 1 -> 2",
|
||||
"/Sim/players/Player[16 \"re\"]/Events/Events/.[EvTurn=3]: only-in-B",
|
||||
"/Sim/players/Player[16 \"re\"]/PvSav: 50000 -> 289688",
|
||||
"/Sim/players/Player[16 \"re\"]/Sav: 289688 -> 532369",
|
||||
"/Sim/players/Player[16 \"re\"]/Sav: 531773 -> 532369",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/BnkEl: -1815833 -> -1820206",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/BnkPr: -898837 -> -901002",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/Events/EvNxID: 2 -> 4",
|
||||
|
|
@ -1163,7 +1165,7 @@
|
|||
"/Sim/players/Player[32 \"Fane Lao\"]/FNG/FNGNum: 1 -> 3",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/Maint: 500 -> 1000",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/PvSav: 38100 -> 80751",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/Sav: 92651 -> 135486",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/Sav: 142259 -> 135486",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/ShipRecs/srb[0]: 1 -> 2",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/ShipRecs/srb[3]: 1 -> 2",
|
||||
"/Sim/players/Player[32 \"Fane Lao\"]/ShipRecs/sri[0]: 1 -> 2",
|
||||
|
|
@ -1180,17 +1182,17 @@
|
|||
"byteMatch": false,
|
||||
"standalone": {
|
||||
"schema": "sots-standalone-metric/1",
|
||||
"input": "/srv/re-lab/build/sre/verify/results/saves/turn2-state.sav",
|
||||
"output": "/tmp/tmpezqlouzs/post-turn2-state.sav",
|
||||
"input": "/home/alex/sots-re/verify/results/saves/turn2-state.sav",
|
||||
"output": "/tmp/tmp3mqpqe52/post-turn2-state.sav",
|
||||
"spine": {
|
||||
"total": 44,
|
||||
"verified": 0,
|
||||
"implemented": 4,
|
||||
"partial": 6,
|
||||
"blocked": 6,
|
||||
"partial": 8,
|
||||
"blocked": 4,
|
||||
"stub": 28,
|
||||
"modelled": 16,
|
||||
"committed": 10
|
||||
"committed": 12
|
||||
},
|
||||
"tail": {
|
||||
"total": 37,
|
||||
|
|
@ -1203,14 +1205,14 @@
|
|||
"committed": 3
|
||||
},
|
||||
"run": {
|
||||
"leafWrites": 23,
|
||||
"blockedLeafWrites": 66,
|
||||
"leafWrites": 29,
|
||||
"blockedLeafWrites": 67,
|
||||
"rngWords": 16,
|
||||
"rngLoaded": true,
|
||||
"rngCommitted": false,
|
||||
"rngUnaccounted": [
|
||||
"encounter detection draws one unit value and one bounded integer per turn on every turn measured (2 words), with no derived rule behind the count -- its bound is the product of the contact and detector counts, so it is left unmodelled",
|
||||
"the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)",
|
||||
"two draws are downstream of the budget's research allocation -- ProcessResearch's completion Chance and the tech-effect callback's own roll (0 or 1 word each). The allocation needs ComputeBudget's per-system money, which is ComputeOutput with the system's OWN rate sliders; the max-income form of that money is now modelled and self-checked (see T31), but it is NOT the one this path takes",
|
||||
"a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined"
|
||||
]
|
||||
},
|
||||
|
|
@ -1361,7 +1363,7 @@
|
|||
"name": "PlayerTurn",
|
||||
"status": "partial",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"writes": 3,
|
||||
"blockedWrites": 4,
|
||||
"rng": 0
|
||||
},
|
||||
|
|
@ -1369,7 +1371,7 @@
|
|||
"driver": "ServerPlayer::ProcessTurn",
|
||||
"id": "P01",
|
||||
"name": "ComputeBudget",
|
||||
"status": "blocked",
|
||||
"status": "partial",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 0,
|
||||
|
|
@ -1379,9 +1381,9 @@
|
|||
"driver": "ServerPlayer::ProcessTurn",
|
||||
"id": "P02",
|
||||
"name": "ApplyNetToSavings",
|
||||
"status": "blocked",
|
||||
"status": "partial",
|
||||
"ran": 0,
|
||||
"writes": 0,
|
||||
"writes": 3,
|
||||
"blockedWrites": 3,
|
||||
"rng": 0
|
||||
},
|
||||
|
|
@ -1980,9 +1982,9 @@
|
|||
"id": "T31",
|
||||
"name": "UpdateBankruptcyLimits",
|
||||
"status": "blocked",
|
||||
"ran": 0,
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 0,
|
||||
"blockedWrites": 1,
|
||||
"rng": 0
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue