sots-engine/src/shim/hooks/colony_turn.h

96 lines
5.6 KiB
C++

// 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 <cstdint>
#include <tuple>
#include <vector>
#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<void*>; // this (ServerSystem*)
static void describe_args(std::vector<trace::Tv>& out, void* self);
static void regions(std::vector<trace::Region>& 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