diff --git a/docs/SD-predictions.md b/docs/SD-predictions.md new file mode 100644 index 0000000..17599c6 --- /dev/null +++ b/docs/SD-predictions.md @@ -0,0 +1,197 @@ +# Lane SD — predictions, written before the shim build + +Target: `AIComposeShipBlueprint 0x006ad700`, the AI ship-design composer. Lane PAR localised the +AI's whole RNG variance to it: on `turn1-state`, six of client 32's seven words are drawn inside it +and the seventh is drawn by a helper it calls. It holds nine live draw sites, including the only +loop-carried one. + +Everything below is derived from the **instruction stream only** (`0x006ad700`–`0x006ae61a`, swept +to the next function start per rule 17; the body ends with a `ret` at `0x006ae61a` and `int3` +padding to `0x006ae620`, so Ghidra's 3864-byte size is right on this one). Nothing here has been +run yet. The instrument is `sots-engine` `wip/sd`, `aidesign=on` in `src/shim/shim.cfg.sd*`. + +--- + +## The model + +One call is `int AIComposeShipBlueprint(ECX agent, EDX parts[3], STACK req*, STACK costOnly)`. +`req = {int hullSize; float budget; int role; uint flags}`. It returns 0 on success and 1..8 for +eight bail-outs. Its **only** caller `0x006ae620` runs it once per candidate hull size — 1, 2 or 3 +sizes depending on `flags & 1` and `flags & 2` — and **breaks on the first success**, so one design +request costs one to three composer calls and every failed attempt has already spent whatever it +drew before it failed. + +Draw sites, in execution order, named by the return address the airng tables know them by: + +| tag | ret | primitive | words | fires when | +|---|---|---|---|---| +| A | `0x006ad878` | `cl_RandFloat` → `NextFloat` | **1** | tech latch still ok **and** `(flags & 0x40) == 0` | +| B | `0x006ad94c` | `cl_RandRange(0, nH-1)` → `NextInt` | `E=(mask+1)/nH` | `parts[0].section == 0` and `nH > 0` (`nH` capped at 0x32) | +| C | `0x00691ea0` | `cl_RandRange(0, nC-1)` → `NextInt` | `E=(mask+1)/nC` | command slot empty, section not fixed, `nC > 0` | +| D | `0x006adf35` | `cl_Chance(0.5f)` | **1** | `K > 0`, where `K` = mounts with kind ∈ {0, 0xc, 0x17} and size 3 | +| E | `0x006adf44` | `cl_RandRange(0, K-1)` | `E=(mask+1)/K` | D accepted | +| — | `0x006adf62`, `0x006adf71` | | **dead** | `xor esi,esi / cmp esi,ebx / jle` with `ebx = 0` — confirmed by Ghidra removing both blocks | +| F | `0x006adfca` | `cl_Chance(0.8f)` | **1** | unconditional once the call gets this far | +| G | `0x006ae418` | `cl_Chance(0.3f)` | **1** | `(flags & 0x20000) == 0` **and** `req.hullSize > 0` | +| H | `0x006ae57a` | `cl_Chance(0.2f)` | **L** | **once per qualifying mount, see below** | + +`RNG_Chance` costs exactly one word at every one of these `p` values (0.2/0.3/0.5/0.8 are all +strictly inside `(0,1)`, so neither of its zero-word early-outs is reachable here). Only B, C and E +can cost more than one word per call, and only through `NextInt`'s rejection loop. + +### The loop-carried draw's trip count + +The tail is `for part in 0..2 { for mount in 0..part.mountCount }`. The **outer count is a literal +3** and never varies. The inner draw fires for a mount iff: + +* `mount.kind == 0 && mount.size == 1` (call these the **qualifying** mounts, `N` of them across all + three sections), **and** +* the section's name is `DEPointDefence` or `CRPointDefence` (case-insensitive) — those take every + qualifying mount — **or** the mount's **global** qualifying index `j` satisfies `j mod D' == 0`, + **and** +* the restricted weapon lookup at `0x006ae3c1` returned non-null (else the site short-circuits with + no call), and the default weapon lookup at `0x006ae3dc` returned non-null (else the whole tail + block is skipped), and `M > 0`. + +with + +``` +f = 1.00 if (flags & 0x20000) == 0 and req.hullSize > 0 and site G accepted + = 0.75 if (flags & 0x20000) != 0, or G was drawn and refused + = 0.50 if (flags & 0x20000) == 0 and req.hullSize <= 0 +M = (int)(N * f) float32 multiply, floor, truncate — non-negative, so one cast +D' = max(1, (N + 1) / M) integer division +L = #{ j in [0,N) : section(j) is *PointDefence, or j mod D' == 0 } +``` + +Consequences that make this falsifiable rather than decorative: + +* `f = 1.00` ⇒ `D' = 1` ⇒ **`L = N`**. +* `f = 0.75` ⇒ `L = 0, 1, 2, 4, 3, 6, 7, 8` for `N = 1..8` — note it is **not** monotone: `N = 5` + costs 3 and `N = 4` costs 4, because `D'` jumps. +* `f = 0.50` ⇒ `L = 0, 1, 1, 2, 2, 3, 4, 4` for `N = 1..8`. +* `N = 1` with `f < 1` gives `M = 0` and **zero** draws — a whole loop that is entered and costs + nothing (method rule 20's shape, and the reason the probe logs `N` next to `H_obs`). +* The 0.3 coin at G therefore changes the word count **only when `N = 1`, `N = 2`, `N = 3` or + `N = 5`**. For `N = 4` and `N ≥ 6` both `f = 0.75` and `f = 1.00` give `D' = 1` and the same `L`. + +### Exit taxonomy — what a failed attempt has already paid + +| rc | where | drawn before it returns | +|---|---|---| +| 5 / 6 / 7 | a forced-tech lookup for flag bit 4 / 8 / 0x10 | **nothing** | +| 2 | no hull-section candidate | A | +| 3 | no command section | A + B | +| 4 | no engine section | A + B + C | +| 1 | over budget | A + B + C | +| — | `costOnly != 0` returns here | A + B + C | +| 8 | no weapon matched a mount | A + B + C + D + E + F | +| 0 | success | everything | + +--- + +## Predictions + +### P1 — the model holds, per call + +On **every** composer row the probe emits, `H_pred == H_obs`, where `H_pred` is computed by the +probe from `N`, the three per-section point-defence flags and `f` (with `f` taken from the request +flags, the hull size, and G's **observed accept/refuse**, which the extended `cl_Chance` detour now +records). + +*Falsified by:* any row printing `model=WRONG`. A row printing `model=GATED-OR-WRONG` (`H_pred > 0` +but `H_obs == 0`) is **not** a falsification on its own — it is the weapon-lookup gate — but it is +also not a confirmation, and if every row is `GATED-OR-WRONG` the model is untested and I will say +so rather than claim it held. + +### P2 — one design request, two composer calls, and the first is a price query + +`0x006cda40` calls the driver twice: once with `costOnly = 1` (statically read at `0x006cda9a`, +`push 0x1`) and, if that succeeded, once with `costOnly = 0` (`0x006cdb17`, `push 0x0`). So a turn +in which the AI designs one ship should show **two** composer rows: one with `dry=1` that returns +`rc=0` having drawn **A, B, C and nothing else**, and one with `dry=0` that goes further. + +This is the arithmetic of client 32's seven turn-1 words, and it is the prediction I most want +checked because it was derived to explain a number rather than measured: + +``` +call 1 (dry=1): A 1 + B 1 + C 1 = 3 words +call 2 (dry=0): A 1 + B 1 + C 1 + F 1 = 4 words + total 7 <- PAR's measured 7 +``` +with `D`, `E`, `G`, `H` all zero: `K = 0` (no size-3 mounts on a turn-1 hull), and G silent because +`req.hullSize <= 0` on the smallest hull. + +*Falsified by:* one composer row, or three; or `dry = 0` on both; or `F` firing on the dry call. + +### P3 — pinned seeds reproduce exactly + +Two runs of `turn1-state → turn2` with `airng.pin_seed=5A17C0DE` and `aidesign=on`, in two fresh +processes, produce **byte-identical `aidesign*` rows** — every field, every row, in order. + +*Falsified by:* any differing field. That would mean the composer's path depends on something the +bracket-entry re-seed does not pin, which would be a bigger finding than the model. + +### P4 — a different pinned seed moves the count, and moves it through the path + +A run with `airng.pin_seed=B16B00B5` differs from P3's runs in at least one of: + +* site A's coin (`RandFloat() >= 0.5`) landing the other way, which sets request-flag bit `0x40`, + forces a tech section and therefore **changes the sections, `N`, and possibly `H`**; +* the number of words `B` or `C` spends, `NextInt`'s rejection loop resolving differently. + +The total for client 32 is **not** required to differ — PAR's §3.4 already showed a pinned run +holding the count at 3 while replacing the whole stream. What is required is that if the *path* +changes, `H_pred` tracks it: `model=HOLDS` must survive the seed change. + +*Falsified by:* `model=WRONG` appearing only under the second seed, which would mean the model is +fitted to one design rather than derived. + +### P5 — the instrument is armed, and says so + +`shim.airng.txt` must contain +`aidesign: composer 0x006ad700 rva=0x002ad700 va=... create=MH_OK enable=MH_OK`. +Lane L3 found a configuration that printed `watch=on` and armed nothing; a missing or failed hook +here would produce **no `aidesign` rows at all**, which is indistinguishable from "the composer was +never called" unless the arming line is read. If that line is absent or not `MH_OK`, every zero in +this lane's output is void and the run is discarded, not interpreted. + +Second arming check, independent of the log line: the `airngcall`/`airngsite` totals for the +bracket must equal the sum of the `aidesign` rows' `sub_words`. If the composer hook silently failed +the sub-bracket would be zero while the turn total stayed 7. + +### P6 — rule 19: the composer detour does not change the game + +With `aidesign=off` (and `airng=on`, unpinned), two fresh processes on `turn1-state` must both +produce the published post-turn autosave `d59bb9f2fd0eb535`. With `aidesign=on`, unpinned, two more +fresh processes must produce the same file. + +The composer detour patches five bytes at a clean prologue boundary (`push ebp; mov ebp,esp; +push -1` is exactly 5 bytes, and the function has one caller and no internal branch target below +`0x006ad705`), but lane H's finding is that a *correctly placed* patch changed the autosave anyway +and the mechanism is still unknown. So this is measured, not argued. + +*Falsified by:* any of the four runs producing a different file. If the `aidesign=off` pair +disagrees **with each other**, the workload is the problem and no control exists (rule 26); I will +say so and fall back to pinned comparisons only. + +--- + +## How this could be wrong + +1. **`N` is read after the call, from the design the call produced.** The composer counts `N` during + its weapon-assignment pass over slots that *received* a weapon; if a slot was skipped, my + post-hoc count is high and `H_pred` is too big. Symptom: `H_pred > H_obs` by a small amount on + rows with `rc = 8`. This is why the rc is in the row. +2. **`part.mountCount` is `min(hull mounts, 0x32)`.** I clamp to the hull's own vector length as + well; if the two disagree the probe prints both and I will notice. +3. **`f` for the `flags & 0x20000` case is an inference from one `fld` at `0x006ae3f8`.** If bit + `0x20000` is never set on this board the term is untested, and I will label it rule-6 rather than + claim it. +4. **The dead-code claim on `0x006adf62`/`0x006adf71`** is static. Ghidra independently removed both + blocks as unreachable, which is a second reading of the same instructions and not a second + instrument. If either ever appears in an `airngcall` row, the claim is wrong. +5. **The measurement could double-count.** PAR's instrument reported one word twice because `Chance` + calls `NextFloat` and both were detoured; it caught that only because it took two independent + measurements. This lane's sub-bracket takes two as well — `left` read off the generator object + (`words`) and the observer sum (`sub_words`) — and prints both on every row. They must agree. diff --git a/src/shim/shim.cfg.sdbr b/src/shim/shim.cfg.sdbr new file mode 100644 index 0000000..caa2662 --- /dev/null +++ b/src/shim/shim.cfg.sdbr @@ -0,0 +1,57 @@ +# Lane SD -- THE SHIP-DESIGN COMPOSER 0x006ad700. FOUR CONFIGS, DIFFERING BY TWO KEYS. +# +# sdoff airng=on aidesign=off the rule-19 control: the airng bracket +# alone, exactly lane PAR's parbr +# sdbr airng=on aidesign=on the composer probe, UNPINNED +# sdpin airng=on aidesign=on pin_seed=5A17C0DE pinned; two fresh processes must agree +# sdpin2 airng=on aidesign=on pin_seed=B16B00B5 pinned, a DIFFERENT seed +# +# sdoff and sdbr must produce the SAME autosave as each other and as the published unpinned result +# for the workload. The two sdpin configs deliberately change the AI's stream and their autosaves +# are never compared against the oracle (method rule 19). +# +# `aidesign=on` installs ONE extra detour, on 0x006ad700. If its arming line does not say +# create=MH_OK enable=MH_OK, every `aidesign` row is absent and an absent row is NOT a measured +# zero (lane L3's `hooks=off watch=on` trap). +hooks=trace +hook.Shim::SelfTest::Fill=off +hook.Mars::GlobalConsts::LoadFile=off +hook.Game::WeaponDictionary::Init=off +hook.Game::SectionDictionary::SectionDictionary=off +hook.Game::TechTree::ProcessResearch=off +hook.Game::ServerPlayer::ComputeBudget=off +hook.Game::ServerPlayer::OnTechResearched=off +hook.Game::ServerPlayer::ProcessTurn=off +hook.Game::ServerSystem::ProcessTurn=off +hook.Game::ServerSystem::GroupOutput=off +hook.Game::ServerSystem::ComputeTotalOutput=off +hook.Game::StrategyServer::MoveFleet=off +hook.Game::StrategyServer::ProcessFleetMovement=off +hook.Game::StrategyHost::Autosave=off +hook.Game::StrategyServer::ProcessTurn=off +hook.Game::StrategyServer::OnAllCombatDone_Tail=off +hook.Game::StrategyServer::ApplyEncounterResult=off +hook.Game::StrategyServer::NodeLineDecay=off +hook.Game::StrategyServer::ProcessNodeSpaceTravel=off +hook.Game::EncounterDetect::AssignContacts=off +hook.Game::EncounterDetect::ProcessTeamRecord=off +hook.Game::StrategyServer::BeginProcessTurn=off +hook.Game::SVSOSwarmQueen::OnTurnBegin=off +hook.Game::SVSOSwarmQueen::RegisterHives=off +hook.Game::SVSOSwarmQueen::TickHives=off +hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off +hook.Mars::RNG::Seed=off +hook.Game::StrategyApp::RunAI=off +fpu.sample_turn=off +fpu.sample_ticks=off +probes=off +watch=off +aiorders=off +aiprobes=off +airesearch=off +trace.path=C:\SOTS\shim.trace.jsonl +trace.flush=always +airng.out=C:\SOTS\shim.airng.txt +airng=on +aidesign=on +airng.pin_seed=off diff --git a/src/shim/shim.cfg.sdoff b/src/shim/shim.cfg.sdoff new file mode 100644 index 0000000..4806197 --- /dev/null +++ b/src/shim/shim.cfg.sdoff @@ -0,0 +1,57 @@ +# Lane SD -- THE SHIP-DESIGN COMPOSER 0x006ad700. FOUR CONFIGS, DIFFERING BY TWO KEYS. +# +# sdoff airng=on aidesign=off the rule-19 control: the airng bracket +# alone, exactly lane PAR's parbr +# sdbr airng=on aidesign=on the composer probe, UNPINNED +# sdpin airng=on aidesign=on pin_seed=5A17C0DE pinned; two fresh processes must agree +# sdpin2 airng=on aidesign=on pin_seed=B16B00B5 pinned, a DIFFERENT seed +# +# sdoff and sdbr must produce the SAME autosave as each other and as the published unpinned result +# for the workload. The two sdpin configs deliberately change the AI's stream and their autosaves +# are never compared against the oracle (method rule 19). +# +# `aidesign=on` installs ONE extra detour, on 0x006ad700. If its arming line does not say +# create=MH_OK enable=MH_OK, every `aidesign` row is absent and an absent row is NOT a measured +# zero (lane L3's `hooks=off watch=on` trap). +hooks=trace +hook.Shim::SelfTest::Fill=off +hook.Mars::GlobalConsts::LoadFile=off +hook.Game::WeaponDictionary::Init=off +hook.Game::SectionDictionary::SectionDictionary=off +hook.Game::TechTree::ProcessResearch=off +hook.Game::ServerPlayer::ComputeBudget=off +hook.Game::ServerPlayer::OnTechResearched=off +hook.Game::ServerPlayer::ProcessTurn=off +hook.Game::ServerSystem::ProcessTurn=off +hook.Game::ServerSystem::GroupOutput=off +hook.Game::ServerSystem::ComputeTotalOutput=off +hook.Game::StrategyServer::MoveFleet=off +hook.Game::StrategyServer::ProcessFleetMovement=off +hook.Game::StrategyHost::Autosave=off +hook.Game::StrategyServer::ProcessTurn=off +hook.Game::StrategyServer::OnAllCombatDone_Tail=off +hook.Game::StrategyServer::ApplyEncounterResult=off +hook.Game::StrategyServer::NodeLineDecay=off +hook.Game::StrategyServer::ProcessNodeSpaceTravel=off +hook.Game::EncounterDetect::AssignContacts=off +hook.Game::EncounterDetect::ProcessTeamRecord=off +hook.Game::StrategyServer::BeginProcessTurn=off +hook.Game::SVSOSwarmQueen::OnTurnBegin=off +hook.Game::SVSOSwarmQueen::RegisterHives=off +hook.Game::SVSOSwarmQueen::TickHives=off +hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off +hook.Mars::RNG::Seed=off +hook.Game::StrategyApp::RunAI=off +fpu.sample_turn=off +fpu.sample_ticks=off +probes=off +watch=off +aiorders=off +aiprobes=off +airesearch=off +trace.path=C:\SOTS\shim.trace.jsonl +trace.flush=always +airng.out=C:\SOTS\shim.airng.txt +airng=on +aidesign=off +airng.pin_seed=off diff --git a/src/shim/shim.cfg.sdpin b/src/shim/shim.cfg.sdpin new file mode 100644 index 0000000..24f697f --- /dev/null +++ b/src/shim/shim.cfg.sdpin @@ -0,0 +1,57 @@ +# Lane SD -- THE SHIP-DESIGN COMPOSER 0x006ad700. FOUR CONFIGS, DIFFERING BY TWO KEYS. +# +# sdoff airng=on aidesign=off the rule-19 control: the airng bracket +# alone, exactly lane PAR's parbr +# sdbr airng=on aidesign=on the composer probe, UNPINNED +# sdpin airng=on aidesign=on pin_seed=5A17C0DE pinned; two fresh processes must agree +# sdpin2 airng=on aidesign=on pin_seed=B16B00B5 pinned, a DIFFERENT seed +# +# sdoff and sdbr must produce the SAME autosave as each other and as the published unpinned result +# for the workload. The two sdpin configs deliberately change the AI's stream and their autosaves +# are never compared against the oracle (method rule 19). +# +# `aidesign=on` installs ONE extra detour, on 0x006ad700. If its arming line does not say +# create=MH_OK enable=MH_OK, every `aidesign` row is absent and an absent row is NOT a measured +# zero (lane L3's `hooks=off watch=on` trap). +hooks=trace +hook.Shim::SelfTest::Fill=off +hook.Mars::GlobalConsts::LoadFile=off +hook.Game::WeaponDictionary::Init=off +hook.Game::SectionDictionary::SectionDictionary=off +hook.Game::TechTree::ProcessResearch=off +hook.Game::ServerPlayer::ComputeBudget=off +hook.Game::ServerPlayer::OnTechResearched=off +hook.Game::ServerPlayer::ProcessTurn=off +hook.Game::ServerSystem::ProcessTurn=off +hook.Game::ServerSystem::GroupOutput=off +hook.Game::ServerSystem::ComputeTotalOutput=off +hook.Game::StrategyServer::MoveFleet=off +hook.Game::StrategyServer::ProcessFleetMovement=off +hook.Game::StrategyHost::Autosave=off +hook.Game::StrategyServer::ProcessTurn=off +hook.Game::StrategyServer::OnAllCombatDone_Tail=off +hook.Game::StrategyServer::ApplyEncounterResult=off +hook.Game::StrategyServer::NodeLineDecay=off +hook.Game::StrategyServer::ProcessNodeSpaceTravel=off +hook.Game::EncounterDetect::AssignContacts=off +hook.Game::EncounterDetect::ProcessTeamRecord=off +hook.Game::StrategyServer::BeginProcessTurn=off +hook.Game::SVSOSwarmQueen::OnTurnBegin=off +hook.Game::SVSOSwarmQueen::RegisterHives=off +hook.Game::SVSOSwarmQueen::TickHives=off +hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off +hook.Mars::RNG::Seed=off +hook.Game::StrategyApp::RunAI=off +fpu.sample_turn=off +fpu.sample_ticks=off +probes=off +watch=off +aiorders=off +aiprobes=off +airesearch=off +trace.path=C:\SOTS\shim.trace.jsonl +trace.flush=always +airng.out=C:\SOTS\shim.airng.txt +airng=on +aidesign=on +airng.pin_seed=5A17C0DE diff --git a/src/shim/shim.cfg.sdpin2 b/src/shim/shim.cfg.sdpin2 new file mode 100644 index 0000000..a04dc39 --- /dev/null +++ b/src/shim/shim.cfg.sdpin2 @@ -0,0 +1,57 @@ +# Lane SD -- THE SHIP-DESIGN COMPOSER 0x006ad700. FOUR CONFIGS, DIFFERING BY TWO KEYS. +# +# sdoff airng=on aidesign=off the rule-19 control: the airng bracket +# alone, exactly lane PAR's parbr +# sdbr airng=on aidesign=on the composer probe, UNPINNED +# sdpin airng=on aidesign=on pin_seed=5A17C0DE pinned; two fresh processes must agree +# sdpin2 airng=on aidesign=on pin_seed=B16B00B5 pinned, a DIFFERENT seed +# +# sdoff and sdbr must produce the SAME autosave as each other and as the published unpinned result +# for the workload. The two sdpin configs deliberately change the AI's stream and their autosaves +# are never compared against the oracle (method rule 19). +# +# `aidesign=on` installs ONE extra detour, on 0x006ad700. If its arming line does not say +# create=MH_OK enable=MH_OK, every `aidesign` row is absent and an absent row is NOT a measured +# zero (lane L3's `hooks=off watch=on` trap). +hooks=trace +hook.Shim::SelfTest::Fill=off +hook.Mars::GlobalConsts::LoadFile=off +hook.Game::WeaponDictionary::Init=off +hook.Game::SectionDictionary::SectionDictionary=off +hook.Game::TechTree::ProcessResearch=off +hook.Game::ServerPlayer::ComputeBudget=off +hook.Game::ServerPlayer::OnTechResearched=off +hook.Game::ServerPlayer::ProcessTurn=off +hook.Game::ServerSystem::ProcessTurn=off +hook.Game::ServerSystem::GroupOutput=off +hook.Game::ServerSystem::ComputeTotalOutput=off +hook.Game::StrategyServer::MoveFleet=off +hook.Game::StrategyServer::ProcessFleetMovement=off +hook.Game::StrategyHost::Autosave=off +hook.Game::StrategyServer::ProcessTurn=off +hook.Game::StrategyServer::OnAllCombatDone_Tail=off +hook.Game::StrategyServer::ApplyEncounterResult=off +hook.Game::StrategyServer::NodeLineDecay=off +hook.Game::StrategyServer::ProcessNodeSpaceTravel=off +hook.Game::EncounterDetect::AssignContacts=off +hook.Game::EncounterDetect::ProcessTeamRecord=off +hook.Game::StrategyServer::BeginProcessTurn=off +hook.Game::SVSOSwarmQueen::OnTurnBegin=off +hook.Game::SVSOSwarmQueen::RegisterHives=off +hook.Game::SVSOSwarmQueen::TickHives=off +hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off +hook.Mars::RNG::Seed=off +hook.Game::StrategyApp::RunAI=off +fpu.sample_turn=off +fpu.sample_ticks=off +probes=off +watch=off +aiorders=off +aiprobes=off +airesearch=off +trace.path=C:\SOTS\shim.trace.jsonl +trace.flush=always +airng.out=C:\SOTS\shim.airng.txt +airng=on +aidesign=on +airng.pin_seed=B16B00B5