#include "shim/hooks/fleet_movement.h" #include #include #include #include #include #include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #include #endif #include "generated/sots_addresses.h" #include "mars/rng/mt19937.h" #include "shim/hooks/movement_inputs.h" namespace shim::hooks { using trace::Tv; namespace tv = trace::tv; namespace { namespace A = sots::addr; constexpr std::size_t kRngSize = A::RNG_size; constexpr int kMtWords = mars::rng::MT19937::N; constexpr std::size_t kMaxPlayers = 64; using ResolveWaypointFn = void*(SHIM_THISCALL*)(void* fleet); using RelationFn = int(SHIM_THISCALL*)(void* player, void* other); struct Env { std::uintptr_t exe_base = 0; void (*log_line)(const char*) = nullptr; ResolveWaypointFn resolve_waypoint = nullptr; }; Env g_env; void logf(const char* fmt, ...) { if (!g_env.log_line) return; char line[512]; va_list ap; va_start(ap, fmt); std::vsnprintf(line, sizeof line, fmt, ap); va_end(ap); g_env.log_line(line); } bool readable(const void* p, std::size_t n) { if (!p) return false; if (n == 0) return true; #if defined(_WIN32) const char* c = static_cast(p); const char* const end = c + n; while (c < end) { MEMORY_BASIC_INFORMATION mbi; if (!VirtualQuery(c, &mbi, sizeof mbi)) return false; if (mbi.State != MEM_COMMIT) return false; if (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) return false; const DWORD ok = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; if (!(mbi.Protect & ok)) return false; c = static_cast(mbi.BaseAddress) + mbi.RegionSize; } return true; #else return true; #endif } template T peek(const void* base, std::size_t off) { T v{}; std::memcpy(&v, static_cast(base) + off, sizeof v); return v; } void* ptr_at(const void* base, std::size_t off) { return peek(base, off); } std::uint32_t fpu_control_word() { #if defined(__i386__) || defined(__x86_64__) unsigned short cw = 0; __asm__ __volatile__("fnstcw %0" : "=m"(cw)); return cw; #else return 0; #endif } void* rng_of(void* server) { if (!readable(server, A::StrategyServer_off_RNGPtr + 4)) return nullptr; void* r = ptr_at(server, A::StrategyServer_off_RNGPtr); return readable(r, kRngSize) ? r : nullptr; } // The front waypoint of a fleet, or null when the vector is empty. const void* front_waypoint(const void* fleet, std::int32_t* count) { *count = 0; if (!readable(fleet, A::StarFleet_off_Flags + 4)) return nullptr; const char* first = static_cast(ptr_at(fleet, A::StarFleet_off_Waypoints)); const char* last = static_cast(ptr_at(fleet, A::StarFleet_off_Waypoints + 4)); if (!first || !last || last <= first) return nullptr; const std::size_t n = static_cast(last - first) / A::Waypoint_stride; if (n > 4096 || !readable(first, A::Waypoint_stride)) return nullptr; *count = static_cast(n); return first; } std::vector fleet_ships(const void* fleet) { std::vector ships; void** begin = static_cast(ptr_at(fleet, A::StarFleet_off_Ships)); void** end = static_cast(ptr_at(fleet, A::StarFleet_off_Ships + 4)); if (!begin || !end || end < begin) return ships; const std::size_t n = static_cast(end - begin); if (n > kMaxTracedShips * 8 || !readable(begin, n * sizeof(void*))) return ships; ships.assign(begin, begin + n); return ships; } std::vector server_players(void* server) { std::vector out; if (!readable(server, A::StrategyServer_off_Players + 8)) return out; void** begin = static_cast(ptr_at(server, A::StrategyServer_off_Players)); void** end = static_cast(ptr_at(server, A::StrategyServer_off_Players + 4)); if (!begin || !end || end < begin) return out; const std::size_t n = static_cast(end - begin); if (n > kMaxPlayers || !readable(begin, n * sizeof(void*))) return out; out.assign(begin, begin + n); return out; } std::vector server_fleets(void* server) { std::vector out; if (!readable(server, A::StrategyServer_off_Fleets + 8)) return out; void** begin = static_cast(ptr_at(server, A::StrategyServer_off_Fleets)); void** end = static_cast(ptr_at(server, A::StrategyServer_off_Fleets + 4)); if (!begin || !end || end < begin) return out; const std::size_t n = static_cast(end - begin); if (n > kMaxTracedFleets || !readable(begin, n * sizeof(void*))) return out; out.assign(begin, begin + n); return out; } // ---- per-call state ------------------------------------------------------------------------ struct MoveState { bool compare = false; FleetStepSnapshot snap; double step = 0; std::uintptr_t rng_base = 0; std::vector mt_before; std::int32_t left_before = 0; bool have_rng = false; void* s_pos = nullptr; void* s_prev = nullptr; void* s_rng = nullptr; std::vector s_ship; std::vector names; // stable storage for Region::name }; MoveState g_move; struct PfmState { bool compare = false; std::vector fleets; std::vector players; std::vector scratch; std::vector names; std::int32_t playerCount = 0; }; PfmState g_pfm; Tv describe_i32(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("v", tv::i32(peek(p, 0))); return s; } Tv describe_f32(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("v", tv::f32(peek(p, 0))); return s; } Tv describe_vec3(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("x", tv::f32(peek(p, 0))); s.add("y", tv::f32(peek(p, 4))); s.add("z", tv::f32(peek(p, 8))); return s; } Tv describe_rng(const void* p, std::size_t, unsigned inline_max) { Tv s = tv::struct_(); s.add("mt", tv::bytes(static_cast(p) + A::RNG_off_State, static_cast(kMtWords) * 4, inline_max)); s.add("left", tv::i32(peek(p, A::RNG_off_Left))); return s; } struct ShimRandom final : sots::sim::IRandom { mars::rng::MT19937 gen; unsigned draws = 0; float NextFloat() override { ++draws; return gen.next_float(); } std::uint32_t NextIntInclusive(std::uint32_t n) override { ++draws; return gen.next_int_inclusive(n); } std::uint32_t NextUInt32() override { ++draws; return gen.next_u32(); } }; void capture_move(void* self, void* fleet, float dt) { MoveState m; FleetStepSnapshot& s = m.snap; s.dt = dt; s.fpuControlWord = fpu_control_word(); if (!readable(fleet, A::StarFleet_off_Flags + 4)) { g_move = std::move(m); return; } s.fleetId = peek(fleet, A::StarFleet_off_Id); s.speed = peek(fleet, A::StarFleet_off_Speed); s.heldFlag = (peek(fleet, A::StarFleet_off_Flags) & 0x100u) ? 1 : 0; for (int i = 0; i < 3; ++i) s.pos[i] = peek(fleet, A::StarFleet_off_Pos + std::size_t(i) * 4); void* owner = ptr_at(fleet, A::StarFleet_off_PID); if (readable(owner, A::ServerPlayer_off_CstT + 4)) { s.ownerIndex = peek(owner, A::ServerPlayer_off_PlyrIdx); s.castEfficiency = peek(owner, A::ServerPlayer_off_CstE); s.castThreshold = peek(owner, A::ServerPlayer_off_CstT); } const void* wp = front_waypoint(fleet, &s.waypointCount); if (wp) s.waypointType = peek(wp, A::Waypoint_off_Type); if (g_env.resolve_waypoint) { void* dest = g_env.resolve_waypoint(fleet); if (readable(dest, 0x24)) { s.destKind = peek(dest, 0x14); for (int i = 0; i < 3; ++i) s.dest[i] = peek(dest, 0x18 + std::size_t(i) * 4); } } const std::vector ships = fleet_ships(fleet); s.shipCount = static_cast(ships.size()); for (std::size_t i = 0; i < ships.size() && i < kMaxTracedShips; ++i) { if (!readable(ships[i], A::StarShip_off_MaxRange + 4)) continue; s.ships[i].range = peek(ships[i], A::StarShip_off_Range); const std::uint32_t lo = peek(ships[i], A::StarShip_off_Flags); s.ships[i].exempt = (lo & 0x1000u) ? 1 : 0; } void* rng = rng_of(self); if (rng) { m.rng_base = reinterpret_cast(rng); m.left_before = peek(rng, A::RNG_off_Left); m.mt_before.resize(static_cast(kMtWords)); std::memcpy(m.mt_before.data(), static_cast(rng) + A::RNG_off_State, m.mt_before.size() * 4); m.have_rng = true; s.rngLeftIn = m.left_before; } // A node-line waypoint's step comes from the stutter profile, which needs the node graph; // this milestone does not walk it, so the node-line case is reported and left to the // original. Every other case is `speed x dt`. m.step = sots::sim::StraightStep(s.speed, dt); g_move = std::move(m); } void capture_pfm(void* self) { PfmState p; p.players = server_players(self); p.playerCount = static_cast(p.players.size()); for (void* f : server_fleets(self)) { FleetSummary fs; if (!readable(f, A::StarFleet_off_Flags + 4)) continue; fs.fleetId = peek(f, A::StarFleet_off_Id); fs.gateTraffic = peek(f, A::StarFleet_off_GateTraffic); void* owner = ptr_at(f, A::StarFleet_off_PID); if (readable(owner, A::ServerPlayer_off_PlyrIdx + 4)) fs.ownerIndex = peek(owner, A::ServerPlayer_off_PlyrIdx); std::int32_t count = 0; const void* wp = front_waypoint(f, &count); if (wp) fs.waypointType = peek(wp, A::Waypoint_off_Type); p.fleets.push_back(fs); } g_pfm = std::move(p); } } // namespace void init_fleet_movement(std::uintptr_t exe_base, void (*log_line)(const char* line)) { g_env.exe_base = exe_base; g_env.log_line = log_line; g_env.resolve_waypoint = reinterpret_cast(exe_base + A::StarFleet_ResolveWaypoint); } // ---- MoveFleet ------------------------------------------------------------------------------- void StrategyServerMoveFleetHook::describe_args(std::vector& out, void* self, void* fleet, float dt) { out.push_back(tv::ptr(self).named("server")); out.push_back(tv::ptr(fleet).named("fleet")); out.push_back(tv::f32(dt).named("dt")); out.push_back(tv::i32(g_move.snap.fleetId).named("fleet_id")); out.push_back(tv::i32(g_move.snap.ownerIndex).named("owner")); out.push_back(tv::i32(g_move.snap.waypointType).named("wpt_type")); out.push_back(tv::i32(g_move.snap.waypointCount).named("wpt_count")); out.push_back(tv::i32(g_move.snap.destKind).named("dest_kind")); out.push_back(tv::f32(g_move.snap.speed).named("speed")); out.push_back(tv::i32(g_move.snap.rngLeftIn).named("rng_left_in")); out.push_back(tv::u32(g_move.snap.fpuControlWord).named("fpu_cw")); } Tv StrategyServerMoveFleetHook::describe_ret(bool r) { return tv::boolean(r); } void StrategyServerMoveFleetHook::regions(std::vector& out, void* self, void* fleet, float dt) { capture_move(self, fleet, dt); if (!readable(fleet, A::StarFleet_off_Flags + 4)) return; trace::Region pos; pos.name = "pos"; pos.ptr = static_cast(fleet) + A::StarFleet_off_Pos; pos.size = 12; pos.describe = &describe_vec3; out.push_back(pos); trace::Region prev; prev.name = "prev_pos"; prev.ptr = static_cast(fleet) + A::StarFleet_off_PrvPos; prev.size = 12; prev.describe = &describe_vec3; out.push_back(prev); // One region per ship's remaining range, named so a diff points at the ship rather than // at a byte offset. `names` is sized once so no push_back can dangle a Region::name. const std::vector ships = fleet_ships(fleet); const std::size_t n = ships.size() < kMaxTracedShips ? ships.size() : kMaxTracedShips; g_move.names.clear(); g_move.names.reserve(n); for (std::size_t i = 0; i < n; ++i) { char buf[32]; std::snprintf(buf, sizeof buf, "ship[%u].range", static_cast(i)); g_move.names.emplace_back(buf); } for (std::size_t i = 0; i < n; ++i) { if (!readable(ships[i], A::StarShip_off_Range + 4)) continue; trace::Region r; r.name = g_move.names[i].c_str(); r.ptr = static_cast(ships[i]) + A::StarShip_off_Range; r.size = 4; r.describe = &describe_f32; out.push_back(r); } if (g_move.have_rng) { trace::Region r; r.name = "rng"; r.ptr = reinterpret_cast(g_move.rng_base); r.size = kRngSize; r.describe = &describe_rng; out.push_back(r); } } StrategyServerMoveFleetHook::Args StrategyServerMoveFleetHook::rebind(trace::Scratch& s, void* self, void* fleet, float dt) { g_move.compare = true; g_move.s_pos = s.count() > 0 && s.size(0) >= 12 ? s.ptr(0) : nullptr; g_move.s_prev = s.count() > 1 && s.size(1) >= 12 ? s.ptr(1) : nullptr; g_move.s_ship.clear(); std::size_t i = 2; for (; i < s.count() && s.size(i) == 4; ++i) g_move.s_ship.push_back(s.ptr(i)); g_move.s_rng = i < s.count() && s.size(i) >= kRngSize ? s.ptr(i) : nullptr; return Args(self, fleet, dt); } bool StrategyServerMoveFleetHook::ours(void* self, void* fleet, float dt) { using H = trace::Hook; const bool compare = g_move.compare; g_move.compare = false; if (H::mode == trace::Mode::Replace) { // Replace is not offered: our side reproduces the step arithmetic but none of the // arrival handling, so feeding it to the game would strand every arriving fleet. static bool warned = false; if (!warned) { warned = true; logf("movement: replace mode is not supported for MoveFleet; " "falling back to the original"); } return H::original ? H::original(self, fleet, dt) : false; } if (!compare) return false; ShimRandom rng; if (g_move.have_rng) rng.gen.load_state(g_move.mt_before.data(), g_move.left_before); const FleetStepResult r = StepFleet(g_move.snap, g_move.step, rng); if (g_move.s_pos) { for (int i = 0; i < 3; ++i) { const float v = static_cast(r.pos[i]); std::memcpy(static_cast(g_move.s_pos) + i * 4, &v, 4); } } // The previous position is only written when the fleet actually moved, and it takes the // ENTRY position -- not the new one. if (g_move.s_prev && r.moved != 0 && !(r.pos[0] == g_move.snap.pos[0] && r.pos[1] == g_move.snap.pos[1] && r.pos[2] == g_move.snap.pos[2])) { for (int i = 0; i < 3; ++i) { const float v = static_cast(g_move.snap.pos[i]); std::memcpy(static_cast(g_move.s_prev) + i * 4, &v, 4); } } for (std::size_t i = 0; i < g_move.s_ship.size() && i < r.shipRanges.size(); ++i) { const float v = static_cast(r.shipRanges[i]); std::memcpy(g_move.s_ship[i], &v, 4); } if (g_move.s_rng && g_move.have_rng) { char* dst = static_cast(g_move.s_rng); std::memcpy(dst + A::RNG_off_State, rng.gen.state(), static_cast(kMtWords) * 4); const std::int32_t left = rng.gen.left(); std::memcpy(dst + A::RNG_off_Left, &left, 4); void* next = reinterpret_cast(g_move.rng_base + A::RNG_off_State + static_cast(kMtWords - left) * 4); std::memcpy(dst + A::RNG_off_Next, &next, sizeof next); } return r.arrived; } // ---- ProcessFleetMovement -------------------------------------------------------------------- void StrategyServerProcessFleetMovementHook::describe_args(std::vector& out, void* self) { out.push_back(tv::ptr(self).named("server")); out.push_back(tv::i32(g_pfm.playerCount).named("players")); out.push_back(tv::i32(static_cast(g_pfm.fleets.size())).named("fleets")); std::vector summary; for (const FleetSummary& f : g_pfm.fleets) { Tv one = tv::struct_(); one.add("id", tv::i32(f.fleetId)); one.add("owner", tv::i32(f.ownerIndex)); one.add("wpt_type", tv::i32(f.waypointType)); one.add("gate_traffic", tv::i16(f.gateTraffic)); summary.push_back(std::move(one)); } out.push_back(tv::list(std::move(summary)).named("fleet_state")); } void StrategyServerProcessFleetMovementHook::regions(std::vector& out, void* self) { // Captured BEFORE the original runs, so the argument record shows the pre-move fleet // state; `ours` re-captures afterwards, because that is when the original sums the // traffic. capture_pfm(self); g_pfm.names.clear(); g_pfm.names.reserve(g_pfm.players.size()); for (std::size_t i = 0; i < g_pfm.players.size(); ++i) { char buf[32]; std::snprintf(buf, sizeof buf, "gate_traffic[%u]", static_cast(i)); g_pfm.names.emplace_back(buf); } for (std::size_t i = 0; i < g_pfm.players.size(); ++i) { if (!readable(g_pfm.players[i], A::ServerPlayer_off_GateTraffic + 4)) continue; trace::Region r; r.name = g_pfm.names[i].c_str(); r.ptr = static_cast(g_pfm.players[i]) + A::ServerPlayer_off_GateTraffic; r.size = 4; r.describe = &describe_i32; out.push_back(r); } } StrategyServerProcessFleetMovementHook::Args StrategyServerProcessFleetMovementHook::rebind(trace::Scratch& s, void* self) { g_pfm.compare = true; g_pfm.scratch.clear(); for (std::size_t i = 0; i < s.count(); ++i) if (s.size(i) == 4) g_pfm.scratch.push_back(s.ptr(i)); return Args(self); } void StrategyServerProcessFleetMovementHook::ours(void* self) { using H = trace::Hook; const bool compare = g_pfm.compare; g_pfm.compare = false; if (H::mode == trace::Mode::Replace) { static bool warned = false; if (!warned) { warned = true; logf("movement: replace mode is not supported for ProcessFleetMovement; " "falling back to the original"); } if (H::original) H::original(self); return; } if (!compare) return; // Re-read the fleets: the original computes the gate-traffic total at the very end of the // pass, from the state the fleets are in after they have moved. capture_pfm(self); const std::vector totals = GateTraffic(g_pfm.fleets, g_pfm.playerCount); for (std::size_t i = 0; i < g_pfm.scratch.size(); ++i) { const std::int32_t v = i < totals.size() ? totals[i] : 0; std::memcpy(g_pfm.scratch[i], &v, 4); } } } // namespace shim::hooks