// 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 #include #include #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; // this, fleet, dt static void describe_args(std::vector& out, void* self, void* fleet, float dt); static trace::Tv describe_ret(bool r); static void regions(std::vector& 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; // this 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("`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