`ComputeBudget`'s savings-interest term is now compared against the running game at
a treasury the corpus actually contains. Three runs on VM146 from turn1-state.sav:
A (widened floats, as shipped) 3,895 calls, 0 diverged, 0 undeclared writes
B (exact decimals, the control) 2,718 calls, 1,359 diverged
The game fills savingsInterest with 499 at a treasury of 50,000, and with 380 at
38,100 -- the exact decimals pay 500 and 381. Every divergence in B lands on a
treasury that is a multiple of 100 and no other state diverges at all, which is
exactly the arithmetic. G3's rule-23 reading is now measured, not inferred, and the
one-money error is shown to propagate into `available` and `researchMoney` too.
The control also settles why the earlier 4,437-call green run was green: slot 5 IS
diffed and the harness CAN see it, so that run simply presented no boundary state.
Coverage is therefore reported as distinct states, not calls: 5 distinct treasuries,
2 of them on the boundary.
Two further rule-23 constants found in the same routine by an operand-width sweep,
corrected, and honestly marked UNVERIFIED because no reference turn can see them:
- the research-yield factor is a widened 0.85f while its two neighbours in the
same product are exact doubles. Boundary: research money a multiple of 40,000;
the run presented 9 distinct values and none is.
- the three research modifiers are summed in single precision, not double.
Boundary: two of the three non-zero; the corpus has shrm = TRM = 0.
Both are pinned by boundary cases in test_economy.cpp that fail with the decimals.
Also verified live, in the same run:
- T31's difficulty-column recovery. The live ServerPlayer+0xf9 / NPC flags on all
eight players are exactly what lane PL's save-only inversion claims, including
the awkward system-owning player that is still ambiguous because it is an NPC.
- BANKRUPTCY_PROTECTION_LIMIT_FACTOR reads 3.29999995 = (float)3.3. Its file image
is zero because the loader fills it at run time, so lane PL-3 had to assume the
value; it is now measured and the assumption was right.
Falsified, and recorded as such: the difficulty-mods record does NOT sit inline at
ServerPlayer+0x36c -- that field is a heap pointer on all eight players. The row IS
reachable from a ServerPlayer (which corrects the hook's standing coverage note),
but the fitted {3.0,1.5}/{1.0,1.0} pair remains unverified. The hook logs the
pointer and does not follow it.
The `verified` column stays 0, deliberately. Every phase this compare touches is
Partial for reasons upstream of it, and promoting one because part of it was checked
is the drift app_test_catalog exists to catch. What moved is models; see
docs/L5-live-verification.md for each one with its coverage.
Gates run separately: clean-room OK, host ctest 54/54, CT111 shim cross-build exit 0.
417 lines
20 KiB
C++
417 lines
20 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/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::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);
|
|
|
|
// 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 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) {
|
|
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);
|
|
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;
|
|
}
|