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

125 lines
7.4 KiB
C++

// Hook descriptors for the strategic movement pass (B4):
//
// Game::StrategyServer::MoveFleet(this, fleet, dt) one movement step for one fleet
// Game::StrategyServer::ProcessFleetMovement(this) the turn's schedule + gate traffic
//
// Both prototypes are verified from the instruction stream (see docs/B4.md), so both go
// through Hook<> with CallConv::Thiscall.
//
// MoveFleet fires five times per turn per participating fleet at most, and recurses into
// itself for a multi-waypoint leg -- the template handles the nesting (depth, call ids), and
// the inner call gets its own record.
//
// declared regions: pos (3 floats), prev_pos (3 floats), one per ship's range, and the
// generator (a type-5 waypoint draws from it); plus the return value.
// input boundary: the departure hook, the route revalidation, every arrival handler, the
// waypoint list itself and the tanker top-up. A call that arrives is
// expected to differ in all of that, none of which is declared.
//
// ProcessFleetMovement runs once per turn.
//
// declared regions: each player's gate-traffic word -- which the original computes at the
// very end of the pass from the post-move fleet state, exactly the state
// our side reads, so the two are genuinely comparable.
// input boundary: everything else. The pass schedule is recorded in the arguments (ours
// predicts the call order so a trace can be checked against it) but is not
// itself compared, because reproducing it would mean running MoveFleet.
#pragma once
#include <cstdint>
#include <tuple>
#include <vector>
#include "shim/trace/hook.h"
namespace shim::hooks {
struct StrategyServerMoveFleetHook {
static constexpr const char* name = "Game::StrategyServer::MoveFleet";
static constexpr trace::CallConv conv = trace::CallConv::Thiscall;
using Ret = bool;
using Args = std::tuple<void*, void*, float>; // this, fleet, dt
static void describe_args(std::vector<trace::Tv>& out, void* self, void* fleet, float dt);
static trace::Tv describe_ret(bool r);
static void regions(std::vector<trace::Region>& out, void* self, void* fleet, float dt);
static Args rebind(trace::Scratch& s, void* self, void* fleet, float dt);
static bool ours(void* self, void* fleet, float dt);
static trace::HookPolicy policy() { return trace::HookPolicy{}; }
static void coverage(trace::Coverage& c) {
c.unmodelled("on arrival: dispatches SEFleetArrived and runs one of three arrival "
"handlers by destination kind (enter system / join fleet / stop at point)",
trace::Risk::High,
"declared input boundary -- an arriving call is expected to differ in all of "
"it, and none of it is declared, so the compare says nothing about arrivals",
"guard:fleet sees the fleet's own words; the event and the system do not");
c.unmodelled("on departure: cancels every still-acting ship (with a log line each) and "
"calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits",
trace::Risk::High, "writes through pointers to ships and to the system");
c.unmodelled("the tanker top-up refuels other ships in the fleet",
trace::Risk::Medium,
"the per-ship range regions would show it, but ours does not model it, so a "
"fleet with a tanker diverges for a known reason");
c.unmodelled("a node-line waypoint's step comes from the stutter profile",
trace::Risk::Medium,
"NodeLineStep / BuildStutterSegments are written and unit-tested but not "
"wired in; the hook steps every waypoint type as speed x dt, so a node-line "
"leg is knowingly mis-stepped and only its type is recorded",
"declared gap: docs/B4.md");
c.unmodelled("a missed probabilistic jump scatters the fleet in a random direction",
trace::Risk::Medium,
"the direction is a second draw whose mapping is not modelled; ours leaves "
"the position alone and reports the scatter distance, so the generator "
"region diverges by one word on a miss");
c.unmodelled("the route revalidation and the waypoint list itself",
trace::Risk::Medium, "declared input boundary; the waypoint vector is not a region");
}
};
struct StrategyServerProcessFleetMovementHook {
static constexpr const char* name = "Game::StrategyServer::ProcessFleetMovement";
static constexpr trace::CallConv conv = trace::CallConv::Thiscall;
using Ret = void;
using Args = std::tuple<void*>; // this
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("`ours` re-reads the LIVE fleet list after the original has run",
trace::Risk::High,
"the gate-traffic total is computed by the original at the very end of the "
"pass, so a pre-call snapshot would diverge for the wrong reason. It breaks "
"the compare invariant that ours never touches live memory, and it makes "
"this hook's verdict partly self-fulfilling: the input to our arithmetic is "
"the original's own post-move state");
c.unmodelled("drives MoveFleet up to five times per fleet",
trace::Risk::High,
"every undeclared effect of MoveFleet happens inside this call too; the pass "
"schedule is recorded in the arguments but never compared");
c.unmodelled("writes FPdpos into every fleet and clears flags 0x2 and 0x100 on every fleet",
trace::Risk::High,
"no region covers the fleets, only the players' gate-traffic words");
c.unmodelled("OnFleetArrived posts EVENT_FLEET_ARRIVED",
trace::Risk::High,
"the same class of write as B3's defect, and there is no replace mode for "
"this hook, so nothing behind the compare could catch it either");
c.unmodelled("the original accumulates by player->index but writes back by the player's "
"position in the server vector, into a fixed 32-int array with no bounds "
"check",
trace::Risk::Medium,
"a real latent bug in the original that our side reproduces only while "
"index == position; the reference save never separates them");
c.unmodelled("PassSchedule() is never called by the hook, and FleetSummary::targetFleetId "
"/ relation are never filled",
trace::Risk::Medium,
"the header claims ours predicts the call order for a trace to check; that "
"prediction is not actually emitted");
}
};
void init_fleet_movement(std::uintptr_t exe_base, void (*log_line)(const char* line));
} // namespace shim::hooks