CB: the seed capture and its predictions -- the seeds are part of the stream

Lane L1 found each AI client's generator takes a fresh per-process word, so the AI is
MT19937 from one word per client and game/ai is a function of (save, seed). A capture
without the seeds records the answer without the input.

- aiseed=log|pin on StrategyApp::RunAI: one detour that reads the fourth stack argument
  where the callee reads it, and in pin mode replaces it. Both modes log observed AND
  used, so a pinned run says so in its own capture.
- aiorders.words= widens the element window past list 1's 48-byte horizon.
- aiorders.deep= follows the heap: the route vector, the counted vector and the
  Population body were ABSENT from every capture so far, not empty.
- tools/turncommands_capture.py (in sots-re) does the typing offline, so a corrected
  element record costs a re-parse rather than a VM run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
This commit is contained in:
alex 2026-09-08 18:59:24 -04:00
parent 4457249b88
commit c3d95dd44f
6 changed files with 509 additions and 8 deletions

View file

@ -179,3 +179,64 @@ Stated in advance so a green capture is not read as more than it is (rule 15):
impossible. Any `aivec` line on a list whose record has no vector is to be read as noise until a
second run reproduces it.
* Nothing here says what list 10 or list 23 *mean*. It says what bytes they carry.
---
# Addendum — the seeds are part of the stream (written before C3/C4/C5, after C0/C1 were driven)
The coordinator relayed lane L1's seed probe mid-lane: **every AI client's generator is seeded with
a fresh per-process 32-bit word**, three per process, none shared between processes. So the AI is
not non-deterministic — it is MT19937 from one word per client, and MT19937 is a generator this
campaign owns bit for bit. `game/ai` is a deterministic function of **(save, per-client seed)**.
That changes what a capture *is*. A block set without its seeds records the AI's **answer** without
its **input**: replayable, but not re-derivable and not re-runnable. Three words turn a log file
into a reproducible pair. The capture format therefore grows one field, and the instrument grows
one detour:
```
"ai_seeds": [ {"call": 1, "netId": 32, "observed": "0x…", "used": "0x…", "pinned": false}, … ]
```
`observed` is what the game chose; `used` is what the client actually got; `pinned` says the run
**forced** it. Every call logs all three, so a pinned run can never be mistaken for a natural one —
which matters because pinning is an intervention, not an instrument.
`aiseed=log` reads the fourth stack argument of `Game::StrategyApp::RunAI` where the callee will
read it and changes nothing. `aiseed=pin` **overwrites** it before the callee sees it.
## P8 — the new rule-26 control, stated as a prediction about method
Three lanes ran `hooks=off` on `turn1-state` and got **three different files**. So no single
un-instrumented run of that workload is a control, and an instrumented run agreeing with one of
them is a **~1/k coincidence** rather than evidence — with k = 6 by lane L4's naming of the `XNC`
set, that is a 17% chance of a false clean bill of health. **C0 is a valid control for
`ref-turn2 → turn3` and for nothing else.** Any rule-19 claim this lane makes about the turn-1
workload must come from a *pinned* pair, or it must not be made.
## P9 — C5: pinned, the same seeds, twice.
C5 is C3 re-run with `aiseed=pin` set to the seeds C3 itself observed. Predicted: **C5a and C5b
produce byte-identical autosaves**, and both are byte-identical to C3's, because the seed is the
only per-process input and it has been removed. If that holds, it is the first fully reproducible
creation-turn record this campaign has: block set, seeds and autosave, all from runs that agree.
*Falsifiers, each worth more than the prediction:*
* *C5a and C5b still differ.* Then the seed is **not** the only per-process input, L1's mechanism is
incomplete, and Rung B needs whatever else varies captured too. This is the single most valuable
outcome available in this lane.
* *C5 is reproducible but differs from C3.* Then pinning perturbs beyond the seed value — most
likely because the detour changes *when* the word is consumed, not only what it is.
* *`RunAI` is never entered.* Then the seed does not reach the client by this path on this workload
and the pin is a no-op that would look like a successful control. The instrument prints its own
entry count precisely so this is visible; a silent empty seed list would be exactly the rule-1
failure of a confident zero.
## P10 — the target gate's resolved name (lane RB's request)
RB's §163 asks the format to carry the **observed tech name** next to the target id, because the
target-gate handler cannot be modelled from the id alone. `airesearch=on` resolves it — phase 18
pushes the tech's `std::string` as a `char*`, which is why it is loggable at all. Predict the three
turn-1 target gates (ids 144, 90, 288) resolve to three names, of which player 512's is the one that
moves between runs. Adding those three detours is a **rule-19 risk on a workload that has no valid
control**, so it goes on the turn-1 runs only, never on the canonical pair.

