From 52ac92ee99d614e6edbde9c5a1a1b5ec18d15ece Mon Sep 17 00:00:00 2001 From: ProgenyAlpha Date: Tue, 13 Oct 2026 01:28:34 -0400 Subject: [PATCH 1/2] Sync: add opt-in live fleet state ("fleet" records, config key "fleets") Subscribe to fleet_watch from the sync patches when any target enables `fleets`, and queue one record per FleetState transition: {"type":"fleet","fid":,"slot":<0-9>,"hull_id":, "level":,"state":,"state_name":"Mining", "prev_state":,"t":} A full snapshot of all occupied slots is queued as a first sync (X-PRIME-SYNC: 2) whenever fleet_watch establishes a baseline (login, fleet topology change) and whenever another category's first sync is queued. Callbacks run on the game thread and only build JSON and queue it; HTTP stays on the existing sync worker. fleet_watch gains an optional Subscription::on_baseline callback. Default off; documented in every example config. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DNMkDr9v6waKrZ538gutCK --- example_community_patch_settings_en.toml | 3 + mods/src/config.h | 3 + mods/src/defaultconfig.h | 1 + mods/src/patches/fleet_watch.cc | 20 ++++ mods/src/patches/fleet_watch.h | 5 + mods/src/patches/parts/sync.cc | 139 +++++++++++++++++++++++ 6 files changed, 171 insertions(+) diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 05588e2af..003ba088b 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -875,6 +875,9 @@ buffs = true # Do you want to sync building data? buildings = true +# Do you want to sync live fleet state (mining, docked, warping...)? Opt-in, off by default +fleets = false + # Do you want to sync inventory data? inventory = true diff --git a/mods/src/config.h b/mods/src/config.h index 039d891e9..e932dc3a3 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -23,6 +23,7 @@ class SyncConfig Buffs, Buildings, EmeraldChain, + Fleets, Haven, Inventory, Jobs, @@ -50,6 +51,7 @@ class SyncConfig bool battlelogs = false; bool buffs = false; bool buildings = true; + bool fleets = false; bool haven = true; bool inventory = false; bool jobs = false; @@ -72,6 +74,7 @@ constexpr std::array SyncOptions{ SyncConfig::Option{SyncConfig::Type::Buffs, "buff", "buffs", &SyncConfig::buffs}, SyncConfig::Option{SyncConfig::Type::Buildings, "module", "buildings", &SyncConfig::buildings}, SyncConfig::Option{SyncConfig::Type::EmeraldChain, "emerald_chain", "buffs", &SyncConfig::buffs}, + SyncConfig::Option{SyncConfig::Type::Fleets, "fleet", "fleets", &SyncConfig::fleets}, SyncConfig::Option{SyncConfig::Type::Haven, "haven", "haven", &SyncConfig::haven}, SyncConfig::Option{SyncConfig::Type::Inventory, "inventory", "inventory", &SyncConfig::inventory}, SyncConfig::Option{SyncConfig::Type::Jobs, "job", "jobs", &SyncConfig::jobs}, diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index 0ce3a01f2..b415b39b9 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -230,6 +230,7 @@ namespace Sync constexpr bool battlelogs = false; constexpr bool buffs = true; constexpr bool buildings = true; + constexpr bool fleets = false; // opt-in: live fleet state from fleet_watch constexpr bool haven = true; constexpr bool inventory = true; constexpr bool jobs = true; diff --git a/mods/src/patches/fleet_watch.cc b/mods/src/patches/fleet_watch.cc index aa3b27dc2..d089e3c5a 100644 --- a/mods/src/patches/fleet_watch.cc +++ b/mods/src/patches/fleet_watch.cc @@ -165,6 +165,23 @@ void dispatch_transition(int slot, FleetPlayerData* fleet, FleetState before, Fl } } +void dispatch_baseline() +{ + CallbackScope callback_scope; + for (const auto& subscription : s_subscriptions) { + if (!subscription.on_baseline) { + continue; + } + try { + subscription.on_baseline(); + } catch (const std::exception& error) { + spdlog::warn("[FleetWatch] baseline callback failed: {}", error.what()); + } catch (...) { + spdlog::warn("[FleetWatch] baseline callback failed with an unknown exception"); + } + } +} + void observe_fleet(FleetPlayerData* fleet, int requested_slot, bool publish) { if (!fleet || requested_slot < 0 || requested_slot >= kFleetSlotCount) { @@ -360,6 +377,9 @@ void Tick() spdlog::info("[FleetWatchProbe] established stable baseline for {} fleet slots", baseline_count); } #endif + if (final_scan.manager_available) { + dispatch_baseline(); + } } } } else if (lifetime >= kSeedLifetimeMs) { diff --git a/mods/src/patches/fleet_watch.h b/mods/src/patches/fleet_watch.h index f865d278c..4e590f7c1 100644 --- a/mods/src/patches/fleet_watch.h +++ b/mods/src/patches/fleet_watch.h @@ -18,10 +18,15 @@ struct Transition { using TransitionCallback = void (*)(const Transition& transition); using FastPollPredicate = bool (*)(FleetState state); +using BaselineCallback = void (*)(); struct Subscription { TransitionCallback on_transition = nullptr; FastPollPredicate needs_fast_poll = nullptr; + // Optional. Runs on the game thread each time a stable baseline is established while the fleet service is available + // (after login, and again after any fleet-slot topology change). Transitions are only dispatched after a baseline, so + // a consumer that needs the complete fleet list reads FleetsManager here. + BaselineCallback on_baseline = nullptr; }; // Registers a process-lifetime fleet-state observer. The first subscription joins the shared screen-update dispatcher; diff --git a/mods/src/patches/parts/sync.cc b/mods/src/patches/parts/sync.cc index 0fd2ffcff..6e7d1db80 100644 --- a/mods/src/patches/parts/sync.cc +++ b/mods/src/patches/parts/sync.cc @@ -1,12 +1,15 @@ #include "config.h" #include "errormsg.h" #include "file.h" +#include "patches/fleet_watch.h" +#include "patches/screen_update_hook.h" #include "str_utils.h" #include #include #include #include +#include #include #include #include @@ -885,6 +888,12 @@ static void save_previously_sent_logs() } } // namespace trackers +namespace fleets +{ +// Set from any thread when another category's first sync is queued; the game thread answers with a full fleet snapshot. +static std::atomic_bool snapshot_requested{false}; +} // namespace fleets + namespace workers { @@ -909,6 +918,10 @@ static void queue_data(SyncConfig::Type type, const std::string& data, bool is_f static void queue_data(SyncConfig::Type type, const nlohmann::json& data, bool is_first_sync = false) { + if (is_first_sync && type != SyncConfig::Type::Fleets) { + fleets::snapshot_requested.store(true, std::memory_order_release); + } + { std::scoped_lock lk(sync_data_mtx); sync_data_queue.emplace(type, data.dump(), is_first_sync); @@ -2692,6 +2705,130 @@ static void HandleEntityGroup(EntityGroup* entity_group) } } +namespace fleets +{ +constexpr int kFleetSlotCount = 10; + +static const char* state_name(FleetState state) +{ + switch (state) { + case FleetState::IdleInSpace: + return "IdleInSpace"; + case FleetState::Docked: + return "Docked"; + case FleetState::Mining: + return "Mining"; + case FleetState::Destroyed: + return "Destroyed"; + case FleetState::TieringUp: + return "TieringUp"; + case FleetState::Repairing: + return "Repairing"; + case FleetState::Battling: + return "Battling"; + case FleetState::WarpCharging: + return "WarpCharging"; + case FleetState::Warping: + return "Warping"; + case FleetState::Impulsing: + return "Impulsing"; + case FleetState::Capturing: + return "Capturing"; + case FleetState::AutoHunting: + return "AutoHunting"; + case FleetState::Outposting: + return "Outposting"; + default: + return "Unknown"; + } +} + +static int64_t unix_seconds() +{ + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +} + +// Game thread only: reads IL2CPP properties of a FleetPlayerData that is valid for the current frame. +static nlohmann::json record(int slot, FleetPlayerData* fleet, FleetState state, FleetState previous) +{ + auto* hull = fleet->Hull; + const int64_t hull_id = hull ? static_cast(hull->Id) : 0; + + return {{"type", SyncConfig::Type::Fleets}, + {"fid", fleet->Id}, + {"slot", slot}, + {"hull_id", hull_id}, + {"level", fleet->Level}, + {"state", static_cast(state)}, + {"state_name", state_name(state)}, + {"prev_state", static_cast(previous)}, + {"t", unix_seconds()}}; +} + +// Game thread only. A snapshot is the complete list of occupied slots, so it is sent as a first sync. +static void queue_snapshot(const char* reason) +{ + auto* manager = FleetsManager::Instance(); + if (!manager || !manager->HasFleetService()) { + return; + } + + auto fleet_array = nlohmann::json::array(); + for (int slot = 0; slot < kFleetSlotCount; ++slot) { + if (auto* fleet = manager->GetFleetPlayerData(slot); fleet) { + fleet_array.push_back(record(slot, fleet, fleet->CurrentState, fleet->PreviousState)); + } + } + + if (!fleet_array.empty()) { + http::logging::debug("PROCESS", "fleets", STR_FORMAT("Fleet snapshot ({}): {} fleets", reason, fleet_array.size())); + workers::queue_data(SyncConfig::Type::Fleets, fleet_array, true); + } +} + +static void on_transition(const fleet_watch::Transition& transition) +{ + if (!transition.fleet) { + return; + } + + auto fleet_array = nlohmann::json::array(); + fleet_array.push_back( + record(transition.after.slot, transition.fleet, transition.after.state, transition.before.state)); + workers::queue_data(SyncConfig::Type::Fleets, fleet_array); +} + +static void on_baseline() +{ + snapshot_requested.store(false, std::memory_order_relaxed); + queue_snapshot("baseline"); +} + +static void tick() +{ + if (!snapshot_requested.load(std::memory_order_relaxed) + || !snapshot_requested.exchange(false, std::memory_order_acq_rel)) { + return; + } + queue_snapshot("first sync"); +} + +static void install() +{ + if (!Config::Get().sync_options.fleets) { + return; + } + + if (!fleet_watch::Subscribe({.on_transition = on_transition, .on_baseline = on_baseline})) { + spdlog::warn("Sync: fleet state subscriber could not be installed; fleet sync disabled"); + return; + } + + register_screen_manager_update_callback(tick); + spdlog::info("Sync: fleet state sync enabled"); +} +} // namespace fleets + namespace hooks { // IL2CPP value type: keep this parameter by value so following arguments retain their platform ABI positions. @@ -2838,6 +2975,8 @@ void InstallSyncPatches() } } + fleets::install(); + std::thread(workers::ship_sync_data).detach(); std::thread(workers::ship_combat_log_data).detach(); } From 8aa63dc300f1bd4dd856657dd2e20f89a3562a7c Mon Sep 17 00:00:00 2001 From: ProgenyAlpha Date: Tue, 13 Oct 2026 03:15:26 -0400 Subject: [PATCH 2/2] Sync: widen hull ids as unsigned in fleet records HullSpec::Id is read as a 32-bit long and hull ids exceed 2^31 (Kumari is 3005922948), so widening the signed value put negative ids in the records and name lookups failed for those hulls. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DNMkDr9v6waKrZ538gutCK --- mods/src/patches/parts/sync.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mods/src/patches/parts/sync.cc b/mods/src/patches/parts/sync.cc index 6e7d1db80..c94cd672f 100644 --- a/mods/src/patches/parts/sync.cc +++ b/mods/src/patches/parts/sync.cc @@ -2752,7 +2752,9 @@ static int64_t unix_seconds() static nlohmann::json record(int slot, FleetPlayerData* fleet, FleetState state, FleetState previous) { auto* hull = fleet->Hull; - const int64_t hull_id = hull ? static_cast(hull->Id) : 0; + // HullSpec::Id is read as a 32-bit long; hull ids run past 2^31 (e.g. Kumari 3005922948), so take + // the value as unsigned before widening or those come out negative in the record. + const int64_t hull_id = hull ? static_cast(static_cast(hull->Id)) : 0; return {{"type", SyncConfig::Type::Fleets}, {"fid", fleet->Id},