// Hook descriptor for the per-system colony turn (B4): // // Game::ServerSystem::ProcessTurn(this) // // A verified `__thiscall` with **no stack arguments** and no return value, so it goes through // Hook<> with CallConv::Thiscall. It is called once per system per turn from // StrategyServer::ProcessTurn -- about 28 calls on the reference save's End Turn. // // Why it is worth a compare even though it is a dispatcher: the words the function body writes // itself are a real, self-contained slice of a colony turn (the unowned infrastructure decay, // both bonus pools, the long-stability accrual, the turn's resource reset, the growth-halt // clear and the two per-player countdown sweeps), and they are exactly the ones our // `sim::ProcessColonyTurn` models. Everything the callees write is left undeclared, so the // harness never compares it. // // The generator is a declared region as well. A colony turn's only RNG consumer is // ProcessRebellion (ProcessPlague, the civilian growth pass and ProcessSlaves were each swept // to call depth one and are draw-free), so on a system with no rebellion the post-call state // must be **identical** -- and any movement of it names the system whose rebellion fired. // COVERAGE STATEMENT (for meta.hooks[H].coverage once HookPolicy carries the field; the // harness audit lane owns that struct change, so this lives as text until it lands). // state: "partial" // unmodelled: // - what: "the whole plague pass (ProcessPlague), imperial and civilian population growth, // the resource debit (AdjustResources), the in-orbit refuel, ProcessSlaves and // ProcessRebellion" // risk: high mitigation: "none - those fields are not declared regions at all" // - what: "the addiction sweep's morale events: ours computes the {species, event id, // delta} list but nothing applies it, so the system's Morale int[7] and its // morale-event vector are never written or compared" // risk: high why: "this is exactly B3's failure mode - a list append outside every // declared region" mitigation: "guard:morale (not yet declared)" // - what: "the independent-colony imperial<->civilian pop drift (0x007514f0), called // between the bonus pools and the stability check" // risk: medium mitigation: "none" // - what: "TnsOH, the build queue's own list, and every ship the queue creates" // risk: medium mitigation: "none" #pragma once #include #include #include #include "shim/trace/hook.h" namespace shim::hooks { struct ServerSystemProcessTurnHook { static constexpr const char* name = "Game::ServerSystem::ProcessTurn"; static constexpr trace::CallConv conv = trace::CallConv::Thiscall; using Ret = void; using Args = std::tuple; // this (ServerSystem*) static void describe_args(std::vector& out, void* self); static void regions(std::vector& out, void* self); static Args rebind(trace::Scratch& s, void* self); static void ours(void* self); static trace::HookPolicy policy() { return trace::HookPolicy{}; } static void coverage(trace::Coverage& c) { c.unmodelled("the addiction sweep raises MoraleEvents, which are constructed and appended " "to the system's capped morale history", trace::Risk::High, "the same class of write as B3's defect. sim::ProcessColonyTurn does compute " "the morale events (ColonyTurnResult), but the hook never emits them: " "DescribeMoraleEvents is dead code, so they are neither compared nor logged", "guard:system"); c.unmodelled("every callee: the plague pass, imperial and civilian growth, the resource " "debit, in-orbit refuel, slaves, rebellion and the build queue", trace::Risk::High, "declared input boundary -- ProcessTurn is a dispatcher and only the words " "it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, " "EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, " "create ships and bump per-player ShipRecords counters", "guard:system covers the system object only, not the other objects"); c.unmodelled("ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the " "build queue writes the owning ServerPlayer", trace::Risk::Medium, "writes through a pointer to another object; no region reaches the player"); c.unmodelled("ProcessRebellion is the pass's only RNG consumer and its draw count is " "data-dependent", trace::Risk::Low, "the generator IS a declared region, so a moved post-state is visible and " "names the system whose rebellion fired -- it is reported, not modelled"); c.unmodelled("replace mode is refused for this hook", trace::Risk::Medium, "our side models the dispatcher's own writes and none of the callees, so a " "replace run would silently skip a colony's whole turn. There is therefore " "no oracle layer behind the compare for this hook"); } }; // Process facts the hook needs (exe base for the RVAs, a line logger). Call once before // installing. void init_colony_turn(std::uintptr_t exe_base, void (*log_line)(const char* line)); } // namespace shim::hooks