sots-engine/src/shim/main.cpp
alex 618ccb1c25 shim: research.replace_cascade, and a completion counter that prints in replace mode too
The B3 hook's unlock cascade was compare-mode only, so a replace run left the
completed node unstamped and no tech unlocked -- and its only per-call counter was
gated on the cascade, so a replace run had no instrument of its own saying a
completion had happened at all. Lane CR needs both halves to ask whether
ProcessResearch can be displaced.

research.replace_cascade=on (default off, so the shipped behaviour is unchanged and
still measurable) lets a replace-mode call write the four TechNode words
SetResearched stamps and the tree's completion-order counter. The ServerPlayer half
of the callback stays unmodelled and stays declared: no event is posted, no
ObservedTech element is appended, no tech effect is applied. The pair of settings is
the measurement -- same binary, one config line, and the leaf difference between the
two autosaves is the cascade's own contribution.

The per-call log line now prints in every mode and counts completions from the pass's
own step results, so 'a completion fired' is a statement by the instrument rather
than an inference from the artefact under test.
2026-09-09 09:15:47 -04:00

483 lines
25 KiB
C++

// sots-engine shim: a proxy binkw32.dll the original game loads.
//
// Every Bink export is forwarded to the real DLL by the linker (see binkw32.def); this file only
// exists to get code running inside the game process. M0 scope: log a banner, install one hook on
// Mars::Application::Initialize, and prove injection + ASLR relocation + hooking end to end.
//
// Binary facts (RVAs, calling conventions) come exclusively from the generated header.
#include <windows.h>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <string>
#include "MinHook.h"
#include "generated/sots_addresses.h"
#include "shim/fpu_force.h"
#include "shim/hooks/dictionaries.h"
#include "shim/hooks/colony_turn.h"
#include "shim/hooks/system_output.h"
#include "shim/hooks/player_turn.h"
#include "shim/hooks/compute_budget.h"
#include "shim/hooks/fleet_movement.h"
#include "shim/hooks/global_consts.h"
#include "shim/hooks/research.h"
#include "shim/hooks/draw_sites.h"
#include "shim/hooks/probe_entry.h"
#include "shim/hooks/tail_rng.h"
#include "shim/hooks/tech_effects.h"
#include "shim/hooks/ai_orders.h"
#include "shim/hooks/ai_rng.h"
#include "shim/hooks/ai_visit.h"
#include "shim/hooks/watchpoints.h"
#include "shim/trace/hook.h"
#include "shim/trace/selftest.h"
#include "shim/trace/tracer.h"
namespace {
// ---- logging ------------------------------------------------------------------------------
FILE* g_log = nullptr;
char g_dir[MAX_PATH] = {}; // directory the shim DLL lives in (== game dir)
void Log(const char* fmt, ...) {
if (!g_log) return;
SYSTEMTIME st;
GetLocalTime(&st);
std::fprintf(g_log, "%02u:%02u:%02u.%03u [tid %5lu] ", st.wHour, st.wMinute, st.wSecond,
st.wMilliseconds, GetCurrentThreadId());
va_list ap;
va_start(ap, fmt);
std::vfprintf(g_log, fmt, ap);
va_end(ap);
std::fputc('\n', g_log);
std::fflush(g_log);
}
// ---- config (shim.cfg next to the DLL; "key=value" per line) ------------------------------
//
// `hooks=`, `hook.<Name>=`, `trace.*` are owned by the tracer (src/shim/trace/tracer.h);
// `hooks=off` additionally means "install nothing" (the M0 asm-stub hook included).
struct Config {
bool hooks = true; // hooks != off
shim::trace::Config trace;
};
Config ReadConfig() {
Config cfg;
char path[MAX_PATH];
std::snprintf(path, sizeof path, "%s\\shim.trace.jsonl", g_dir);
cfg.trace.path = path;
std::snprintf(path, sizeof path, "%s\\shim.cfg", g_dir);
FILE* f = std::fopen(path, "r");
if (!f) {
Log("config: %s not found, using defaults", path);
return cfg;
}
char line[512];
while (std::fgets(line, sizeof line, f)) {
char* p = line;
while (*p == ' ' || *p == '\t') ++p;
if (*p == '#' || *p == ';' || *p == '\n' || *p == '\0') continue;
char* eq = std::strchr(p, '=');
if (!eq) continue;
*eq = '\0';
char* val = eq + 1;
val[std::strcspn(val, "\r\n")] = '\0';
std::string 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::hooks::ai_rng_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else if (shim::hooks::ai_visit_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else if (shim::hooks::ai_orders_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else if (shim::hooks::research_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else if (shim::hooks::watch_apply_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else if (shim::fpu::apply_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else if (cfg.trace.apply(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else {
Log("config: ignoring unknown key '%s'", p);
}
}
std::fclose(f);
cfg.hooks = cfg.trace.default_mode != shim::trace::Mode::Off;
return cfg;
}
// ---- the one M0 hook ----------------------------------------------------------------------
bool g_minhookUp = false;
} // namespace
// The detour is a register-transparent asm stub rather than a C++ thiscall function: the
// prototype in sots_addresses.h is [unverified], and a C++ detour that *calls* the original
// bakes in assumptions about stack args / EDX / return value. Calling out for the log with
// everything saved and then tail-jumping to the trampoline leaves the original's own `ret`
// in charge of cleanup, so the hook is correct for any calling convention. (v1 of this hook
// was a thiscall wrapper and crashed the game inside Initialize.)
extern "C" void* g_origInitialize; // trampoline, filled in by MH_CreateHook
void* g_origInitialize = nullptr;
extern "C" void InitializeDetour();
extern "C" void LogInitializeCalled(void* self) {
Log("Application::Initialize called (this=%p)", self);
}
asm(R"(
.text
.globl _InitializeDetour
_InitializeDetour:
pushfl
pushal
pushl %ecx
call _LogInitializeCalled
addl $4, %esp
popal
popfl
jmp *_g_origInitialize
)");
// Line logger handed to the template hooks (they live outside this TU's anonymous namespace).
void ShimLogLine(const char* line) { Log("%s", line); }
namespace {
// ---- template hooks (verified cdecl prototypes; docs/shim-trace.md "Declaring a hook") ----
template <class D>
void InstallTemplateHook(shim::trace::Tracer& tracer, uintptr_t exeBase, uint32_t rva) {
using H = shim::trace::Hook<D>;
H::configure(tracer);
void* target = reinterpret_cast<void*>(exeBase + rva);
if (H::mode == shim::trace::Mode::Off) {
Log("hook: %s rva=0x%08x mode=off (not installed)", D::name, rva);
return;
}
MH_STATUS st = MH_CreateHook(target, reinterpret_cast<void*>(H::detour()),
reinterpret_cast<void**>(&H::original));
Log("hook: %s rva=0x%08x -> va=%p MH_CreateHook -> %s (trampoline=%p)", D::name, rva, target,
MH_StatusToString(st), reinterpret_cast<void*>(H::original));
if (st != MH_OK) return;
st = MH_EnableHook(target);
Log("hook: %s MH_EnableHook -> %s mode=%s", D::name, MH_StatusToString(st),
shim::trace::mode_name(H::mode));
}
using LoadFileHook = shim::trace::Hook<shim::hooks::GlobalConstsLoadFileHook>;
using WeaponInitHook = shim::trace::Hook<shim::hooks::WeaponDictionaryInitHook>;
using SectionCtorHook = shim::trace::Hook<shim::hooks::SectionDictionaryCtorHook>;
using ProcessResearchHook = shim::trace::Hook<shim::hooks::TechTreeProcessResearchHook>;
using OnTechResearchedHook = shim::trace::Hook<shim::hooks::ServerPlayerOnTechResearchedHook>;
using ComputeBudgetHook = shim::trace::Hook<shim::hooks::ComputeBudgetHook>;
using ColonyTurnHook = shim::trace::Hook<shim::hooks::ServerSystemProcessTurnHook>;
// Lane N: the population -> base-output term (docs/N-output-term.md).
using GroupOutputHook = shim::trace::Hook<shim::hooks::ServerSystemGroupOutputHook>;
using TotalOutputHook = shim::trace::Hook<shim::hooks::ServerSystemComputeTotalOutputHook>;
using PlayerTurnHook = shim::trace::Hook<shim::hooks::ServerPlayerProcessTurnHook>;
using MoveFleetHook = shim::trace::Hook<shim::hooks::StrategyServerMoveFleetHook>;
using FleetMovementHook = shim::trace::Hook<shim::hooks::StrategyServerProcessFleetMovementHook>;
// Lane Z: the per-turn RNG ledger (docs/Z-tail-rng.md).
using AutosaveHook = shim::trace::Hook<shim::hooks::StrategyHostAutosaveHook>;
using ServerTurnHook = shim::trace::Hook<shim::hooks::StrategyServerProcessTurnHook>;
using CombatDoneTailHook = shim::trace::Hook<shim::hooks::OnAllCombatDoneTailHook>;
using ApplyEncounterHook = shim::trace::Hook<shim::hooks::ApplyEncounterResultHook>;
using NodeDecayHook = shim::trace::Hook<shim::hooks::NodeLineDecayHook>;
using NodeSpaceHook = shim::trace::Hook<shim::hooks::ProcessNodeSpaceTravelHook>;
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) {
const uintptr_t exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleA(nullptr));
void* target = reinterpret_cast<void*>(exeBase + sots::addr::Mars_Application_Initialize);
Log("hook: Mars_Application_Initialize rva=0x%08x -> va=%p", sots::addr::Mars_Application_Initialize,
target);
MH_STATUS st = MH_Initialize();
Log("hook: MH_Initialize -> %s", MH_StatusToString(st));
if (st != MH_OK) return;
g_minhookUp = true;
st = MH_CreateHook(target, reinterpret_cast<void*>(&InitializeDetour), &g_origInitialize);
Log("hook: MH_CreateHook -> %s (trampoline=%p)", MH_StatusToString(st), g_origInitialize);
if (st != MH_OK) return;
st = MH_EnableHook(target);
Log("hook: MH_EnableHook -> %s", MH_StatusToString(st));
// M1: GlobalConsts::LoadFile (cdecl, verified) through the template.
shim::hooks::init_global_consts(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::GlobalConstsLoadFileHook>(tracer, exeBase, sots::addr::GlobalConsts_LoadFile);
// M2: the id-manifest loaders (verified thiscall) through the template's Thiscall conv.
shim::hooks::init_dictionaries(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::WeaponDictionaryInitHook>(tracer, exeBase, sots::addr::WeaponDictionary_Init);
InstallTemplateHook<shim::hooks::SectionDictionaryCtorHook>(tracer, exeBase, sots::addr::SectionDictionary_ctor);
// B3: the per-turn research pass (verified thiscall). One call per player per turn.
shim::hooks::init_research(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::TechTreeProcessResearchHook>(tracer, exeBase, sots::addr::TechTree_ProcessResearch);
// B2: the hard-coded tech-effect callback (verified thiscall, vft slot 4). Fires once
// per tech completion, so on most turns not at all.
shim::hooks::init_tech_effects(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::ServerPlayerOnTechResearchedHook>(tracer, exeBase, sots::addr::ServerPlayer_OnTechResearched);
// B1: ServerPlayer::ComputeBudget (verified thiscall) -- the first behavioural compare.
shim::hooks::init_compute_budget(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::ComputeBudgetHook>(tracer, exeBase, sots::addr::ServerPlayer_ComputeBudget);
// B4: the per-system colony turn (once per system per turn) and the two movement entry
// points. All three are verified thiscall prototypes with no stack-argument surprises.
shim::hooks::init_colony_turn(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::ServerSystemProcessTurnHook>(tracer, exeBase, sots::addr::ServerSystem_ProcessTurn);
// Lane N: the two side-effect-free output functions. GroupOutput is the population ->
// output law itself; ComputeTotalOutput is the sum the budget roll-up ultimately reads.
shim::hooks::init_system_output(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::ServerSystemGroupOutputHook>(tracer, exeBase, sots::addr::ServerSystem_GroupOutput);
InstallTemplateHook<shim::hooks::ServerSystemComputeTotalOutputHook>(tracer, exeBase, sots::addr::ServerSystem_ComputeTotalOutput);
// Lane T: the per-player turn driver (once per player per turn). Verified thiscall with
// one ignored float argument; see docs/T-turn-driver.md.
shim::hooks::init_player_turn(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::ServerPlayerProcessTurnHook>(tracer, exeBase, sots::addr::ServerPlayer_ProcessTurn);
shim::hooks::init_fleet_movement(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::StrategyServerMoveFleetHook>(tracer, exeBase, sots::addr::StrategyServer_MoveFleet);
InstallTemplateHook<shim::hooks::StrategyServerProcessFleetMovementHook>(tracer, exeBase, sots::addr::StrategyServer_ProcessFleetMovement);
// Lane Z: the per-turn RNG ledger. Six nested hooks bracketing one End Turn between the two
// autosaves; every one declares the strategic generator and nothing else. Installed BEFORE
// the fpu module because both want StrategyServer::ProcessTurn and MinHook allows one hook
// per target -- `fpu.sample_turn=off` is the config that hands it over cleanly, and if it is
// left on the fpu sampler's MH_CreateHook is what fails and says so.
shim::hooks::init_tail_rng(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::StrategyHostAutosaveHook>(tracer, exeBase, sots::addr::StrategyHost_Autosave);
InstallTemplateHook<shim::hooks::StrategyServerProcessTurnHook>(tracer, exeBase, sots::addr::StrategyServer_ProcessTurn);
InstallTemplateHook<shim::hooks::OnAllCombatDoneTailHook>(tracer, exeBase, sots::addr::StrategyServer_OnAllCombatDone_Tail);
InstallTemplateHook<shim::hooks::ApplyEncounterResultHook>(tracer, exeBase, sots::addr::StrategyServer_ApplyEncounterResult);
InstallTemplateHook<shim::hooks::NodeLineDecayHook>(tracer, exeBase, sots::addr::StrategyServer_NodeLineDecay);
InstallTemplateHook<shim::hooks::ProcessNodeSpaceTravelHook>(tracer, exeBase, sots::addr::StrategyServer_ProcessNodeSpaceTravel);
// 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.
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);
// Lane L1: the turn-begin driver and the three script-object writers that run inside a turn.
// BeginProcessTurn is the interval lane Z's autosave bracket CONTAINED but never attributed --
// nothing had ever been hooked between the pre-turn autosave and ProcessTurn. Lane SV read the
// swarm-queen registrar as taking one RNG_NextInt per new hive there, which if true qualifies
// "the residual outside the two turn drivers is exactly zero". These five settle it live.
InstallTemplateHook<shim::hooks::StrategyServerBeginProcessTurnHook>(tracer, exeBase, sots::addr::StrategyServer_BeginProcessTurn);
InstallTemplateHook<shim::hooks::SwarmQueenOnTurnBeginHook>(tracer, exeBase, sots::addr::SVSOSwarmQueen_OnTurnBegin);
InstallTemplateHook<shim::hooks::SwarmQueenRegisterHivesHook>(tracer, exeBase, sots::addr::SVSOSwarmQueen_RegisterHives);
InstallTemplateHook<shim::hooks::SwarmQueenTickHivesHook>(tracer, exeBase, sots::addr::SVSOSwarmQueen_TickHives);
InstallTemplateHook<shim::hooks::SlaversRefuelUpdateDifficultyTierHook>(tracer, exeBase, sots::addr::SVSOSlaversRefuel_UpdateDifficultyTier);
// Lane L1, the AI seed probe: is `turn1-state -> turn2`'s nondeterminism a per-process SEED or
// an ordering effect? Two launches, load only, and diff the ordered (rng, seed) sequences.
// Both hooks are off in every config except shim.cfg.l1seed.
InstallTemplateHook<shim::hooks::RngSeedHook>(tracer, exeBase, sots::addr::RNG_Seed);
InstallTemplateHook<shim::hooks::StrategyAppRunAIHook>(tracer, exeBase, sots::addr::StrategyApp_RunAI);
// 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 --
// they write no record and take no snapshot, just two 4-byte reads of `left` -- because
// NextFloat alone has 109 call sites and a record per draw would drown the log. The table is
// emitted once per turn on the post-turn autosave, where it can be reconciled against the
// bracket total the region ledger measured independently.
shim::hooks::init_draw_sites(exeBase);
{
std::size_t nsites = 0;
const shim::hooks::DrawSiteHook* sites = shim::hooks::draw_site_hooks(&nsites);
for (std::size_t i = 0; i < nsites; ++i) {
void* t = reinterpret_cast<void*>(exeBase + sites[i].rva);
MH_STATUS s1 = MH_CreateHook(t, sites[i].detour, sites[i].trampoline);
MH_STATUS s2 = s1 == MH_OK ? MH_EnableHook(t) : s1;
Log("drawsite: %s rva=0x%08x -> va=%p create=%s enable=%s", sites[i].name, sites[i].rva,
t, MH_StatusToString(s1), MH_StatusToString(s2));
}
}
// 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 W2: hardware data-write watchpoints. One MinHook detour (the arming point); the
// watchpoints themselves modify no code at all. Off unless `watch=on` (rule 19).
shim::hooks::install_watchpoints(exeBase, g_dir, &ShimLogLine);
// Lane L4: the AI command-block dump plus its entry probes. Off unless `aiorders=on`, and
// `aiorders=on` alone installs exactly ONE detour -- `aiprobes=` adds the rest, so the two
// halves of the instrument can be given separate rule-19 controls. Its batch target
// (ApplyTurnCommandBatch) is a different function from the watchpoint module's arming point
// (ApplyAllTurnCommands, its only caller), so the two never contend for a MinHook target.
shim::hooks::install_ai_orders(exeBase, g_dir, &ShimLogLine);
// Lane PAR: the per-client RNG bracket on StrategyClient::OnResumePlaying. Off unless
// `airng=on`. Installed AFTER the draw-site detours because it registers an observer on them
// rather than hooking the seven entry points a second time (MinHook: one hook per target).
// Its target is a client event handler, shared with nothing else in the shim.
shim::hooks::install_ai_rng(exeBase, g_dir, &ShimLogLine);
// Lane BU: the fleet-assignment visit-order log. Off unless `aivisit=on`. Three detours, all
// of them function entries; two of the three targets have exactly ONE caller in the image and
// that caller is the third, so this module's traffic cannot come from anywhere else in the
// game. See ai_visit.h for why the loop body itself is not patched.
shim::hooks::install_ai_visit(exeBase, g_dir, &ShimLogLine);
// 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.
shim::fpu::install(exeBase, &ShimLogLine);
}
// ---- lifecycle -----------------------------------------------------------------------------
void Shim_Init(HMODULE self) {
GetModuleFileNameA(self, g_dir, sizeof g_dir);
if (char* slash = std::strrchr(g_dir, '\\')) *slash = '\0';
char logPath[MAX_PATH];
std::snprintf(logPath, sizeof logPath, "%s\\shim.log", g_dir);
g_log = std::fopen(logPath, "a");
if (!g_log) return; // nothing we can do; the forwarders still work without us
char exePath[MAX_PATH] = {};
GetModuleFileNameA(nullptr, exePath, sizeof exePath);
const uintptr_t exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleA(nullptr));
Log("==== sots-engine shim (binkw32 proxy) build %s ====", SHIM_BUILD_ID);
Log("exe: %s", exePath);
Log("exe base=0x%08lx (link-time image base 0x%08lx, ASLR delta %+ld) pid=%lu shim=%p",
static_cast<unsigned long>(exeBase), static_cast<unsigned long>(sots::addr::IMAGE_BASE),
static_cast<long>(exeBase - sots::addr::IMAGE_BASE), GetCurrentProcessId(),
static_cast<void*>(self));
Log("addresses: %s", SOTS_ADDR_PROVENANCE);
const Config cfg = ReadConfig();
if (!cfg.hooks) {
// `hooks=off` turns off the TRACE hooks. It must not turn off the watchpoint module.
// Those are two independent instruments: the watchpoints install one arming detour of
// their own and write no trace records, and lane L3 wears them for a whole game played
// forward, where the template hooks' 30-45 s per End Turn is pure cost and measures
// nothing being asked about. Before this, `hooks=off watch=on` silently armed nothing --
// a config that reports "watch=on" in the banner and then measures a confident zero,
// which is exactly the failure method rule 1 exists to catch.
if (shim::hooks::watch_enabled()) {
MH_STATUS st = MH_Initialize();
Log("hook: trace hooks disabled by config; MH_Initialize (watch only) -> %s",
MH_StatusToString(st));
if (st == MH_OK) {
g_minhookUp = true;
shim::hooks::install_watchpoints(exeBase, g_dir, &ShimLogLine);
}
} else {
Log("hook: disabled by config");
}
return;
}
// Trace log: register every template hook's policy (goes into the meta line), then open.
// Hooks read their mode from the tracer after open(); a tracer that failed to open reports
// Off for everything, so the game runs un-instrumented rather than half-instrumented.
shim::trace::Tracer& tracer = shim::trace::Tracer::instance();
tracer.configure(cfg.trace);
shim::trace::Hook<shim::selftest::FillHook>::register_policy(tracer);
LoadFileHook::register_policy(tracer);
ComputeBudgetHook::register_policy(tracer);
WeaponInitHook::register_policy(tracer);
SectionCtorHook::register_policy(tracer);
ProcessResearchHook::register_policy(tracer);
OnTechResearchedHook::register_policy(tracer);
ColonyTurnHook::register_policy(tracer);
GroupOutputHook::register_policy(tracer);
TotalOutputHook::register_policy(tracer);
PlayerTurnHook::register_policy(tracer);
MoveFleetHook::register_policy(tracer);
FleetMovementHook::register_policy(tracer);
AutosaveHook::register_policy(tracer);
ServerTurnHook::register_policy(tracer);
CombatDoneTailHook::register_policy(tracer);
ApplyEncounterHook::register_policy(tracer);
NodeDecayHook::register_policy(tracer);
NodeSpaceHook::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
// shim.log as well as in the trace's meta line (docs/harness-audit.md).
for (const std::string& h : tracer.unstated_hooks())
Log("COVERAGE: hook %s registered with no coverage statement -- its compare results are "
"not trustworthy", h.c_str());
char exeSha[65] = {};
if (!shim::trace::sha256_file(exePath, exeSha)) Log("trace: could not hash %s", exePath);
if (tracer.open(SHIM_BUILD_ID, exeSha)) {
Log("trace: %s (default mode %s, inline_max %u, flush %s)", cfg.trace.path.c_str(),
shim::trace::mode_name(cfg.trace.default_mode), cfg.trace.inline_max,
cfg.trace.flush_always ? "always" : "lazy");
} else {
Log("trace: cannot open %s; template hooks forced off", cfg.trace.path.c_str());
}
InstallHooks(tracer);
// Self-test through the hook template (no MinHook involved): one record per launch proves
// the emitter/tracer inside the game process. `hook.Shim::SelfTest::Fill=off` silences it.
{
using H = shim::trace::Hook<shim::selftest::FillHook>;
H::configure(tracer);
const unsigned sum = shim::selftest::run_once(H::mode);
Log("selftest: %s mode=%s checksum=%08x records=%lu", shim::selftest::FillHook::name,
shim::trace::mode_name(H::mode), sum, static_cast<unsigned long>(tracer.records_written()));
}
}
void Shim_Shutdown() {
shim::hooks::watch_flush(&ShimLogLine);
shim::hooks::ai_orders_flush(&ShimLogLine);
shim::hooks::ai_rng_flush(&ShimLogLine);
shim::hooks::ai_visit_flush(&ShimLogLine);
Log("%s", shim::fpu::summary().c_str());
shim::trace::Tracer& tracer = shim::trace::Tracer::instance();
if (tracer.is_open()) {
Log("shutdown: trace records=%lu", static_cast<unsigned long>(tracer.records_written()));
tracer.close();
}
if (g_minhookUp) {
MH_STATUS st = MH_Uninitialize();
Log("shutdown: MH_Uninitialize -> %s", MH_StatusToString(st));
}
Log("==== shim detach ====");
if (g_log) std::fclose(g_log);
g_log = nullptr;
}
} // namespace
extern "C" BOOL WINAPI DllMain(HINSTANCE hinst, DWORD reason, LPVOID) {
switch (reason) {
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hinst);
Shim_Init(hinst);
break;
case DLL_PROCESS_DETACH:
Shim_Shutdown();
break;
}
return TRUE;
}