The cost model for 0x006ad700 read from the instruction stream: nine live draw
sites, an exit taxonomy saying what each of the eight bail-outs has already
drawn, and a closed form for the loop-carried draw's trip count
f = 1.00 / 0.75 / 0.50 by request flags, hull size and one 0.3 coin
M = (int)(N * f)
D' = max(1, (N + 1) / M)
L = #{ qualifying mounts j : PointDefence section, or j mod D' == 0 }
with the falsifiable corollary that the 0.3 coin can only move the word count
at N in {1,2,3,5}, and that N=1 with f<1 enters the loop and draws nothing.
Also predicts client 32's seven turn-1 words as two composer calls -- a
costOnly=1 price query (A+B+C) and a costOnly=0 build (A+B+C+F) -- which is
read statically off 0x006cda40's two push sites, not fitted to the number.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
197 lines
11 KiB
Markdown
197 lines
11 KiB
Markdown
# 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.
|