sots-engine/docs/L4-ai-orders.md

84 lines
5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
Lane L5 established this first and better, on VM146, and owns it
(`sots-re/findings/subsystems/turn1-to-turn2-nondeterminism.md`); use `ref-turn2 → turn3` as the
oracle, not this pair. What this lane adds is where the divergence lives: the differing value is in
the **submitted command block**, in player 512's research-target gate, so the decision is made
client-side before submission and the sim is not diverging on identical input.