Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions example_community_patch_settings_en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions mods/src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class SyncConfig
Buffs,
Buildings,
EmeraldChain,
Fleets,
Haven,
Inventory,
Jobs,
Expand Down Expand Up @@ -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;
Expand All @@ -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},
Expand Down
1 change: 1 addition & 0 deletions mods/src/defaultconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions mods/src/patches/fleet_watch.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions mods/src/patches/fleet_watch.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
141 changes: 141 additions & 0 deletions mods/src/patches/parts/sync.cc
Original file line number Diff line number Diff line change
@@ -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 <Digit.PrimeServer.Models.pb.h>
#include <il2cpp-api-types.h>
#include <il2cpp/il2cpp_helper.h>
#include <prime/EntityGroup.h>
#include <prime/FleetsManager.h>
#include <prime/RealtimeDataPayload.h>
#include <prime/ServiceResponse.h>
#include <prime/UserProfileManager.h>
Expand Down Expand Up @@ -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
{

Expand All @@ -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);
Expand Down Expand Up @@ -2692,6 +2705,132 @@ 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::seconds>(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;
// 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<int64_t>(static_cast<uint32_t>(hull->Id)) : 0;

return {{"type", SyncConfig::Type::Fleets},
{"fid", fleet->Id},
{"slot", slot},
{"hull_id", hull_id},
{"level", fleet->Level},
{"state", static_cast<int>(state)},
{"state_name", state_name(state)},
{"prev_state", static_cast<int>(previous)},
Comment on lines +2764 to +2766

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistent, shouldn't we include prev_state_name too?

{"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.
Expand Down Expand Up @@ -2838,6 +2977,8 @@ void InstallSyncPatches()
}
}

fleets::install();

std::thread(workers::ship_sync_data).detach();
std::thread(workers::ship_combat_log_data).detach();
}