Three function-entry detours, off unless `aivisit=on`:
* StrategyAIAgent::AssignFleetsAndIssueOrders -- the bracket, recording
(agent, task, pass, walked vector, its slots in index order);
* StrategyAIAgent::ClaimShipsOfFleet -- the head loop, one row per element;
* StrategyAIAgent::ReleaseShipsOfFleet -- the tail loop, one row per element.
The two per-element targets have exactly ONE caller each in the whole image and
that caller is the third target, so the module's traffic cannot come from
anywhere else in the game and no return-address filter is needed. The element
loop's own body is deliberately not patched: its first instruction is a branch
target, which is the shape rule 19 exists for.
Each element row carries the element address (the hypothesis under test), the
fleet's id, its ship ids and its Location pointer. The ship ids are the join key
to the command block's list 10; the join must not be by position because the
walk can visit more elements than it emits commands for.
Reads of game addresses are probed before every access and laundered through a
register, so a wrong offset logs a zero instead of faulting inside a detour.
Also: shim.cfg.bupin, which is shim.cfg.bppin plus exactly three non-comment
lines, and is marked `# exhaustive` so tools/check_shim_configs.py enforces
that every registered hook is named in it.
480 lines
25 KiB
C++
480 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::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;
|
|
}
|