View file

@ -25,7 +25,9 @@ FILE* g_out = nullptr;
void (*g_log)(const char*) = nullptr;
void LogF(const char* fmt, ...) {
char buf[1200];
// 4 KB, not 1200: lane CB's widened element window (`aiorders.words=64`) can put 64 hex words
// and 64 signed ints on one line, which is 1,408 characters before the prefix.
char buf[4096];
va_list ap;
va_start(ap, fmt);
std::vsnprintf(buf, sizeof buf, fmt, ap);
@ -77,7 +79,60 @@ constexpr int kListCount = 27;
// {i32, OutputRates frame}; 48 bytes covers every scalar-only record with room to spare and is
// short enough that a heap node's tail is unlikely to matter. Words that do not probe readable are
// printed as `????????` rather than as zero, so truncation is visible.
constexpr int kElemWords = 12;
//
// LANE CB widened this to a configurable window (`aiorders.words=`), because list 1's element is a
// polymorphic `ShipDesignDef` whose id sits past 48 bytes -- L4 §1 P3 could not read it and said
// so. The default is still 12, so a run that does not ask for more gets L4's exact behaviour.
constexpr int kMaxElemWords = 64;
int g_elemWords = 12;
// ---- the AI client seeds (lane CB, on lane L1's finding) ---------------------------------------
//
// L1 probed `Game::StrategyApp::RunAI` across two processes and found each AI client's generator is
// seeded with a FRESH per-process 32-bit word -- three words per process, none shared between
// processes. That reframes the whole Rung-B problem: the AI is not non-deterministic, it is
// MT19937 from one word per client, and MT19937 is a generator this campaign owns bit for bit. So
// `game/ai` is a deterministic function of (save, per-client seed), and
//
// THE SEEDS ARE PART OF THE STREAM.
//
// A capture that records the command block but not the seeds records the AI's *answer* without its
// *input*; it can be replayed but it cannot be re-derived, and it cannot be re-run. Three words
// turn a log file into a reproducible pair.
//
// TWO MODES, AND THE SECOND IS NOT AN INSTRUMENT -- IT IS AN INTERVENTION.
// `aiseed=log` reads the seed argument where the callee will read it and changes nothing.
// `aiseed=pin` OVERWRITES the caller's pushed argument before the callee sees it. That is the
// only way to take a control on a workload whose outcome set has size k > 1: three lanes ran
// `hooks=off` on `turn1-state` and got three different files, so no single un-instrumented run
// on that workload is a control at all, and an instrumented run agreeing with one of them is a
// ~1/k coincidence rather than evidence.
// Both are logged with the observed AND the used value on every call, so a pinned run says so in
// its own capture and can never be mistaken for a natural one.
enum class SeedMode { Off, Log, Pin };
SeedMode g_seedMode = SeedMode::Off;
constexpr std::size_t kMaxSeedPins = 8;
struct SeedPin {
int netId; // -1 == the wildcard `*`, applied to any client with no exact pin
std::uint32_t value;
};
SeedPin g_seedPins[kMaxSeedPins];
std::size_t g_seedPinCount = 0;
std::uint32_t g_seedCalls = 0;
// `aiorders.deep=` -- follow the heap out of the element window (lane CB).
//
// THREE PAYLOADS IN THE CANONICAL BLOCK ARE BEHIND POINTERS and are therefore simply ABSENT from a
// window dump: list 8's route vector, list 10's counted vector and list 23's `Population` body.
// A replayer cannot reconstruct a fleet move without the route, so "the capture is complete" was
// false while these were unread.
//
// The follower is deliberately GENERIC: it scans the window for anything shaped like an MSVC
// `{_Myfirst,_Mylast,_Myend}` and anything shaped like an MSVC `std::string`, and dumps the bytes.
// It does NOT know which list it is looking at, and it types nothing -- which keeps L4's design
// point (a wrong record shows up as a wrong value offline, instead of being baked into the
// instrument) and means a false positive is a visible extra line rather than a silent mis-decode.
bool g_deep = false;
std::uint32_t g_batchSeq = 0;
@ -270,17 +325,91 @@ static_assert(kProbeCount <= kMaxProbes, "add more AI_PROBE_STUB() slots");
// ---- the dump ---------------------------------------------------------------------------------
// ---- the deep scan (lane CB) --------------------------------------------------------------------
//
// Both detectors below are SHAPE tests on words the game handed us, and both can fire on a
// coincidence. That is stated in the report rather than tuned away: the thresholds bound how often
// it happens, and a `aivec`/`aistr` line on a list whose record has no vector or string is noise
// until a second run reproduces it.
// A plausible heap address. Below 64 KB is the null page; above 2 GB is kernel space on a 32-bit
// user process without /3GB, and no element in this block has ever pointed there.
inline bool PlausibleHeap(std::uint32_t p) { return p >= 0x00010000u && p < 0x80000000u; }
// `{_Myfirst, _Mylast, _Myend}` -- allocator LAST, so the three pointers are the first three words
// of the member and a fourth word is not needed to recognise it.
void ScanForVectors(int blk, int pid, int list, int idx, std::uintptr_t val, int words) {
int found = 0;
for (int i = 0; i + 2 < words && found < 8; ++i) {
const std::uintptr_t base = val + 4u * static_cast<unsigned>(i);
if (!Readable(base, 12)) continue;
const std::uint32_t first = U32(base), last = U32(base + 4), end = U32(base + 8);
if (!PlausibleHeap(first) || last < first || end < last) continue;
const std::uint32_t span = last - first, cap = end - first;
if ((span & 3u) || (cap & 3u) || cap > 4096u) continue;
const std::uint32_t count = span / 4u;
if (count > 256u) continue;
if (count && !Readable(first, span)) continue;
char hex[32 * 9 + 8] = {};
char ints[32 * 13 + 8] = {};
int hp = 0, ip = 0;
const std::uint32_t show = count < 32u ? count : 32u;
for (std::uint32_t w = 0; w < show; ++w) {
const std::uint32_t v = U32(first + 4u * w);
hp += std::snprintf(hex + hp, sizeof hex - hp, "%08x ", v);
ip += std::snprintf(ints + ip, sizeof ints - ip, "%d ", static_cast<int>(v));
}
LogF("aivec blk=%d pid=%d list=%d idx=%d at=w%d first=0x%08x cap=%u count=%u ints=[ %s] "
"hex=[ %s]%s",
blk, pid, list, idx, i, first, cap / 4u, count, ints, hex,
count > show ? " TRUNCATED" : "");
++found;
i += 2; // a match consumes its three words; overlapping reports are noise, not evidence
}
}
// MSVC `std::string`: `{union { char buf[16]; char* ptr }, _Mysize, _Myres, _Alval}` = 0x1c bytes,
// allocator last (method rule 5). `_Myres == 15` is the short-string case and the name is inline.
void ScanForStrings(int blk, int pid, int list, int idx, std::uintptr_t val, int words) {
int found = 0;
for (int i = 0; i + 5 < words && found < 4; ++i) {
const std::uintptr_t base = val + 4u * static_cast<unsigned>(i);
if (!Readable(base, 24)) continue;
const std::uint32_t size = U32(base + 16), res = U32(base + 20);
if (size > res || res > 0x1000u || res < 15u) continue;
char text[257] = {};
const std::uintptr_t chars =
(res == 15u) ? base : static_cast<std::uintptr_t>(U32(base));
if (res != 15u && !PlausibleHeap(static_cast<std::uint32_t>(chars))) continue;
const std::uint32_t n = size < 256u ? size : 256u;
if (n && !Readable(chars, n)) continue;
bool printable = true;
for (std::uint32_t c = 0; c < n; ++c) {
const std::uint8_t ch = U8(chars + c);
text[c] = static_cast<char>(ch);
if (ch < 0x20 || ch > 0x7e) printable = false;
}
if (!printable || n == 0) continue; // a non-printable "string" is a coincidence
LogF("aistr blk=%d pid=%d list=%d idx=%d at=w%d sso=%d len=%u text=\"%s\"", blk, pid, list,
idx, i, res == 15u ? 1 : 0, size, text);
++found;
i += 6;
}
}
void DumpElements(int blk, int pid, int list, std::uintptr_t head) {
// MSVC std::list node: {_Next, _Prev, _Myval}. begin() == _Myhead->_Next; the head is the nil
// sentinel and terminates the walk.
std::uintptr_t node = U32(head);
int idx = 0;
const int elemWords = g_elemWords;
while (node && node != head && idx < 64) {
const std::uintptr_t val = node + 8;
char hex[kElemWords * 9 + 8] = {};
char ints[kElemWords * 13 + 8] = {};
char hex[kMaxElemWords * 9 + 8] = {};
char ints[kMaxElemWords * 13 + 8] = {};
int hp = 0, ip = 0;
for (int w = 0; w < kElemWords; ++w) {
for (int w = 0; w < elemWords; ++w) {
const std::uintptr_t p = val + 4u * static_cast<unsigned>(w);
if (Readable(p, 4)) {
const std::uint32_t v = *reinterpret_cast<volatile std::uint32_t*>(p);
@ -293,9 +422,14 @@ void DumpElements(int blk, int pid, int list, std::uintptr_t head) {
}
// The first two words as floats as well: several element records lead with or contain a
// rate/fraction, and reading 0x3f4ccccd as 1061997773 hides that.
LogF("aielem blk=%d pid=%d list=%d idx=%d node=0x%08x f0=%g f1=%g ints=[ %s] hex=[ %s]",
blk, pid, list, idx, static_cast<unsigned>(node), static_cast<double>(F32(val)),
static_cast<double>(F32(val + 4)), ints, hex);
LogF("aielem blk=%d pid=%d list=%d idx=%d node=0x%08x words=%d f0=%g f1=%g ints=[ %s] "
"hex=[ %s]",
blk, pid, list, idx, static_cast<unsigned>(node), elemWords,
static_cast<double>(F32(val)), static_cast<double>(F32(val + 4)), ints, hex);
if (g_deep) {
ScanForVectors(blk, pid, list, idx, val, elemWords);
ScanForStrings(blk, pid, list, idx, val, elemWords);
}
node = U32(node);
++idx;
}
@ -602,7 +736,111 @@ _AiBatchDetour:
jmp *_g_aiBatchOrig
)");
// ---- the RunAI seed detour ----------------------------------------------------------------------
//
// `RunAI` is `__thiscall` with FOUR stack arguments and `ret 0x10`, so the caller pushed them and
// the callee reads them out of the caller's frame. At the stub's entry the return address is at
// esp+0 and `rngSeed` -- the fourth argument -- is at esp+0x10. After `pushfl` (4) + `pushal` (32)
// that is esp+52, and `netId` is esp+40. The stub hands the C function the NET ID BY VALUE and the
// SEED SLOT BY ADDRESS, which is what lets one function both read it and, in pin mode, replace it
// where the callee will look.
extern "C" void* g_aiSeedOrig;
void* g_aiSeedOrig = nullptr;
extern "C" void AiSeedDetour();
extern "C" void AiOnRunAI(int netId, std::uint32_t* seedSlot) {
++g_seedCalls;
const std::uint32_t observed = *seedSlot;
std::uint32_t used = observed;
if (g_seedMode == SeedMode::Pin) {
const SeedPin* chosen = nullptr;
for (std::size_t i = 0; i < g_seedPinCount; ++i)
if (g_seedPins[i].netId == netId) { chosen = &g_seedPins[i]; break; }
if (!chosen)
for (std::size_t i = 0; i < g_seedPinCount; ++i)
if (g_seedPins[i].netId < 0) { chosen = &g_seedPins[i]; break; }
if (chosen) {
*seedSlot = chosen->value;
used = chosen->value;
}
}
// `call` is the ordinal of this RunAI within the process, so a reader can bind the three seeds
// to the three clients even if two clients share a net id.
LogF("aiseed call=%u netId=%d observed=0x%08x used=0x%08x pinned=%d", g_seedCalls, netId,
observed, used, used != observed ? 1 : 0);
}
asm(R"(
.text
.globl _AiSeedDetour
_AiSeedDetour:
pushfl
pushal
leal 52(%esp), %eax
pushl %eax
pushl 44(%esp)
call _AiOnRunAI
addl $8, %esp
popal
popfl
jmp *_g_aiSeedOrig
)");
// `<netid>=<hex>` or `*=<hex>`, comma separated. A malformed entry is reported and the whole key
// is refused: a half-applied pin set is worse than none, because the run would look pinned.
bool ParseSeedPins(const char* value, std::string* err) {
g_seedPinCount = 0;
const char* p = value;
while (*p) {
while (*p == ' ' || *p == ',') ++p;
if (!*p) break;
if (g_seedPinCount >= kMaxSeedPins) {
if (err) *err = "too many seed pins";
return false;
}
int netId = -1;
if (*p == '*') {
++p;
} else {
char* end = nullptr;
netId = static_cast<int>(std::strtol(p, &end, 10));
if (end == p) {
if (err) *err = "expected <netid>=<hex> or *=<hex>";
return false;
}
p = end;
}
if (*p != '=') {
if (err) *err = "expected '=' after the net id";
return false;
}
++p;
char* end = nullptr;
const unsigned long v = std::strtoul(p, &end, 16);
if (end == p) {
if (err) *err = "expected a hex seed";
return false;
}
p = end;
g_seedPins[g_seedPinCount].netId = netId;
g_seedPins[g_seedPinCount].value = static_cast<std::uint32_t>(v);
++g_seedPinCount;
}
return true;
}
bool ai_orders_config(const char* key, const char* value, std::string* err) {
if (std::strcmp(key, "aiseed") == 0) {
if (std::strcmp(value, "off") == 0) g_seedMode = SeedMode::Off;
else if (std::strcmp(value, "log") == 0) g_seedMode = SeedMode::Log;
else if (std::strcmp(value, "pin") == 0) g_seedMode = SeedMode::Pin;
else if (err) *err = "expected off|log|pin";
return true;
}
if (std::strcmp(key, "aiseed.values") == 0) {
if (!ParseSeedPins(value, err)) g_seedPinCount = 0;
return true;
}
if (std::strcmp(key, "aiorders") == 0) {
if (std::strcmp(value, "on") == 0) g_enabled = true;
else if (std::strcmp(value, "off") == 0) g_enabled = false;
@ -613,6 +851,22 @@ bool ai_orders_config(const char* key, const char* value, std::string* err) {
std::snprintf(g_outPath, sizeof g_outPath, "%s", value);
return true;
}
if (std::strcmp(key, "aiorders.words") == 0) {
char* end = nullptr;
const long v = std::strtol(value, &end, 10);
if (end == value || v < 1) {
if (err) *err = "expected a word count >= 1";
return true;
}
g_elemWords = static_cast<int>(v < kMaxElemWords ? v : kMaxElemWords);
return true;
}
if (std::strcmp(key, "aiorders.deep") == 0) {
if (std::strcmp(value, "on") == 0) g_deep = true;
else if (std::strcmp(value, "off") == 0) g_deep = false;
else if (err) *err = "expected on|off";
return true;
}
if (std::strcmp(key, "airesearch") == 0) {
if (std::strcmp(value, "on") == 0) g_research = true;
else if (std::strcmp(value, "off") == 0) g_research = false;
@ -652,6 +906,20 @@ void install_ai_orders(std::uintptr_t exeBase, const char* gameDir, void (*log)(
if (!g_out) LogF("aiorders: cannot open %s -- output goes to shim.log only", g_outPath);
LogF("aiorders: out=%s probes=%u of %u", g_outPath,
static_cast<unsigned>(g_probeInstallCount), static_cast<unsigned>(kProbeCount));
// The capture has to be self-describing: an offline reader must not have to be told what
// window the words were taken at, and a capture with `deep=0` is INCOMPLETE rather than
// "a turn with no route", which is exactly the confusion method rule 20 is about.
LogF("aicfg words=%d deep=%d research=%d seed=%s pins=%u detours=%u", g_elemWords,
g_deep ? 1 : 0, g_research ? 1 : 0,
g_seedMode == SeedMode::Off ? "off" : (g_seedMode == SeedMode::Pin ? "pin" : "log"),
static_cast<unsigned>(g_seedPinCount),
static_cast<unsigned>(1 + g_probeInstallCount + (g_research ? 3u : 0u) +
(g_seedMode == SeedMode::Off ? 0u : 1u)));
if (g_seedMode == SeedMode::Pin && g_seedPinCount == 0)
LogF("aiseed: PIN MODE WITH NO PINS -- every seed passes through unchanged, so this run is "
"NOT pinned and must not be reported as one");
for (std::size_t i = 0; i < g_seedPinCount; ++i)
LogF("aiseed pin netId=%d value=0x%08x", g_seedPins[i].netId, g_seedPins[i].value);
void* target =
reinterpret_cast<void*>(exeBase + sots::addr::StrategySim_ApplyTurnCommandBatch);
@ -665,6 +933,19 @@ void install_ai_orders(std::uintptr_t exeBase, const char* gameDir, void (*log)(
LogF("COVERAGE: aiorders batch hook NOT INSTALLED -- no block will be dumped, and an empty "
"report means the instrument failed, not that the AI emitted nothing");
if (g_seedMode != SeedMode::Off) {
void* t = reinterpret_cast<void*>(exeBase + sots::addr::StrategyApp_RunAI);
MH_STATUS q1 = MH_CreateHook(t, reinterpret_cast<void*>(&AiSeedDetour), &g_aiSeedOrig);
MH_STATUS q2 = q1 == MH_OK ? MH_EnableHook(t) : q1;
LogF("aiseed: StrategyApp::RunAI rva=0x%08x va=%p create=%s enable=%s",
sots::addr::StrategyApp_RunAI, t, MH_StatusToString(q1), MH_StatusToString(q2));
if (q2 != MH_OK)
LogF("COVERAGE: aiseed hook NOT INSTALLED -- the capture will carry NO seeds, and an "
"absent seed list means the instrument failed, not that the clients were unseeded");
} else {
LogF("aiseed: disabled (aiseed=off) -- the capture will carry no seeds");
}
if (g_research) {
const struct {
const char* name;

View file

@ -0,0 +1,51 @@
# Lane CB -- THE CAPTURE. The deliverable config: ONE MinHook detour, the block dump at
# `StrategySim::ApplyTurnCommandBatch`, with the element window widened to 32 words and the heap
# follower on.
#
# WHY DEEP MATTERS AND WHY IT IS NAMED IN THE LOG. Three payloads in the canonical block are behind
# pointers -- list 8's route vector, list 10's counted vector, list 23's `Population` body. Without
# `aiorders.deep=on` they are ABSENT from the capture, and an absent route reads exactly like a
# fleet order with nowhere to go. The instrument prints `aicfg words= deep=` for that reason: a
# reader must never have to guess which of the two it is looking at (method rule 20).
#
# Everything else is off, including lane H's probe set (the one measured to move an autosave by four
# bytes) -- rule 19 is run against `shim.cfg.cboff`, which installs nothing at all.
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
trace.path=C:\SOTS\shim.trace.jsonl
trace.flush=always
probes=off
watch=off
# The trace framework installs its OWN template hook on this function (lane L1's seed probe), and
# MinHook refuses a second detour on the same target -- `MH_ERROR_ALREADY_CREATED`. The COVERAGE
# line the module prints when that happens is the only reason this was not a capture with a
# silently empty seed list, which is precisely the failure method rule 1 is about. Turning the
# template hook off leaves the detour count unchanged: one instrument on RunAI, not two.
hook.Game::StrategyApp::RunAI=off
aiorders.out=C:\SOTS\shim.aiorders.txt
aiorders=on
aiprobes=off
airesearch=off
aiorders.words=32
aiorders.deep=on
aiseed=log

6
src/shim/shim.cfg.cboff Normal file
View file

@ -0,0 +1,6 @@
# Lane CB, the rule-19 control: the shim DLL loads and forwards Bink and installs NOTHING.
# Identical in every other respect to the capture runs -- same binary, same guest, same workload,
# same click path -- so a difference between this run's autosaves and `shim.cfg.cbcapture`'s is the
# instrument and nothing else. A capture that changes the turn it records is worthless for a
# byte-match, and the only way to know is to take the same turn with the instrument removed.
hooks=off

49
src/shim/shim.cfg.cbpin Normal file
View file

@ -0,0 +1,49 @@
# Lane CB -- the pinned creation-turn capture. `shim.cfg.cbcapture` with `aiseed=pin`.
#
# WHY PINNING IS NOT CHEATING. Lane L1 showed every AI client's generator is seeded with a
# fresh per-process word, so `turn1-state -> turn2` has an outcome set of size k > 1 and NO
# un-instrumented run of it is a control -- three lanes ran `hooks=off` on it and got three
# different files. Pinning the seed removes the only per-process input, which is what makes a
# control possible at all on this workload. The seed values below are the ones a previous
# UNPINNED run of this same save observed, so the pinned run reproduces a turn that happened.
#
# `aiseed.values` is filled in per run; with none set the run is NOT pinned and says so.
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
trace.path=C:\SOTS\shim.trace.jsonl
trace.flush=always
probes=off
watch=off
# The trace framework installs its OWN template hook on this function (lane L1's seed probe), and
# MinHook refuses a second detour on the same target -- `MH_ERROR_ALREADY_CREATED`. The COVERAGE
# line the module prints when that happens is the only reason this was not a capture with a
# silently empty seed list, which is precisely the failure method rule 1 is about. Turning the
# template hook off leaves the detour count unchanged: one instrument on RunAI, not two.
hook.Game::StrategyApp::RunAI=off
aiorders.out=C:\SOTS\shim.aiorders.txt
aiorders=on
aiprobes=off
airesearch=off
aiorders.words=32
aiorders.deep=on
aiseed=pin

53
src/shim/shim.cfg.cbtrap Normal file
View file

@ -0,0 +1,53 @@
# Lane CB -- THE CAPTURE PLUS THE MODCOUNT TRAP. `shim.cfg.cbcapture` with lane W2's watchpoint
# module armed, and nothing else changed.
#
# TWO INSTRUMENTS, ONE PROCESS, FOR THE FIRST TIME. The watchpoint module arms DR0-3 from a detour
# on `StrategyServer::ApplyAllTurnCommands`, which is the CALLER of the function this lane's dump
# detours -- so the two are adjacent and have never run together. That is exactly why this is a
# SEPARATE config and a separate run: if its autosaves come off the oracle, this run is the one
# that perturbed and `shim.cfg.cbcapture` remains the capture (method rule 19).
#
# `watch.players=0` because this run wants the ModCount word and its writers' EIPs, not the two
# player Status words -- two fewer debug registers armed is two fewer ways to be wrong.
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
trace.path=C:\SOTS\shim.trace.jsonl
trace.flush=always
probes=off
watch=on
# The trace framework installs its OWN template hook on this function (lane L1's seed probe), and
# MinHook refuses a second detour on the same target -- `MH_ERROR_ALREADY_CREATED`. The COVERAGE
# line the module prints when that happens is the only reason this was not a capture with a
# silently empty seed list, which is precisely the failure method rule 1 is about. Turning the
# template hook off leaves the detour count unchanged: one instrument on RunAI, not two.
hook.Game::StrategyApp::RunAI=off
aiorders.out=C:\SOTS\shim.aiorders.txt
aiorders=on
aiprobes=off
airesearch=off
aiorders.words=32
aiorders.deep=on
watch.mode=modcount
watch.players=0
watch.out=C:\SOTS\shim.watch.txt
aiseed=log