diff --git a/docs/CB-predictions.md b/docs/CB-predictions.md index e4ba23b..fcdbe6f 100644 --- a/docs/CB-predictions.md +++ b/docs/CB-predictions.md @@ -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. diff --git a/src/shim/hooks/ai_orders.cpp b/src/shim/hooks/ai_orders.cpp index 68ae3ae..ea009f1 100644 --- a/src/shim/hooks/ai_orders.cpp +++ b/src/shim/hooks/ai_orders.cpp @@ -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(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(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(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(U32(base)); + if (res != 15u && !PlausibleHeap(static_cast(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(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(w); if (Readable(p, 4)) { const std::uint32_t v = *reinterpret_cast(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(node), static_cast(F32(val)), - static_cast(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(node), elemWords, + static_cast(F32(val)), static_cast(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 +)"); + +// `=` or `*=`, 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(std::strtol(p, &end, 10)); + if (end == p) { + if (err) *err = "expected = or *="; + 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(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(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(g_probeInstallCount), static_cast(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(g_seedPinCount), + static_cast(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(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(exeBase + sots::addr::StrategyApp_RunAI); + MH_STATUS q1 = MH_CreateHook(t, reinterpret_cast(&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; diff --git a/src/shim/shim.cfg.cbcapture b/src/shim/shim.cfg.cbcapture new file mode 100644 index 0000000..f7112ad --- /dev/null +++ b/src/shim/shim.cfg.cbcapture @@ -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 diff --git a/src/shim/shim.cfg.cboff b/src/shim/shim.cfg.cboff new file mode 100644 index 0000000..798be1a --- /dev/null +++ b/src/shim/shim.cfg.cboff @@ -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 diff --git a/src/shim/shim.cfg.cbpin b/src/shim/shim.cfg.cbpin new file mode 100644 index 0000000..5467a80 --- /dev/null +++ b/src/shim/shim.cfg.cbpin @@ -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 diff --git a/src/shim/shim.cfg.cbtrap b/src/shim/shim.cfg.cbtrap new file mode 100644 index 0000000..a5eb044 --- /dev/null +++ b/src/shim/shim.cfg.cbtrap @@ -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