New shim module src/shim/hooks/ai_orders.{h,cpp}: one register-transparent entry
stub on StrategySim::ApplyTurnCommandBatch dumps every submitted TurnCommands block
(six gates, 27 list lengths, element bytes) at the point where all of them are
complete in memory; sixteen entry probes, with RunTaskList's stub recording the
pass so every later hit is pass-attributed.
Two workloads on VM145, one End Turn each. The rule-19 control passed with all
seventeen detours installed: both autosaves byte-identical to the published oracle.
What the AI actually emits, and three things no reading had produced:
- a list-23 element on EVERY turn, the first element ever observed in the free
half of the cost table -- and both turns still cost the measured 12;
- the ids in AI commands are client-allocated and travel in the command (design
18, fleet 34; neither exists in the input save);
- pass 0 emits nothing, measured from element counts rather than inferred.
tests/game_ai/test_live_blocks.cpp rebuilds both captured blocks through the public
OrderClient API and asserts the list profile, element values, gate counts and
ModCount total: 44 checks. Kept separate from test_orders.cpp, which stays the
record of what static reading predicted.
Gates: clean_room_check OK, host ctest 55/55, CT111 shim cross-build exit 0.
81 lines
4.8 KiB
Markdown
81 lines
4.8 KiB
Markdown
# L4 — the AI command block, live
|
||
|
||
Companion to `docs/L4-predictions.md` (written and committed **before** the module existed) and to
|
||
`sots-re/findings/subsystems/ai-order-capture.md` (the full report, with the raw logs).
|
||
|
||
## What was added
|
||
|
||
`src/shim/hooks/ai_orders.{h,cpp}` — two instruments behind three config keys.
|
||
|
||
**`aiorders=on`** installs exactly one detour: a register-transparent entry stub on
|
||
`Game::StrategySim::ApplyTurnCommandBatch`. That function takes `(blocks, n)` as stack arguments and
|
||
multiplies `n` by `0x1b4` to make its end pointer, so at its entry every player's submitted
|
||
`TurnCommands` block is complete, in memory, at a known stride. One hook there dumps the whole
|
||
turn's command traffic: six gates, twenty-seven list lengths and 48 bytes per element, per block.
|
||
|
||
**`aiprobes=all`** adds sixteen entry counters in lane H's asm-stub style, in their own table so
|
||
`probe_entry.cpp`'s set is untouched and `probes=` still means what it meant. Row 0 is
|
||
`StrategyAIAgent::RunTaskList`, and its stub is hand-written: it reads that function's `pass` stack
|
||
argument into a global before tail-jumping, so **every later probe hit is attributed to a pass**.
|
||
That is the difference between counting entries and measuring the two-pass model.
|
||
|
||
**`aiorders.out=<path>`** — the dump goes to its own file as well as `shim.log`.
|
||
|
||
Both halves are separately switchable so they can be given separate rule-19 controls. In the end
|
||
they did not need to be: the full seventeen-detour configuration reproduced the campaign's published
|
||
End-Turn oracle byte for byte, on a guest (VM145) that had never been checked against it.
|
||
|
||
## What it found
|
||
|
||
Full account in the findings document. The three that change this repo:
|
||
|
||
1. **The AI submits a list-23 element every turn.** Lists 17–27 had never been populated by any
|
||
workload, so the free half of `ListAdvancesModCount` was read from the instruction stream and
|
||
nothing else. It is now exercised twice and both turns still cost the measured 12.
|
||
2. **Ids in AI commands are client-allocated.** The build order names design `18` and the fleet
|
||
order names fleet `34` — neither exists in the input save, and the *other* new design that turn
|
||
(a monster faction's, made server-side) took `1712` from the save's master counter. Two id
|
||
spaces; the small one travels in the command. `OrderClient` does not model id allocation and now
|
||
has a named reason to.
|
||
3. **Pass 0 emits nothing, measured from the output.** All three pass-1-gated exits were entered in
|
||
both passes in equal numbers, and the block carries one copy of each element. `OrderClient`'s
|
||
`EnterTaskPass` gate is confirmed.
|
||
|
||
## Tests
|
||
|
||
`tests/game_ai/test_live_blocks.cpp` — 44 checks, both captured blocks rebuilt through the public
|
||
`OrderClient` API from the dumped values, asserting the list profile, the element values, the gate
|
||
counts and the `ModCount` total for each turn, plus a two-sided check that the list-23 element is
|
||
free while a list-16 element is not.
|
||
|
||
It is deliberately a **separate binary** from `test_orders.cpp`. That file is the record of what
|
||
static reading predicted before any of this ran, and it stays that way; this one is the record of
|
||
what the game did. The agreement between them is evidence only while the two stay independent.
|
||
|
||
Two placeholder ids in `test_orders.cpp` were corrected in place from the capture (the AI fleet
|
||
order names fleet 34 with a hop to 272, not fleet 1744; the three research targets are techIds
|
||
144/90/288). Rule 11: a wrong id in a test is how a wrong id spreads.
|
||
|
||
## Gates
|
||
|
||
Run as separate commands (rule 13).
|
||
|
||
* `tools/clean_room_check.sh` — **OK**
|
||
* host `ctest --preset host` — **55/55**
|
||
* CT111 shim cross-build (`/srv/re-lab/build/sots-engine-l4`, `DIST=/srv/re-lab/shim/dist-l4`) —
|
||
**exit 0**, exports 66 names identical to the real `binkw32.dll`
|
||
|
||
The generated header was regenerated from `sots-re/ghidra/addresses.json` plus every
|
||
`ghidra/addresses.d/*.json` fragment (1,209 entries). This lane's fragment is `lane-l4.json`, nine
|
||
entries: eight `IAITask::Execute` bodies and the list-16 order method. Nine other entries in the
|
||
regenerated header belong to concurrent lanes' fragments and came along with the merge, as the
|
||
per-lane fragment mechanism intends.
|
||
|
||
## One thing that did not work, recorded because it costs a run
|
||
|
||
The turn-1 workload (`turn1-state.sav` + one End Turn) is **not reproducible**. Three runs produced
|
||
three different post-turn autosaves, differing in exactly one field: the research target of the one
|
||
AI player that owns nothing. Every other byte — ids, designs, fleets, `ModCount` — is identical.
|
||
`determinism-oracle.md` verifies the oracle for the `ref-turn2` workload and only for that one; this
|
||
lane is the first to run the turn-1 workload more than once, and it is now on the record that a
|
||
byte comparison on that workload is a comparison against noise.
|