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.
483 lines
25 KiB
C++
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;
|
|
}
|