diff --git a/example_community_patch_settings_da.toml b/example_community_patch_settings_da.toml index 2069b388a..71d2722eb 100644 --- a/example_community_patch_settings_da.toml +++ b/example_community_patch_settings_da.toml @@ -813,6 +813,9 @@ show_station_cargo = true # Subgroup: Data # -------------- +# Vil du synkronisere landgangsholdsdata? +away_assignments = true + # Vil du synkronisere kamplogge? battlelogs = true diff --git a/example_community_patch_settings_de.toml b/example_community_patch_settings_de.toml index dc626072b..e7b13f68d 100644 --- a/example_community_patch_settings_de.toml +++ b/example_community_patch_settings_de.toml @@ -813,6 +813,9 @@ show_station_cargo = true # Subgroup: Daten # --------------- +# Außenteam-Einsatzdaten synchronisieren? +away_assignments = true + # Kampfberichte synchronisieren? battlelogs = true diff --git a/example_community_patch_settings_en-GB-x-cockney.toml b/example_community_patch_settings_en-GB-x-cockney.toml index e1aed8205..73b82c5d6 100644 --- a/example_community_patch_settings_en-GB-x-cockney.toml +++ b/example_community_patch_settings_en-GB-x-cockney.toml @@ -813,6 +813,9 @@ show_station_cargo = true # Subgroup: Data # -------------- +# Do you want to sync away team assignment data? +away_assignments = true + # Do you want to sync battlelogs? battlelogs = true diff --git a/example_community_patch_settings_en-x-minionese.toml b/example_community_patch_settings_en-x-minionese.toml index 562af2608..bf2a3f9db 100644 --- a/example_community_patch_settings_en-x-minionese.toml +++ b/example_community_patch_settings_en-x-minionese.toml @@ -813,6 +813,9 @@ show_station_cargo = true # Subgroup: Data # -------------- +# Do you want to sync away team assignment data? +away_assignments = true + # Do you want to sync battlelogs? battlelogs = true diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 8dcc031a6..c1472e4be 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -833,6 +833,9 @@ show_station_cargo = true # Subgroup: Data # -------------- +# Do you want to sync away team assignment data? +away_assignments = true + # Do you want to sync battlelogs? battlelogs = true diff --git a/example_community_patch_settings_es.toml b/example_community_patch_settings_es.toml index 795c8f1c8..80f172170 100644 --- a/example_community_patch_settings_es.toml +++ b/example_community_patch_settings_es.toml @@ -813,6 +813,9 @@ show_station_cargo = true # Subgroup: Data # -------------- +# ¿Quieres sincronizar los datos de asignaciones de equipos de exploración? +away_assignments = true + # ¿Quieres sincronizar los registros de batalla? battlelogs = true diff --git a/example_community_patch_settings_fr.toml b/example_community_patch_settings_fr.toml index 8bf856ea3..8fcdeaaff 100644 --- a/example_community_patch_settings_fr.toml +++ b/example_community_patch_settings_fr.toml @@ -813,6 +813,9 @@ show_station_cargo = true # Subgroup: Data # -------------- +# Voulez-vous synchroniser les données des missions d'équipe d'intervention ? +away_assignments = true + # Voulez-vous synchroniser les journaux de combat ? battlelogs = true diff --git a/example_community_patch_settings_nl.toml b/example_community_patch_settings_nl.toml index 719b54a1d..f9720abdd 100644 --- a/example_community_patch_settings_nl.toml +++ b/example_community_patch_settings_nl.toml @@ -813,6 +813,9 @@ show_station_cargo = true # Subgroup: Data # -------------- +# Wil je data van landingsteam-opdrachten syncen? +away_assignments = true + # Wil je Gevechtslogboeken syncen? battlelogs = true diff --git a/example_community_patch_settings_ru.toml b/example_community_patch_settings_ru.toml index 6e1d0ca2a..573b953b2 100644 --- a/example_community_patch_settings_ru.toml +++ b/example_community_patch_settings_ru.toml @@ -813,6 +813,9 @@ show_station_cargo = true # Subgroup: Data # -------------- +# Синхронизировать данные заданий десантной группы? +away_assignments = true + # Синхронизировать журналы боёв? battlelogs = true diff --git a/example_community_patch_settings_tlh.toml b/example_community_patch_settings_tlh.toml index ca88ac0b1..0b4d726e4 100644 --- a/example_community_patch_settings_tlh.toml +++ b/example_community_patch_settings_tlh.toml @@ -813,6 +813,9 @@ show_station_cargo = true # Subgroup: Data # -------------- +# Do you want Daq sync away team assignment De'? +away_assignments = true + # Do you want Daq sync battlelogs? battlelogs = true diff --git a/mods/src/config.h b/mods/src/config.h index 2ab320ce8..ada789575 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -16,6 +16,7 @@ class SyncConfig { public: enum class Type { + AwayAssignments, Battles, Buffs, Buildings, @@ -42,26 +43,29 @@ class SyncConfig std::string proxy; - bool verify_ssl = true; - bool battlelogs = false; - bool buffs = false; - bool buildings = true; - bool haven = true; - bool inventory = false; - bool jobs = false; - bool missions = false; - bool officer = false; - bool research = true; - bool resources = false; - bool ships = false; - bool slots = false; - bool tech = false; - bool traits = false; + bool verify_ssl = true; + bool away_assignments = false; + bool battlelogs = false; + bool buffs = false; + bool buildings = true; + bool haven = true; + bool inventory = false; + bool jobs = false; + bool missions = false; + bool officer = false; + bool research = true; + bool resources = false; + bool ships = false; + bool slots = false; + bool tech = false; + bool traits = false; [[nodiscard]] bool enabled(Type type) const; }; constexpr std::array SyncOptions{ + SyncConfig::Option{SyncConfig::Type::AwayAssignments, "away_assignment", "away_assignments", + &SyncConfig::away_assignments}, SyncConfig::Option{SyncConfig::Type::Battles, "battlelog", "battlelogs", &SyncConfig::battlelogs}, SyncConfig::Option{SyncConfig::Type::Buffs, "buff", "buffs", &SyncConfig::buffs}, SyncConfig::Option{SyncConfig::Type::Buildings, "module", "buildings", &SyncConfig::buildings}, diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index 10764b99f..0ed5e64bd 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -217,6 +217,7 @@ namespace Sync constexpr const char* spocksclub_url = "https://spocks.club/sync/ingress/"; constexpr const char* nextspocksclub_url = "https://next.spocks.club/sync/ingress/"; + constexpr bool away_assignments = true; constexpr bool battlelogs = true; constexpr bool buffs = true; constexpr bool buildings = true; diff --git a/mods/src/patches/parts/sync.cc b/mods/src/patches/parts/sync.cc index 6d96a8768..6a0221bfb 100644 --- a/mods/src/patches/parts/sync.cc +++ b/mods/src/patches/parts/sync.cc @@ -796,6 +796,9 @@ static std::mutex resource_states_alliance_mtx; static std::unordered_map slot_states; static std::mutex slot_states_mtx; +static std::unordered_map away_assignment_states; +static std::mutex away_assignment_states_mtx; + static std::unordered_map structure_states; static std::mutex structure_states_mtx; @@ -1095,6 +1098,110 @@ static void ship_combat_log_data() namespace processors { +// Builds the sync event for one away team assignment instance and appends it to `out_array` only if the +// instance's observable state changed since the last time it was emitted. Caller must hold +// trackers::away_assignment_states_mtx. +static void away_assignment_event(const Digit::PrimeServer::Models::AwayAssignmentInstance& instance, + nlohmann::json& out_array) +{ + using json = nlohmann::json; + using trackers::away_assignment_states; + + // Protobuf map iteration order is not guaranteed to match slot order, so sort by slot index + // before emitting officer_ids to keep the array (and therefore the de-dup hash) stable. + std::vector> officer_slots(instance.officerids().begin(), instance.officerids().end()); + std::ranges::sort(officer_slots, {}, &std::pair::first); + + auto officer_ids = json::array(); + for (const auto& [slot, officer_id] : officer_slots) { + if (officer_id != 0) { + officer_ids.push_back(officer_id); + } + } + + json event = {{"type", SyncConfig::Type::AwayAssignments}, + {"aid", instance.id()}, + {"template_id", instance.awayassignmenttemplateid()}, + {"state", instance.state()}, + {"officer_ids", officer_ids}, + {"job_uuid", instance.jobuuid()}, + {"duration", instance.parameters().duration()}, + {"rarity", instance.rarity()}}; + + const auto state_value = std::hash{}(event); + + if (const auto& it = away_assignment_states.find(instance.id()); + it == away_assignment_states.end() || it->second != state_value) { + away_assignment_states[instance.id()] = state_value; + out_array.push_back(std::move(event)); + } +} + +static void away_assignments_list(std::unique_ptr&& bytes) +{ + using json = nlohmann::json; + static std::atomic_bool is_first_sync{true}; + + if (auto response = Digit::PrimeServer::Models::AwayAssignmentUserListResponse(); response.ParseFromString(*bytes)) { + + http::logging::trace("PROCESS", "away assignments", + STR_FORMAT("Processing {} away assignments", response.instances_size())); + + std::unordered_set ids_in_response; + ids_in_response.reserve(static_cast(response.instances_size())); + auto assignment_array = json::array(); + + { + std::scoped_lock lk(trackers::away_assignment_states_mtx); + + for (const auto& instance : response.instances()) { + ids_in_response.insert(instance.id()); + away_assignment_event(instance, assignment_array); + } + + // Prune entries that are no longer present to prevent unbounded growth + for (auto it = trackers::away_assignment_states.begin(); it != trackers::away_assignment_states.end();) { + if (!ids_in_response.contains(it->first)) { + assignment_array.push_back({{"type", "collected_" + SyncConfig::Type::AwayAssignments}, {"aid", it->first}}); + it = trackers::away_assignment_states.erase(it); + } else { + ++it; + } + } + } + + if (!assignment_array.empty()) { + const bool first_sync = is_first_sync.exchange(false, std::memory_order_acq_rel); + workers::queue_data(SyncConfig::Type::AwayAssignments, assignment_array, first_sync); + } + } else { + spdlog::error("Failed to parse away assignments"); + } +} + +static void away_assignment_instance(std::unique_ptr&& bytes) +{ + using json = nlohmann::json; + + if (auto instance = Digit::PrimeServer::Models::AwayAssignmentInstance(); instance.ParseFromString(*bytes)) { + + http::logging::trace("PROCESS", "away assignment instance", + STR_FORMAT("Processing away assignment {}", instance.id())); + + auto assignment_array = json::array(); + { + std::scoped_lock lk(trackers::away_assignment_states_mtx); + away_assignment_event(instance, assignment_array); + } + + if (!assignment_array.empty()) { + workers::queue_data(SyncConfig::Type::AwayAssignments, assignment_array); + } + } else { + spdlog::error("Failed to parse away assignment instance"); + } +} + static void queue_battle_ids(const std::vector& battle_ids) { std::vector to_enqueue; @@ -1502,6 +1609,10 @@ static void jobs(std::unique_ptr&& bytes) const auto& scrap = job.scrapyardparams(); job_params = {{"psid", scrap.shipid()}, {"hull_id", scrap.hullid()}, {"level", scrap.level()}}; } break; + case Digit::PrimeServer::Models::JOBTYPE_AWAYASSIGNMENT: { + const auto& away_assignment = job.awayassignmentparams(); + job_params = {{"aid", away_assignment.awayassignmentinstanceid()}}; + } break; default: continue; } @@ -2301,6 +2412,18 @@ static void HandleEntityGroup(EntityGroup* entity_group) const auto& sync_options = Config::Get().sync_options; switch (entity_group->Type_) { + // away assignments + case EntityGroup::Type::AwayAssignmentsList: + if (sync_options.away_assignments) { + submit_async(processors::away_assignments_list); + } + break; + case EntityGroup::Type::AwayAssignmentsInstance: + if (sync_options.away_assignments) { + submit_async(processors::away_assignment_instance); + } + break; + // battlelogs case EntityGroup::Type::BattleResultHeaders: if (sync_options.battlelogs) {