lane H: entry-probe module, ProcessTeamRecord hook, probes= config, and the outcome of the five predictions
This commit is contained in:
parent
a01305fcf2
commit
a26cd183f1
18 changed files with 1027 additions and 4 deletions
|
|
@ -100,11 +100,15 @@ if(WIN32)
|
||||||
src/shim/hooks/fleet_movement.cpp
|
src/shim/hooks/fleet_movement.cpp
|
||||||
src/shim/hooks/player_turn.cpp
|
src/shim/hooks/player_turn.cpp
|
||||||
src/shim/hooks/tail_rng.cpp
|
src/shim/hooks/tail_rng.cpp
|
||||||
src/shim/hooks/draw_sites.cpp)
|
src/shim/hooks/draw_sites.cpp
|
||||||
|
src/shim/hooks/probe_entry.cpp)
|
||||||
|
# `minhook` is here for its include directory: lane H's probe_entry.cpp installs its own
|
||||||
|
# detours (MH_CreateHook/MH_EnableHook) rather than handing descriptors back to main.cpp,
|
||||||
|
# because a register-transparent asm stub has no C++ prototype for the template to take.
|
||||||
target_link_libraries(shim_hooks PUBLIC shim_trace sots_addresses sots_game_config sots_game_sim
|
target_link_libraries(shim_hooks PUBLIC shim_trace sots_addresses sots_game_config sots_game_sim
|
||||||
sots_game_effects mars_rng shim_budget shim_techfx
|
sots_game_effects mars_rng shim_budget shim_techfx
|
||||||
shim_colony shim_movement shim_events
|
shim_colony shim_movement shim_events
|
||||||
shim_player_turn shim_rng_ledger)
|
shim_player_turn shim_rng_ledger minhook)
|
||||||
target_compile_options(shim_hooks PRIVATE -Wall -Wextra -Werror)
|
target_compile_options(shim_hooks PRIVATE -Wall -Wextra -Werror)
|
||||||
|
|
||||||
add_library(binkw32 SHARED src/shim/main.cpp src/shim/fpu_force.cpp src/shim/binkw32.def)
|
add_library(binkw32 SHARED src/shim/main.cpp src/shim/fpu_force.cpp src/shim/binkw32.def)
|
||||||
|
|
|
||||||
|
|
@ -271,3 +271,45 @@ count, only a call count — which is deliberate, because the word counts alread
|
||||||
independent instruments that agree.
|
independent instruments that agree.
|
||||||
|
|
||||||
A clean run here is a statement about RNG accounting and coverage, and about nothing else.
|
A clean run here is a statement about RNG accounting and coverage, and about nothing else.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Outcome — written after the runs
|
||||||
|
|
||||||
|
Everything above this line was committed before the shim was built. This section is the adjudication;
|
||||||
|
the full report with the evidence is `sots-re/findings/control-flow/tail-probes.md`.
|
||||||
|
|
||||||
|
| prediction | verdict |
|
||||||
|
|---|---|
|
||||||
|
| **P1** oracle reproduces | **held.** `bb4fd9ac89f41e3b…` and `978041acd168b56e…`, both byte-exact |
|
||||||
|
| **P2a** four outer callees entered once per End Turn | **held.** 1 per turn, 3 turns, 2 saves, every row |
|
||||||
|
| **P2b** none of the eight draw sites fires on the corpus | **held.** No draw-site row at any of the eight return addresses; tail cost 0 |
|
||||||
|
| **P2c** the three inner callees entered 0 times | **held.** The gate is inside each outer body, above the inner call |
|
||||||
|
| **P2b-alt** the fleet loop makes slot 13 draw on a fleet-rich save | **falsified.** 23 fleets, slot 13 entered, 0x00820ca0 entered zero times — the gate is the per-fleet test chain |
|
||||||
|
| **P3a** `ProcessTeamRecord` called once per record | **vacuous.** It was called **zero** times; the record vector is empty |
|
||||||
|
| **P3b** the 2 detection words are inside `ProcessTeamRecord` | **falsified**, exactly as its falsification clause anticipated. They are in 0x007d5150's subtree, one call from `DetectEncounters` |
|
||||||
|
| **P3c** the 2 words are not the `AssignContacts` family | **held**, and for a stronger reason than predicted: not a failed gate, an empty vector |
|
||||||
|
| **P4** `CreateRaidEncounter` entered ~2/turn, drawing 0 | **held.** 2 entries on one turn, 1 on the next, 0 words on all of them — candidate list empty |
|
||||||
|
| **P5** 7 calls / 7 words per site on a Zuul save, 14 not 16 | **held exactly**, on two consecutive turns |
|
||||||
|
|
||||||
|
## The thing no prediction covered
|
||||||
|
|
||||||
|
**The instrument changed the game.** Three End Turns under the full lane-H build produced 19/18/19
|
||||||
|
words where lane Z recorded 19/18/20, from a *byte-identical* input save, and an autosave 4 bytes
|
||||||
|
different from the one lane Z's runs produced twice. Six single-End-Turn runs from `z2-endturn.sav`
|
||||||
|
bisected it to one address: a MinHook detour on `ServerTradeManager_GenerateTradeRaidEncounters`
|
||||||
|
0x00893290. `hooks=off`, the template hooks alone, and the first eight probes all reproduce the
|
||||||
|
un-instrumented result; adding the ninth does not.
|
||||||
|
|
||||||
|
Two consequences for this repo:
|
||||||
|
|
||||||
|
1. `probes=` was added to `shim.cfg` so the probe set is switchable and bisectable. Every number in
|
||||||
|
the report was re-taken at `probes=8`, the largest configuration proved byte-identical to the
|
||||||
|
un-instrumented game.
|
||||||
|
2. **Lane Z's ledger is behaviour-neutral and its published numbers stand** — that had never been
|
||||||
|
checked, and it came out the right way.
|
||||||
|
|
||||||
|
The mechanism is undetermined. The five bytes MinHook patches at 0x00893290 land on a clean
|
||||||
|
instruction boundary (`push ebp; mov ebp,esp; push -1`) with no branch target inside them, so a
|
||||||
|
mangled prologue is not the explanation. The suppressed draw is `RollResearchEvent`'s at 0x0088df4f,
|
||||||
|
which runs *earlier in the turn* than the hooked function — which is the part to chase.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
||||||
// Source: sots-re ghidra/addresses.json @ 58e3d85, generated 2026-09-08 by tools/gen_addresses.py
|
// Source: sots-re ghidra/addresses.json @ 52581ef, generated 2026-09-08 by tools/gen_addresses.py
|
||||||
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
@ -1361,6 +1361,12 @@ constexpr uint32_t StrategyServer_DestroyFleet = 0x0048b980;
|
||||||
constexpr uint32_t StrategyServer_OrderFleetMove = 0x004653c0;
|
constexpr uint32_t StrategyServer_OrderFleetMove = 0x004653c0;
|
||||||
// thiscall void** (std::map<int, void*>* this, const int* key) // 125 B, ret 4. MSVC std::map<int,T*>::operator[]: _Lbound over the tree from this->_Myhead(+0x04)->_Parent, testing _Isnil at node+0x15 and the key at node+0x0c; if found returns &node->_Myval.second (node+0x10), else default-inserts the pair {key, 0} via _Buynode 0x008b91d0 + _Insert 0x0072b400 and returns the same. NODE IS 0x18 BY ENUMERATION from _Buynode's operator new(0x18): _Left +0x00, _Parent +0x04, _Right +0x08, pair<int,void*> at +0x0c/+0x10, and _Color/_Isnil written as ONE 16-bit store at +0x14/+0x15, plus 2 bytes padding. DELEGATED instruction-level read [verified]
|
// thiscall void** (std::map<int, void*>* this, const int* key) // 125 B, ret 4. MSVC std::map<int,T*>::operator[]: _Lbound over the tree from this->_Myhead(+0x04)->_Parent, testing _Isnil at node+0x15 and the key at node+0x0c; if found returns &node->_Myval.second (node+0x10), else default-inserts the pair {key, 0} via _Buynode 0x008b91d0 + _Insert 0x0072b400 and returns the same. NODE IS 0x18 BY ENUMERATION from _Buynode's operator new(0x18): _Left +0x00, _Parent +0x04, _Right +0x08, pair<int,void*> at +0x0c/+0x10, and _Color/_Isnil written as ONE 16-bit store at +0x14/+0x15, plus 2 bytes padding. DELEGATED instruction-level read [verified]
|
||||||
constexpr uint32_t Map_IntPtr_Subscript = 0x0036bce0;
|
constexpr uint32_t Map_IntPtr_Subscript = 0x0036bce0;
|
||||||
|
// thiscall int (void* this, int, int) // the ONE callee through which Game::ServerSpyManager vftable 0x00a3073c slot 13 (0x008877b0) reaches the strategic generator. Exactly ONE incoming reference in the image: an unconditional call at 0x00887af2 inside that slot. Real body 0x008408e0..0x00840a59, 370 bytes. Contains three of lane V2's eight draw sites -- Chance 0x00840929, NextInt 0x008409c7, Chance 0x00840a3c -- all loading the generator as [reg+0x16c]. Named for the verified relationship (which slot reaches it, and that it draws), NOT for any semantics: its body was not read. Lane H hooks it with a register-transparent entry counter so that 'the spy path never fired' can be split into 'slot 13 was not entered' and 'slot 13 was entered and gated above this call' [mapped]
|
||||||
|
constexpr uint32_t SpyManager_Slot13RngCallee = 0x004408e0;
|
||||||
|
// thiscall uint (void* fleet, char) // first of the two callees through which Game::ServerTradeManagerImpl vftable 0x00a31b74 slot 13 (0x0088ef80) reaches the strategic generator. Exactly ONE incoming reference: an unconditional call at 0x0088f036. Real body 0x00820ca0..0x00820e53, 436 bytes, containing lane V2's NextFloat site 0x00820e18. Its own body loops the vector at arg+0xa4/+0xa8 testing per-element flags, and its boolean result is what gates the call to TradeManager_Slot13RngCalleeB one instruction later. THE CALLER'S LOOP IS OVER THE FLEETS VECTOR at GetServer()+0x64/+0x68, not over a trade-route list -- which is why 'the tail draws nothing because we have no trade routes' does not explain this slot [mapped]
|
||||||
|
constexpr uint32_t TradeManager_Slot13RngCalleeA = 0x00420ca0;
|
||||||
|
// thiscall void (void* this, int) // second of the two callees through which Game::ServerTradeManagerImpl slot 13 (0x0088ef80) reaches the strategic generator, called at 0x0088f042 ONLY when TradeManager_Slot13RngCalleeA returned non-zero. Exactly ONE incoming reference. Real body 0x0088b440..0x0088b976, 1323 bytes, containing lane V2's NextInt site 0x0088b613 (`mov ecx,[ecx+0x16c]; add ecx,4`). The two form a two-stage gate: A draws first and decides whether B runs, so a run where A fires and B does not is a MEASUREMENT, not a gap [mapped]
|
||||||
|
constexpr uint32_t TradeManager_Slot13RngCalleeB = 0x0048b440;
|
||||||
// thiscall uint32_t (Mars::RNG* this /*ecx = THE OBJECT, not &mt*/) // plain RET, no stack args. THE FOURTH DRAW ENTRY POINT. Whole 84-byte body read from the instruction stream: `cmp [ecx+0x9c8],0; push esi; lea esi,[ecx+4]; jne skip; mov ecx,esi; call RNG_Twist; skip: eax=[esi+0x9c0]; dec [esi+0x9c4]; ecx=*eax; eax+=4; [esi+0x9c0]=eax;` then the standard Mars temper (shr 11 / and 0xff3a58ad shl 7 / and 0xffffdf8c shl 15 / shr 18) and `ret`. EXACTLY ONE MT WORD, UNCONDITIONAL -- no rejection loop, no early-out, no branch except the lazy twist. Contrast RNG_NextFloat and RNG_NextInt, which are entered with ECX = &mt = obj+4; this one takes the object and does the +4 itself. Body ends 0x004f76c3 (Ghidra's 84 is correct here), then 12 int3 to 0x004f76d0. 11 callers image-wide; in StrategyServer::ProcessTurn's direct-call closure at DEPTH 4 via ProcessFleetMovement 0x007da9a0 -> MoveFleet 0x007d9ee0 -> ProbabilisticJump 0x007b6700 @0x007b67e7 [verified]
|
// thiscall uint32_t (Mars::RNG* this /*ecx = THE OBJECT, not &mt*/) // plain RET, no stack args. THE FOURTH DRAW ENTRY POINT. Whole 84-byte body read from the instruction stream: `cmp [ecx+0x9c8],0; push esi; lea esi,[ecx+4]; jne skip; mov ecx,esi; call RNG_Twist; skip: eax=[esi+0x9c0]; dec [esi+0x9c4]; ecx=*eax; eax+=4; [esi+0x9c0]=eax;` then the standard Mars temper (shr 11 / and 0xff3a58ad shl 7 / and 0xffffdf8c shl 15 / shr 18) and `ret`. EXACTLY ONE MT WORD, UNCONDITIONAL -- no rejection loop, no early-out, no branch except the lazy twist. Contrast RNG_NextFloat and RNG_NextInt, which are entered with ECX = &mt = obj+4; this one takes the object and does the +4 itself. Body ends 0x004f76c3 (Ghidra's 84 is correct here), then 12 int3 to 0x004f76d0. 11 callers image-wide; in StrategyServer::ProcessTurn's direct-call closure at DEPTH 4 via ProcessFleetMovement 0x007da9a0 -> MoveFleet 0x007d9ee0 -> ProbabilisticJump 0x007b6700 @0x007b67e7 [verified]
|
||||||
constexpr uint32_t Mars_RNG_NextUInt = 0x000f7670;
|
constexpr uint32_t Mars_RNG_NextUInt = 0x000f7670;
|
||||||
// thiscall float (Mars::RNG* this /*ecx = THE OBJECT*/, float lo, float hi) // RET 8. FIFTH DRAW ENTRY POINT, in no previous lane's primitive set. `add ecx,4; call RNG_NextFloat` then `lo + (float)((hi-lo) * unit)`, with the product STORED TO A FLOAT before the add and the sum stored to a float again -- two roundings, both must be reproduced. EXACTLY ONE MT WORD. In StrategyServer::ProcessTurn's closure at depth 3 via ServerPlayer::ProcessTurn -> 0x00889dc0 (call sites 0x0088a1bd, 0x0088a20f) [verified]
|
// thiscall float (Mars::RNG* this /*ecx = THE OBJECT*/, float lo, float hi) // RET 8. FIFTH DRAW ENTRY POINT, in no previous lane's primitive set. `add ecx,4; call RNG_NextFloat` then `lo + (float)((hi-lo) * unit)`, with the product STORED TO A FLOAT before the add and the sum stored to a float again -- two roundings, both must be reproduced. EXACTLY ONE MT WORD. In StrategyServer::ProcessTurn's closure at depth 3 via ServerPlayer::ProcessTurn -> 0x00889dc0 (call sites 0x0088a1bd, 0x0088a20f) [verified]
|
||||||
|
|
|
||||||
204
src/shim/hooks/probe_entry.cpp
Normal file
204
src/shim/hooks/probe_entry.cpp
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
#include "shim/hooks/probe_entry.h"
|
||||||
|
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "MinHook.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "generated/sots_addresses.h"
|
||||||
|
|
||||||
|
// ---- the counters ------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Plain 32-bit counters, incremented from a stub that has already saved the flags, so no atomic is
|
||||||
|
// needed for correctness of the arithmetic on x86 and none is used: the turn pipeline is
|
||||||
|
// single-threaded (lane Z §7) and a torn read here would cost a count, not a crash. If a probe ever
|
||||||
|
// reads a wild number, that is itself the finding -- it would mean the address is entered off the
|
||||||
|
// turn thread.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr std::size_t kMaxProbes = 16;
|
||||||
|
std::uint32_t g_calls[kMaxProbes];
|
||||||
|
std::uint32_t g_total[kMaxProbes];
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" void ProbeHit(int index) {
|
||||||
|
if (index < 0 || static_cast<std::size_t>(index) >= kMaxProbes) return;
|
||||||
|
++g_calls[index];
|
||||||
|
++g_total[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- one trampoline slot and one asm stub per probe ---------------------------------------------
|
||||||
|
//
|
||||||
|
// The stub is written out per index rather than generated at runtime because a runtime-generated
|
||||||
|
// thunk would need an executable allocation and a relocation, and MinHook already owns that
|
||||||
|
// problem. Sixteen slots is the cap; `kProbes` below is shorter and the rest are inert.
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define PROBE_STUB(i) \
|
||||||
|
extern "C" void* g_tr_probe##i; \
|
||||||
|
void* g_tr_probe##i = nullptr; \
|
||||||
|
extern "C" void ProbeStub##i(void); \
|
||||||
|
asm(".text\n" \
|
||||||
|
".globl _ProbeStub" #i "\n" \
|
||||||
|
"_ProbeStub" #i ":\n" \
|
||||||
|
" pushfl\n" \
|
||||||
|
" pushal\n" \
|
||||||
|
" pushl $" #i "\n" \
|
||||||
|
" call _ProbeHit\n" \
|
||||||
|
" addl $4, %esp\n" \
|
||||||
|
" popal\n" \
|
||||||
|
" popfl\n" \
|
||||||
|
" jmp *_g_tr_probe" #i "\n")
|
||||||
|
|
||||||
|
PROBE_STUB(0);
|
||||||
|
PROBE_STUB(1);
|
||||||
|
PROBE_STUB(2);
|
||||||
|
PROBE_STUB(3);
|
||||||
|
PROBE_STUB(4);
|
||||||
|
PROBE_STUB(5);
|
||||||
|
PROBE_STUB(6);
|
||||||
|
PROBE_STUB(7);
|
||||||
|
PROBE_STUB(8);
|
||||||
|
PROBE_STUB(9);
|
||||||
|
PROBE_STUB(10);
|
||||||
|
PROBE_STUB(11);
|
||||||
|
#undef PROBE_STUB
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace shim::hooks {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
namespace A = sots::addr;
|
||||||
|
|
||||||
|
struct ProbeDef {
|
||||||
|
const char* name;
|
||||||
|
std::uint32_t rva;
|
||||||
|
void* stub;
|
||||||
|
void** trampoline;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::uintptr_t g_exe_base = 0;
|
||||||
|
void (*g_log)(const char*) = nullptr;
|
||||||
|
bool g_installed[kMaxProbes];
|
||||||
|
|
||||||
|
void logf(const char* fmt, ...) {
|
||||||
|
if (!g_log) return;
|
||||||
|
char line[512];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
std::vsnprintf(line, sizeof line, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
g_log(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define PROBE(name, rva, i) ProbeDef{name, rva, reinterpret_cast<void*>(&ProbeStub##i), &g_tr_probe##i}
|
||||||
|
#else
|
||||||
|
#define PROBE(name, rva, i) ProbeDef{name, rva, nullptr, nullptr}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The probe set. Order is the report order and the stub index, so it is fixed.
|
||||||
|
//
|
||||||
|
// Rows 0-3 are the four phase-23/33 callees lane V2 proved can reach the generator; rows 4-6 are
|
||||||
|
// the three inner functions those bodies reach the draw *through*, so a row-0..3 hit with no
|
||||||
|
// row-4..6 hit localises the gate to the outer body. Rows 7-8 are the trade-raid pair (P4). Rows
|
||||||
|
// 9-11 are CONTROLS, and they are the reason a zero in rows 0-6 can be believed: 9 and 10 are two
|
||||||
|
// phase-23/33 callees lane V2 measured as draw-free, and 11 is `EncounterDetect_Run`, which every
|
||||||
|
// prior lane has observed running once per turn. If the controls read 0 the instrument is broken,
|
||||||
|
// not the game (method rule 1 -- a hook that compares nothing prints a clean verdict).
|
||||||
|
const ProbeDef kProbes[] = {
|
||||||
|
PROBE("Game::ServerSpyManager::vslot13", A::ServerSpyManager_vslot13, 0),
|
||||||
|
PROBE("Game::ServerSpyManager::vslot14", A::ServerSpyManager_vslot14, 1),
|
||||||
|
PROBE("Game::ServerTradeManagerImpl::vslot13", A::ServerTradeManagerImpl_vslot13, 2),
|
||||||
|
PROBE("Game::ServerTradeManagerImpl::vslot15", A::ServerTradeManagerImpl_vslot15, 3),
|
||||||
|
PROBE("Game::SpyManager::Slot13RngCallee", A::SpyManager_Slot13RngCallee, 4),
|
||||||
|
PROBE("Game::TradeManager::Slot13RngCalleeA", A::TradeManager_Slot13RngCalleeA, 5),
|
||||||
|
PROBE("Game::TradeManager::Slot13RngCalleeB", A::TradeManager_Slot13RngCalleeB, 6),
|
||||||
|
PROBE("Game::ServerTradeManager::CreateRaidEncounter", A::ServerTradeManager_CreateRaidEncounter, 7),
|
||||||
|
PROBE("Game::ServerTradeManager::GenerateTradeRaidEncounters",
|
||||||
|
A::ServerTradeManager_GenerateTradeRaidEncounters, 8),
|
||||||
|
PROBE("Game::ServerSpyManager::vslot15 [control]", A::ServerSpyManager_vslot15, 9),
|
||||||
|
PROBE("Game::ServerTradeManagerImpl::vslot14 [control]", A::ServerTradeManagerImpl_vslot14, 10),
|
||||||
|
PROBE("Game::EncounterDetect::Run [control]", A::EncounterDetect_Run, 11),
|
||||||
|
};
|
||||||
|
#undef PROBE
|
||||||
|
|
||||||
|
constexpr std::size_t kProbeCount = sizeof kProbes / sizeof kProbes[0];
|
||||||
|
static_assert(kProbeCount <= kMaxProbes, "add more PROBE_STUB() slots");
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::size_t g_install_count = kProbeCount; // `probes=` overrides; default is all of them
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool probe_config(const char* key, const char* value, std::size_t* count_out) {
|
||||||
|
if (std::strcmp(key, "probes") != 0) return false;
|
||||||
|
if (std::strcmp(value, "off") == 0 || std::strcmp(value, "none") == 0) {
|
||||||
|
g_install_count = 0;
|
||||||
|
} else if (std::strcmp(value, "all") == 0 || std::strcmp(value, "on") == 0) {
|
||||||
|
g_install_count = kProbeCount;
|
||||||
|
} else {
|
||||||
|
char* end = nullptr;
|
||||||
|
const long n = std::strtol(value, &end, 10);
|
||||||
|
if (end == value || n < 0) return true; // ours, but unparseable: leave the default
|
||||||
|
g_install_count = static_cast<std::size_t>(n) < kProbeCount
|
||||||
|
? static_cast<std::size_t>(n)
|
||||||
|
: kProbeCount;
|
||||||
|
}
|
||||||
|
if (count_out) *count_out = g_install_count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_probe_entries(std::uintptr_t exe_base, void (*log_line)(const char* line)) {
|
||||||
|
g_exe_base = exe_base;
|
||||||
|
g_log = log_line;
|
||||||
|
}
|
||||||
|
|
||||||
|
void install_probe_entries() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
logf("probe: installing %u of %u (probes= in shim.cfg)",
|
||||||
|
static_cast<unsigned>(g_install_count), static_cast<unsigned>(kProbeCount));
|
||||||
|
for (std::size_t i = 0; i < g_install_count; ++i) {
|
||||||
|
void* target = reinterpret_cast<void*>(g_exe_base + kProbes[i].rva);
|
||||||
|
MH_STATUS s1 = MH_CreateHook(target, kProbes[i].stub, kProbes[i].trampoline);
|
||||||
|
MH_STATUS s2 = s1 == MH_OK ? MH_EnableHook(target) : s1;
|
||||||
|
g_installed[i] = (s2 == MH_OK);
|
||||||
|
logf("probe: %s rva=0x%08x -> va=%p create=%s enable=%s", kProbes[i].name, kProbes[i].rva,
|
||||||
|
target, MH_StatusToString(s1), MH_StatusToString(s2));
|
||||||
|
if (!g_installed[i])
|
||||||
|
logf("COVERAGE: probe %s NOT INSTALLED -- its call count is meaningless, not zero",
|
||||||
|
kProbes[i].name);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
for (std::size_t i = 0; i < kProbeCount; ++i) g_installed[i] = false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void probe_entries_reset() {
|
||||||
|
for (std::size_t i = 0; i < kProbeCount; ++i) g_calls[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t probe_entries_count() { return kProbeCount; }
|
||||||
|
|
||||||
|
std::size_t probe_entries_snapshot(ProbeEntryRow* out, std::size_t max) {
|
||||||
|
const std::size_t n = kProbeCount < max ? kProbeCount : max;
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
out[i].name = kProbes[i].name;
|
||||||
|
out[i].rva = kProbes[i].rva;
|
||||||
|
out[i].calls = g_calls[i];
|
||||||
|
out[i].total = g_total[i];
|
||||||
|
out[i].installed = g_installed[i];
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace shim::hooks
|
||||||
69
src/shim/hooks/probe_entry.h
Normal file
69
src/shim/hooks/probe_entry.h
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
// Lane H -- entry counters for functions whose *reachability* is the open question.
|
||||||
|
//
|
||||||
|
// WHY THIS IS NOT A TEMPLATE HOOK, AND NOT A DRAW-SITE DETOUR.
|
||||||
|
//
|
||||||
|
// Lane V2 found four of the eleven phase-23/33 vtable targets on `StrategyServer+0x158`/`+0x15c`
|
||||||
|
// reach a draw on the strategic generator, and lane Z measured 0 tail words over eight turns. Those
|
||||||
|
// two facts leave a question a word count cannot answer: were the callees **not reached**, or
|
||||||
|
// **reached and gated**? The consequences are opposite. "Not reached" means the phase is
|
||||||
|
// conditional and a reimplementation may skip it; "reached and gated" means the phase always runs
|
||||||
|
// and the gate is a data condition our saves never satisfy -- which is a latent divergence exactly
|
||||||
|
// like the node-line decay lane K found.
|
||||||
|
//
|
||||||
|
// The instrument therefore has to answer "did control enter this address", for functions whose
|
||||||
|
// prototypes are **not verified**. Lane V2 read the dispatch sites, not the bodies: it recorded
|
||||||
|
// `void (this)` for the vtable slots from the pushes at the call site, and nothing at all for the
|
||||||
|
// three inner functions. Declaring a C++ detour with a guessed calling convention and then calling
|
||||||
|
// the trampoline is how a shim corrupts a stack -- and the campaign has already paid for that once
|
||||||
|
// (`main.cpp`: v1 of the Initialize hook was a thiscall wrapper and crashed the game).
|
||||||
|
//
|
||||||
|
// So each probe is a **register-transparent asm stub**: `pushfl; pushal; push <index>;
|
||||||
|
// call ProbeHit; add esp,4; popal; popfl; jmp *trampoline`. It saves and restores every register
|
||||||
|
// and the flags, touches nothing but its own counter, and tail-jumps to MinHook's trampoline so
|
||||||
|
// the ORIGINAL's own `ret` does the stack cleanup. It cannot be wrong about a calling convention
|
||||||
|
// because it never assumes one, it cannot perturb arguments, and it works for `ret 0`, `ret 4`,
|
||||||
|
// `cdecl`, `thiscall` and `fastcall` alike.
|
||||||
|
//
|
||||||
|
// WHAT IT CANNOT DO, stated because it is the price of that safety: a tail-jumping stub never
|
||||||
|
// regains control, so it cannot read the generator after the call and **cannot report a word
|
||||||
|
// count** -- only a call count. That is deliberate and costs nothing here, because the word counts
|
||||||
|
// already come from two independent instruments that agree (the boundary ledger and the
|
||||||
|
// return-address detours in `draw_sites.cpp`). A probe entry row and a draw-site row are meant to
|
||||||
|
// be read together: `calls > 0` with no matching draw-site row is precisely "reached and gated".
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace shim::hooks {
|
||||||
|
|
||||||
|
struct ProbeEntryRow {
|
||||||
|
const char* name = nullptr;
|
||||||
|
std::uint32_t rva = 0;
|
||||||
|
std::uint32_t calls = 0; // entries since the last reset
|
||||||
|
std::uint32_t total = 0; // entries since process start (never reset)
|
||||||
|
bool installed = false; // false => MinHook refused; `calls == 0` means NOTHING for this row
|
||||||
|
};
|
||||||
|
|
||||||
|
// `log_line` is used once per probe at install time. Nothing logs per call: some of these run
|
||||||
|
// inside the turn pipeline and a line per entry would be a different experiment.
|
||||||
|
void init_probe_entries(std::uintptr_t exe_base, void (*log_line)(const char* line));
|
||||||
|
|
||||||
|
// Installs the probes. Safe to call once, after MH_Initialize.
|
||||||
|
void install_probe_entries();
|
||||||
|
|
||||||
|
// Config, from `probes=` in shim.cfg. Returns false if `key` is not ours.
|
||||||
|
//
|
||||||
|
// WHY THIS KEY EXISTS, and it is not convenience. Lane H measured a real perturbation: with these
|
||||||
|
// probes installed, one End Turn from a byte-identical input save produced an autosave 4 bytes
|
||||||
|
// different from the one the un-instrumented game produces, and spent 19 generator words where the
|
||||||
|
// game spends 20. An instrument that changes the thing it measures has to be switchable, or the
|
||||||
|
// question "which of the twelve does it" cannot be asked. `probes=off` installs none; `probes=N`
|
||||||
|
// installs the first N in the fixed table order, which makes the set bisectable in one build.
|
||||||
|
bool probe_config(const char* key, const char* value, std::size_t* count_out);
|
||||||
|
|
||||||
|
void probe_entries_reset();
|
||||||
|
std::size_t probe_entries_snapshot(ProbeEntryRow* out, std::size_t max);
|
||||||
|
std::size_t probe_entries_count();
|
||||||
|
|
||||||
|
} // namespace shim::hooks
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include "generated/sots_addresses.h"
|
#include "generated/sots_addresses.h"
|
||||||
#include "mars/rng/mt19937.h"
|
#include "mars/rng/mt19937.h"
|
||||||
#include "shim/hooks/draw_sites.h"
|
#include "shim/hooks/draw_sites.h"
|
||||||
|
#include "shim/hooks/probe_entry.h"
|
||||||
#include "shim/hooks/rng_ledger.h"
|
#include "shim/hooks/rng_ledger.h"
|
||||||
|
|
||||||
namespace shim::hooks {
|
namespace shim::hooks {
|
||||||
|
|
@ -403,6 +404,7 @@ void StrategyHostAutosaveHook::describe_args(std::vector<Tv>& out, void* self, v
|
||||||
// independently; a shortfall is an unattributed word, which is the whole point of collecting it.
|
// independently; a shortfall is an unattributed word, which is the whole point of collecting it.
|
||||||
if (end_turn) {
|
if (end_turn) {
|
||||||
draw_sites_reset();
|
draw_sites_reset();
|
||||||
|
probe_entries_reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DrawSiteRow rows[64];
|
DrawSiteRow rows[64];
|
||||||
|
|
@ -424,6 +426,26 @@ void StrategyHostAutosaveHook::describe_args(std::vector<Tv>& out, void* self, v
|
||||||
out.push_back(tv::u32(draw_sites_other_words()).named("draw_site_words_other_rng"));
|
out.push_back(tv::u32(draw_sites_other_words()).named("draw_site_words_other_rng"));
|
||||||
out.push_back(tv::u32(draw_sites_other_calls()).named("draw_site_calls_other_rng"));
|
out.push_back(tv::u32(draw_sites_other_calls()).named("draw_site_calls_other_rng"));
|
||||||
out.push_back(tv::u32(draw_sites_overflow()).named("draw_site_overflow"));
|
out.push_back(tv::u32(draw_sites_overflow()).named("draw_site_overflow"));
|
||||||
|
|
||||||
|
// Lane H: entry counts for addresses whose reachability -- not their cost -- is the question.
|
||||||
|
// Emitted on the same marker as the draw sites so the two are read together: a probe row with
|
||||||
|
// `calls > 0` and no draw-site row for the sites inside it is "reached and gated", which is
|
||||||
|
// exactly what eight turns of `tail words = 0` could not distinguish from "not reached".
|
||||||
|
// `installed` is carried on every row because a MinHook failure would otherwise present as a
|
||||||
|
// confident zero.
|
||||||
|
ProbeEntryRow probes[16];
|
||||||
|
const std::size_t np = probe_entries_snapshot(probes, sizeof probes / sizeof probes[0]);
|
||||||
|
std::vector<Tv> plist;
|
||||||
|
for (std::size_t i = 0; i < np; ++i) {
|
||||||
|
Tv one = tv::struct_();
|
||||||
|
one.add("name", tv::str(probes[i].name));
|
||||||
|
one.add("rva", tv::u32(probes[i].rva));
|
||||||
|
one.add("calls", tv::u32(probes[i].calls));
|
||||||
|
one.add("calls_since_launch", tv::u32(probes[i].total));
|
||||||
|
one.add("installed", tv::boolean(probes[i].installed));
|
||||||
|
plist.push_back(std::move(one));
|
||||||
|
}
|
||||||
|
out.push_back(tv::list(std::move(plist)).named("probe_entries"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Tv StrategyHostAutosaveHook::describe_ret(void* r) { return tv::ptr(r); }
|
Tv StrategyHostAutosaveHook::describe_ret(void* r) { return tv::ptr(r); }
|
||||||
|
|
@ -779,6 +801,166 @@ void EncounterDetectAssignContactsHook::coverage(trace::Coverage& c) {
|
||||||
"arg:max_trials is the upper bound only");
|
"arg:max_trials is the upper bound only");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Game::EncounterDetect::ProcessTeamRecord (lane H) -------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// The record's entry vector. Lane I: the gate walks `rec->(+0x28 .. +0x2c)` with stride 0x44 and
|
||||||
|
// tests `entry[0]->+0xfc`. Both bounds are read here rather than assumed, and an implausible span
|
||||||
|
// is reported as unknown rather than iterated.
|
||||||
|
constexpr std::size_t kRecEntriesFirst = 0x28;
|
||||||
|
constexpr std::size_t kRecEntriesLast = 0x2c;
|
||||||
|
constexpr std::size_t kRecEntryStride = 0x44;
|
||||||
|
constexpr std::size_t kObjFb = 0xfb;
|
||||||
|
constexpr std::size_t kObjFc = 0xfc;
|
||||||
|
constexpr std::size_t kMaxRecEntries = 4096;
|
||||||
|
constexpr std::size_t kMaxEntryDetail = 12;
|
||||||
|
|
||||||
|
struct TeamRecordStats {
|
||||||
|
std::int32_t entries = -1;
|
||||||
|
std::int32_t gate = -1; // 1 if any entry object has +0xfc != 0
|
||||||
|
std::int32_t contacts = -1; // +0xfc != 0
|
||||||
|
std::int32_t detectors = -1; // +0xfc == 0 && +0xfb == 0
|
||||||
|
std::int32_t neither = -1; // +0xfc == 0 && +0xfb != 0 -- counted, not silently dropped
|
||||||
|
std::int32_t bound = -1; // contacts * detectors, lane I's AssignContacts worst case
|
||||||
|
std::int32_t fc_wide_disagrees = -1; // see the coverage note: byte vs dword at +0xfc
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per-entry detail, so the classification is checkable instead of asserted. Lane I's rules read
|
||||||
|
// `+0xfc` and `+0xfb` as bytes; if `+0xfc` is really a dword whose low byte happens to be zero,
|
||||||
|
// every count above is wrong in the same direction and nothing else would show it. So the dword is
|
||||||
|
// read too and disagreements are counted.
|
||||||
|
struct EntryDetail {
|
||||||
|
std::uint8_t fb = 0;
|
||||||
|
std::uint8_t fc = 0;
|
||||||
|
std::uint32_t fc_wide = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TeamRecordStats team_record_stats(void* rec, EntryDetail* detail, std::size_t* ndetail) {
|
||||||
|
TeamRecordStats st;
|
||||||
|
if (ndetail) *ndetail = 0;
|
||||||
|
if (!readable(rec, kRecEntriesLast + 4)) return st;
|
||||||
|
const char* first = static_cast<const char*>(ptr_at(rec, kRecEntriesFirst));
|
||||||
|
const char* last = static_cast<const char*>(ptr_at(rec, kRecEntriesLast));
|
||||||
|
if (!first || !last || last < first) return st;
|
||||||
|
const std::size_t span = static_cast<std::size_t>(last - first);
|
||||||
|
if (span % kRecEntryStride != 0) return st;
|
||||||
|
const std::size_t n = span / kRecEntryStride;
|
||||||
|
if (n > kMaxRecEntries || !readable(first, span)) return st;
|
||||||
|
|
||||||
|
st.entries = static_cast<std::int32_t>(n);
|
||||||
|
st.gate = st.contacts = st.detectors = st.neither = st.fc_wide_disagrees = 0;
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
const char* e = first + i * kRecEntryStride;
|
||||||
|
const char* obj = static_cast<const char*>(ptr_at(e, 0));
|
||||||
|
if (!readable(obj, kObjFc + 4)) continue;
|
||||||
|
const std::uint8_t fb = peek<std::uint8_t>(obj, kObjFb);
|
||||||
|
const std::uint8_t fc = peek<std::uint8_t>(obj, kObjFc);
|
||||||
|
const std::uint32_t fcw = peek<std::uint32_t>(obj, kObjFc);
|
||||||
|
if ((fc != 0) != (fcw != 0)) ++st.fc_wide_disagrees;
|
||||||
|
if (fc != 0) {
|
||||||
|
++st.contacts;
|
||||||
|
st.gate = 1;
|
||||||
|
} else if (fb == 0) {
|
||||||
|
++st.detectors;
|
||||||
|
} else {
|
||||||
|
++st.neither;
|
||||||
|
}
|
||||||
|
if (detail && ndetail && *ndetail < kMaxEntryDetail) {
|
||||||
|
detail[*ndetail].fb = fb;
|
||||||
|
detail[*ndetail].fc = fc;
|
||||||
|
detail[*ndetail].fc_wide = fcw;
|
||||||
|
++*ndetail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (st.contacts > 0 && st.detectors > 0) st.bound = st.contacts * st.detectors;
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TeamRecordState {
|
||||||
|
RngEntry entry;
|
||||||
|
};
|
||||||
|
TeamRecordState g_team_rec;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void EncounterDetectProcessTeamRecordHook::describe_args(std::vector<Tv>& out, void* self,
|
||||||
|
void* rec) {
|
||||||
|
void* server = readable(self, 4) ? ptr_at(self, 0) : nullptr;
|
||||||
|
g_team_rec.entry = observe_entry(server);
|
||||||
|
|
||||||
|
EntryDetail detail[kMaxEntryDetail];
|
||||||
|
std::size_t ndetail = 0;
|
||||||
|
const TeamRecordStats st = team_record_stats(rec, detail, &ndetail);
|
||||||
|
|
||||||
|
out.push_back(tv::ptr(self).named("ctx"));
|
||||||
|
out.push_back(tv::ptr(server).named("server"));
|
||||||
|
out.push_back(tv::ptr(rec).named("record"));
|
||||||
|
out.push_back(tv::i32(st.entries).named("entries"));
|
||||||
|
// `gate` is the predicate lane I read at 0x007ca671, recomputed here. `false` predicts that
|
||||||
|
// AssignContacts will NOT be called on this record -- a prediction the AssignContacts hook
|
||||||
|
// either confirms by staying silent or falsifies by firing.
|
||||||
|
out.push_back((st.gate < 0 ? tv::null() : tv::boolean(st.gate != 0)).named("gate"));
|
||||||
|
out.push_back(tv::i32(st.contacts).named("pred_contacts"));
|
||||||
|
out.push_back(tv::i32(st.detectors).named("pred_detectors"));
|
||||||
|
out.push_back(tv::i32(st.neither).named("pred_neither"));
|
||||||
|
out.push_back(tv::i32(st.bound).named("pred_max_trials"));
|
||||||
|
out.push_back(tv::i32(st.fc_wide_disagrees).named("fc_byte_vs_dword_disagreements"));
|
||||||
|
|
||||||
|
std::vector<Tv> ents;
|
||||||
|
for (std::size_t i = 0; i < ndetail; ++i) {
|
||||||
|
Tv one = tv::struct_();
|
||||||
|
one.add("fb", tv::u32(detail[i].fb));
|
||||||
|
one.add("fc", tv::u32(detail[i].fc));
|
||||||
|
one.add("fc_dword", tv::u32(detail[i].fc_wide));
|
||||||
|
ents.push_back(std::move(one));
|
||||||
|
}
|
||||||
|
out.push_back(tv::list(std::move(ents)).named("entry_flags"));
|
||||||
|
push_rng_args(out, g_team_rec.entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncounterDetectProcessTeamRecordHook::regions(std::vector<trace::Region>& out, void*, void*) {
|
||||||
|
push_rng_region(out, g_team_rec.entry.rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
EncounterDetectProcessTeamRecordHook::Args EncounterDetectProcessTeamRecordHook::rebind(
|
||||||
|
trace::Scratch&, void* self, void* rec) {
|
||||||
|
return Args(self, rec);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncounterDetectProcessTeamRecordHook::ours(void* self, void* rec) {
|
||||||
|
using H = trace::Hook<EncounterDetectProcessTeamRecordHook>;
|
||||||
|
if (H::mode == trace::Mode::Replace) {
|
||||||
|
refuse_replace("EncounterDetect::ProcessTeamRecord");
|
||||||
|
if (H::original) H::original(self, rec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncounterDetectProcessTeamRecordHook::coverage(trace::Coverage& c) {
|
||||||
|
tail_rng_common_coverage(c);
|
||||||
|
c.unmodelled("the whole body of ProcessTeamRecord: the gate call, the two vector builds and "
|
||||||
|
"the bucket construction",
|
||||||
|
trace::Risk::Medium,
|
||||||
|
"this hook measures the word cost of the call and recomputes three integers the "
|
||||||
|
"original derives from the same record. It models none of the work and asserts "
|
||||||
|
"nothing about the contact assignment",
|
||||||
|
"region:rng; arg:gate/pred_contacts/pred_detectors are a prediction, not a check");
|
||||||
|
c.unmodelled("`+0xfc` and `+0xfb` are read as BYTES, following lane I's reading of the "
|
||||||
|
"classifier functions",
|
||||||
|
trace::Risk::Medium,
|
||||||
|
"if either is really a wider field, every count here is wrong in the same "
|
||||||
|
"direction and no cross-check inside this hook would notice. So the dword at "
|
||||||
|
"+0xfc is read as well and any row where the two disagree is COUNTED, not "
|
||||||
|
"silently resolved -- a non-zero disagreement count means the byte reading is "
|
||||||
|
"unsafe on this workload",
|
||||||
|
"arg:fc_byte_vs_dword_disagreements; arg:entry_flags carries the raw values");
|
||||||
|
c.unmodelled("an entry whose object pointer is unreadable is skipped",
|
||||||
|
trace::Risk::Low,
|
||||||
|
"it is not counted into any of the three classes, so entries != contacts + "
|
||||||
|
"detectors + neither is the signal that this happened",
|
||||||
|
"arg:entries against the three class counts");
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Game::StrategyServer::ProcessNodeSpaceTravel ------------------------------------------------
|
// ---- Game::StrategyServer::ProcessNodeSpaceTravel ------------------------------------------------
|
||||||
|
|
||||||
void ProcessNodeSpaceTravelHook::describe_args(std::vector<Tv>& out, void* self) {
|
void ProcessNodeSpaceTravelHook::describe_args(std::vector<Tv>& out, void* self) {
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,37 @@ struct EncounterDetectAssignContactsHook {
|
||||||
static void coverage(trace::Coverage& c);
|
static void coverage(trace::Coverage& c);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- lane H: the caller of AssignContacts, which is where the residual actually is ------------
|
||||||
|
//
|
||||||
|
// `EncounterDetect::ProcessTeamRecord` 0x007ca640, `ret 4`, the ONLY caller of `AssignContacts`
|
||||||
|
// and the only depth-2 function under `StrategyServer::DetectEncounters`. Lane Y's standalone
|
||||||
|
// leaves a 2-word residual per turn on both calibrated pairs, attributed by lane Z's site table to
|
||||||
|
// 0x0050329d (`NextFloat`, depth 4) and 0x007929a4 (`NextInt`, depth 3) -- both below this
|
||||||
|
// function. Lane Z also found `AssignContacts` **never ran**, because the gate here was never
|
||||||
|
// satisfied. So the residual is not the `|contacts| x |detectors|` family that lane I bounded; it
|
||||||
|
// is something above that gate, and no instrument has ever bracketed the function that contains
|
||||||
|
// both.
|
||||||
|
//
|
||||||
|
// This hook computes lane I's three quantities from the record AT ENTRY -- the gate predicate, the
|
||||||
|
// contact count and the detector count -- and reports them beside the measured word cost. When the
|
||||||
|
// gate is false they are a prediction of a call that will not happen (and the hook says so); when
|
||||||
|
// it is true they become checkable against the numbers `AssignContactsHook` reads from the callee's
|
||||||
|
// own arguments. Two hooks, one function apart, printing the same two integers derived two
|
||||||
|
// different ways.
|
||||||
|
struct EncounterDetectProcessTeamRecordHook {
|
||||||
|
static constexpr const char* name = "Game::EncounterDetect::ProcessTeamRecord";
|
||||||
|
static constexpr trace::CallConv conv = trace::CallConv::Thiscall;
|
||||||
|
using Ret = void;
|
||||||
|
using Args = std::tuple<void*, void*>; // ctx {S, TechDef*, TechDef*}, TeamRecord* (0x74 B)
|
||||||
|
|
||||||
|
static void describe_args(std::vector<trace::Tv>& out, void* self, void* rec);
|
||||||
|
static void regions(std::vector<trace::Region>& out, void* self, void* rec);
|
||||||
|
static Args rebind(trace::Scratch& s, void* self, void* rec);
|
||||||
|
static void ours(void* self, void* rec);
|
||||||
|
static trace::HookPolicy policy() { return trace::HookPolicy{}; }
|
||||||
|
static void coverage(trace::Coverage& c);
|
||||||
|
};
|
||||||
|
|
||||||
// `ProcessNodeSpaceTravel` 0x007a0e20, run TWICE a turn (ProcessTurn phase 7 and tail phase 10)
|
// `ProcessNodeSpaceTravel` 0x007a0e20, run TWICE a turn (ProcessTurn phase 7 and tail phase 10)
|
||||||
// and never swept for draws by any lane.
|
// and never swept for draws by any lane.
|
||||||
struct ProcessNodeSpaceTravelHook {
|
struct ProcessNodeSpaceTravelHook {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
#include "shim/hooks/global_consts.h"
|
#include "shim/hooks/global_consts.h"
|
||||||
#include "shim/hooks/research.h"
|
#include "shim/hooks/research.h"
|
||||||
#include "shim/hooks/draw_sites.h"
|
#include "shim/hooks/draw_sites.h"
|
||||||
|
#include "shim/hooks/probe_entry.h"
|
||||||
#include "shim/hooks/tail_rng.h"
|
#include "shim/hooks/tail_rng.h"
|
||||||
#include "shim/hooks/tech_effects.h"
|
#include "shim/hooks/tech_effects.h"
|
||||||
#include "shim/trace/hook.h"
|
#include "shim/trace/hook.h"
|
||||||
|
|
@ -84,7 +85,11 @@ Config ReadConfig() {
|
||||||
char* val = eq + 1;
|
char* val = eq + 1;
|
||||||
val[std::strcspn(val, "\r\n")] = '\0';
|
val[std::strcspn(val, "\r\n")] = '\0';
|
||||||
std::string err;
|
std::string err;
|
||||||
if (shim::fpu::apply_config(p, val, &err)) {
|
std::size_t probe_n = 0;
|
||||||
|
if (shim::hooks::probe_config(p, val, &probe_n)) {
|
||||||
|
Log("config: %s=%s -> %u lane-H entry probes", p, val,
|
||||||
|
static_cast<unsigned>(probe_n));
|
||||||
|
} else if (shim::fpu::apply_config(p, val, &err)) {
|
||||||
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
|
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
|
||||||
else Log("config: %s=%s", p, val);
|
else Log("config: %s=%s", p, val);
|
||||||
} else if (cfg.trace.apply(p, val, &err)) {
|
} else if (cfg.trace.apply(p, val, &err)) {
|
||||||
|
|
@ -180,6 +185,8 @@ using ApplyEncounterHook = shim::trace::Hook<shim::hooks::ApplyEncounterResultHo
|
||||||
using NodeDecayHook = shim::trace::Hook<shim::hooks::NodeLineDecayHook>;
|
using NodeDecayHook = shim::trace::Hook<shim::hooks::NodeLineDecayHook>;
|
||||||
using NodeSpaceHook = shim::trace::Hook<shim::hooks::ProcessNodeSpaceTravelHook>;
|
using NodeSpaceHook = shim::trace::Hook<shim::hooks::ProcessNodeSpaceTravelHook>;
|
||||||
using AssignContactsHook = shim::trace::Hook<shim::hooks::EncounterDetectAssignContactsHook>;
|
using AssignContactsHook = shim::trace::Hook<shim::hooks::EncounterDetectAssignContactsHook>;
|
||||||
|
// Lane H: the caller of AssignContacts, where lane Y's 2-word detection residual lives.
|
||||||
|
using ProcessTeamRecordHook = shim::trace::Hook<shim::hooks::EncounterDetectProcessTeamRecordHook>;
|
||||||
|
|
||||||
void InstallHooks(shim::trace::Tracer& tracer) {
|
void InstallHooks(shim::trace::Tracer& tracer) {
|
||||||
const uintptr_t exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleA(nullptr));
|
const uintptr_t exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleA(nullptr));
|
||||||
|
|
@ -252,6 +259,10 @@ void InstallHooks(shim::trace::Tracer& tracer) {
|
||||||
// Lane I's one inlined-draw site inside ProcessTurn's closure. It leaves no call-graph edge, so
|
// Lane I's one inlined-draw site inside ProcessTurn's closure. It leaves no call-graph edge, so
|
||||||
// neither a sweep nor the entry-point detours below can see it; only a boundary hook can.
|
// neither a sweep nor the entry-point detours below can see it; only a boundary hook can.
|
||||||
InstallTemplateHook<shim::hooks::EncounterDetectAssignContactsHook>(tracer, exeBase, sots::addr::EncounterDetect_AssignContacts);
|
InstallTemplateHook<shim::hooks::EncounterDetectAssignContactsHook>(tracer, exeBase, sots::addr::EncounterDetect_AssignContacts);
|
||||||
|
// Lane H: its only caller. AssignContacts never ran on any measured turn because the gate
|
||||||
|
// here was never satisfied, so the 2-word detection residual is above that gate and no
|
||||||
|
// instrument has bracketed the function containing both.
|
||||||
|
InstallTemplateHook<shim::hooks::EncounterDetectProcessTeamRecordHook>(tracer, exeBase, sots::addr::EncounterDetect_ProcessTeamRecord);
|
||||||
|
|
||||||
// Per-call-site attribution: detour the SEVEN generator entry points and record
|
// Per-call-site attribution: detour the SEVEN generator entry points and record
|
||||||
// __builtin_return_address(0) with the word cost of each call. These are NOT template hooks --
|
// __builtin_return_address(0) with the word cost of each call. These are NOT template hooks --
|
||||||
|
|
@ -272,6 +283,12 @@ void InstallHooks(shim::trace::Tracer& tracer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lane H: register-transparent entry counters. Installed after the draw-site detours and
|
||||||
|
// before the fpu module; they share no target with either. See probe_entry.h for why these are
|
||||||
|
// asm stubs and not C++ detours.
|
||||||
|
shim::hooks::init_probe_entries(exeBase, &ShimLogLine);
|
||||||
|
shim::hooks::install_probe_entries();
|
||||||
|
|
||||||
// Lane F: x87 control-word forcing at the turn gate + the per-tick change sampler.
|
// Lane F: x87 control-word forcing at the turn gate + the per-tick change sampler.
|
||||||
// Installed last so it is nowhere near the template hooks it is meant to measure.
|
// Installed last so it is nowhere near the template hooks it is meant to measure.
|
||||||
shim::fpu::install(exeBase, &ShimLogLine);
|
shim::fpu::install(exeBase, &ShimLogLine);
|
||||||
|
|
@ -331,6 +348,7 @@ void Shim_Init(HMODULE self) {
|
||||||
NodeDecayHook::register_policy(tracer);
|
NodeDecayHook::register_policy(tracer);
|
||||||
NodeSpaceHook::register_policy(tracer);
|
NodeSpaceHook::register_policy(tracer);
|
||||||
AssignContactsHook::register_policy(tracer);
|
AssignContactsHook::register_policy(tracer);
|
||||||
|
ProcessTeamRecordHook::register_policy(tracer);
|
||||||
// A hook that never stated what it does not check is a defect, not a detail: say so in
|
// A hook that never stated what it does not check is a defect, not a detail: say so in
|
||||||
// shim.log as well as in the trace's meta line (docs/harness-audit.md).
|
// shim.log as well as in the trace's meta line (docs/harness-audit.md).
|
||||||
for (const std::string& h : tracer.unstated_hooks())
|
for (const std::string& h : tracer.unstated_hooks())
|
||||||
|
|
|
||||||
3
src/shim/shim.cfg.hoff
Normal file
3
src/shim/shim.cfg.hoff
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Lane H: the control run. hooks=off installs NOTHING (not even the M0 asm stub), so this
|
||||||
|
# is the game with a proxy DLL that only forwards Bink exports.
|
||||||
|
hooks=off
|
||||||
52
src/shim/shim.cfg.hp0
Normal file
52
src/shim/shim.cfg.hp0
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::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=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=0
|
||||||
52
src/shim/shim.cfg.hp10
Normal file
52
src/shim/shim.cfg.hp10
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::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=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=10
|
||||||
52
src/shim/shim.cfg.hp11
Normal file
52
src/shim/shim.cfg.hp11
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::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=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=11
|
||||||
52
src/shim/shim.cfg.hp4
Normal file
52
src/shim/shim.cfg.hp4
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::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=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=4
|
||||||
52
src/shim/shim.cfg.hp8
Normal file
52
src/shim/shim.cfg.hp8
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::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=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=8
|
||||||
52
src/shim/shim.cfg.hp9
Normal file
52
src/shim/shim.cfg.hp9
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::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=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=9
|
||||||
52
src/shim/shim.cfg.hpall
Normal file
52
src/shim/shim.cfg.hpall
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::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=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
|
probes=all
|
||||||
49
src/shim/shim.cfg.hprobes
Normal file
49
src/shim/shim.cfg.hprobes
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::ProcessTurn=off
|
||||||
|
hook.Game::StrategyServer::MoveFleet=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
|
||||||
|
hook.Game::StrategyHost::Autosave=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
51
src/shim/shim.cfg.hprobes2
Normal file
51
src/shim/shim.cfg.hprobes2
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Lane H -- the five probes, TRACE. Copy over C:\SOTS\shim.cfg.
|
||||||
|
#
|
||||||
|
# Lane Z's ledger plus two additions:
|
||||||
|
# * Game::EncounterDetect::ProcessTeamRecord -- the caller of AssignContacts, where lane Y's
|
||||||
|
# 2-word-per-turn detection residual has to be (both residual sites are below it, and
|
||||||
|
# AssignContacts itself never ran on any measured turn);
|
||||||
|
# * the register-transparent entry counters in probe_entry.cpp, which are NOT template hooks
|
||||||
|
# and are therefore NOT configurable from here. They install unconditionally whenever
|
||||||
|
# hooks != off, and every one of them logs its MinHook status to shim.log at startup. A probe
|
||||||
|
# that failed to install carries installed=false into the record, because a MinHook failure
|
||||||
|
# would otherwise present as a confident zero.
|
||||||
|
#
|
||||||
|
# Everything else is off so the log holds only the ledger: eight records per End Turn instead of
|
||||||
|
# ~40 per player. Read `side.rng.before.words` and `side.rng.after.words` on each record; the
|
||||||
|
# difference is the words the strategic generator consumed inside that call.
|
||||||
|
#
|
||||||
|
# fpu.sample_turn=off is REQUIRED: the fpu module samples the control word at
|
||||||
|
# StrategyServer::ProcessTurn, MinHook allows one hook per target, and this config needs that
|
||||||
|
# address for the ledger.
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=off
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=off
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=off
|
||||||
|
hook.Game::ServerPlayer::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=trace
|
||||||
|
hook.Game::StrategyServer::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||||
|
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||||
|
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||||
|
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||||
|
hook.Game::EncounterDetect::AssignContacts=trace
|
||||||
|
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||||
|
|
||||||
|
fpu.sample_turn=off
|
||||||
|
fpu.sample_ticks=off
|
||||||
|
|
||||||
|
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||||
|
# each record ~5 kB of state nobody reads.
|
||||||
|
trace.inline_max=64
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.flush=always
|
||||||
Loading…
Add table
Reference in a new issue