154 lines
9.3 KiB
C++
154 lines
9.3 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.
|
|
// COVERAGE STATEMENT (see the note in colony_turn.h about where this will live).
|
|
//
|
|
// MoveFleet -- state: "partial"
|
|
// - what: "every arrival handler: FleetArrives, the SEFleetArrived event object and its
|
|
// dispatch, the next-leg pathability check, and the insert into the server's
|
|
// in-motion set at +0x214"
|
|
// risk: high why: "an event post and two std::set inserts, none of them declared"
|
|
// - what: "the departure block: the script hook at StrategyServer+0x1b4, per-ship action
|
|
// cancellation, and ServerSystem::FleetDeparts (which rewrites a system's fleet
|
|
// vector and its per-player presence bitmasks)"
|
|
// risk: high mitigation: "none"
|
|
// - what: "the waypoint vector itself - a leg that completes pops its waypoint, and a failed
|
|
// probabilistic jump rewrites the whole route"
|
|
// risk: high mitigation: "none"
|
|
// - what: "the tanker top-up (flag 0x1000 ships are refuelled to full each step) and the
|
|
// fleet flag word at +0x10c (bit 1 set when the fleet moved)"
|
|
// risk: medium mitigation: "none"
|
|
// - what: "the node-line step: our side computes speed x dt for every waypoint type, so a
|
|
// type-2 waypoint's step is wrong by construction"
|
|
// risk: high mitigation: "the record carries wpt_type; treat type 2 as not compared"
|
|
//
|
|
// ProcessFleetMovement -- state: "partial"
|
|
// - what: "the entire pass schedule and therefore every MoveFleet call it makes"
|
|
// risk: high mitigation: "sim::PlanFleetMovement predicts the order; check the trace"
|
|
// - what: "the per-fleet destination position at +0xec and the flag-0x2 / flag-0x100 clears"
|
|
// risk: medium mitigation: "none"
|
|
// - what: "OnFleetArrived: it takes the set difference of +0x204 and +0x214, clears both,
|
|
// and dispatches a per-player arrival event"
|
|
// risk: high mitigation: "none"
|
|
#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
|