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.
4.8 KiB
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:
- The AI submits a list-23 element every turn. Lists 17–27 had never been populated by any
workload, so the free half of
ListAdvancesModCountwas read from the instruction stream and nothing else. It is now exercised twice and both turns still cost the measured 12. - Ids in AI commands are client-allocated. The build order names design
18and the fleet order names fleet34— neither exists in the input save, and the other new design that turn (a monster faction's, made server-side) took1712from the save's master counter. Two id spaces; the small one travels in the command.OrderClientdoes not model id allocation and now has a named reason to. - 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'sEnterTaskPassgate 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 realbinkw32.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.