125 lines
7.4 KiB
C++
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
|