From 10ebb9a4355cdc063756fbba4b033769c0fe1ac8 Mon Sep 17 00:00:00 2001 From: blevouschurverantunt <35274597+blevouschurverantunt@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:26:52 -0500 Subject: [PATCH] Sync the local player's own profile (opt-in profile) New opt-in sync type `profile`: one `profile` event with level, xp, military_might and planetary_base_unlocked, plus the player's own uid. Source is the UserProfiles entity group the client already receives; other players' records are never read or forwarded. Sent at first sight and when level, Power or the Haven flag change. The server record's level is 0, so level is read from the in-memory local profile and can lag one response after a level-up. Co-Authored-By: Claude Fable 5.1 --- example_community_patch_settings_en.toml | 4 + mods/src/config.h | 3 + mods/src/defaultconfig.h | 1 + mods/src/patches/parts/sync.cc | 111 +++++++++++++++++++---- mods/src/prime/UserProfile.h | 7 ++ 5 files changed, 109 insertions(+), 17 deletions(-) diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 33c835d5e..5fc86e459 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -940,6 +940,10 @@ missions = true # Do you want to sync owned officer data? officers = true +# Do you want to sync your own player profile (level, XP, Power, Haven unlocked)? +# Only your own record is sent, never other players'. +profile = false + # Do you want to sync research data? research = true diff --git a/mods/src/config.h b/mods/src/config.h index cf19f9982..77663a1ce 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -28,6 +28,7 @@ class SyncConfig Jobs, Missions, Officer, + Profile, Research, Resources, Ships, @@ -55,6 +56,7 @@ class SyncConfig bool jobs = false; bool missions = false; bool officers = false; + bool profile = false; bool research = true; bool resources = false; bool ships = false; @@ -77,6 +79,7 @@ constexpr std::array SyncOptions{ SyncConfig::Option{SyncConfig::Type::Jobs, "job", "jobs", &SyncConfig::jobs}, SyncConfig::Option{SyncConfig::Type::Missions, "mission", "missions", &SyncConfig::missions}, SyncConfig::Option{SyncConfig::Type::Officer, "officer", "officers", &SyncConfig::officers}, + SyncConfig::Option{SyncConfig::Type::Profile, "profile", "profile", &SyncConfig::profile}, SyncConfig::Option{SyncConfig::Type::Research, "research", "research", &SyncConfig::research}, SyncConfig::Option{SyncConfig::Type::Resources, "resource", "resources", &SyncConfig::resources}, SyncConfig::Option{SyncConfig::Type::Ships, "ship", "ships", &SyncConfig::ships}, diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index 584987176..f1da5b15a 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -245,6 +245,7 @@ namespace Sync constexpr bool jobs = true; constexpr bool missions = true; constexpr bool officers = true; + constexpr bool profile = false; constexpr const char* proxy = ""; constexpr bool research = true; constexpr bool resources = true; diff --git a/mods/src/patches/parts/sync.cc b/mods/src/patches/parts/sync.cc index bba2c6a2c..cef106f60 100644 --- a/mods/src/patches/parts/sync.cc +++ b/mods/src/patches/parts/sync.cc @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -1556,6 +1557,24 @@ static void planetary_map_prosperity(std::unique_ptr&& bytes) } } +// The game's record of the local player, or nullptr. Calls into IL2CPP: game thread only. +static UserProfile* local_user_profile() +{ + auto* const manager = UserProfileManager::Instance(); + if (manager == nullptr) { + return nullptr; + } + + static auto fn = UserProfileManager::get_class_helper().GetMethodInfo("GetLocalUserProfile"); + if (fn == nullptr) { + return nullptr; + } + + Il2CppException* exception = nullptr; + auto* const profile = reinterpret_cast(il2cpp_runtime_invoke(fn, manager, nullptr, &exception)); + return exception == nullptr ? profile : nullptr; +} + static void planetary_map(std::unique_ptr&& bytes) { if (auto response = Digit::PrimeServer::Models::PlanetaryMapResponse(); response.ParseFromString(*bytes)) { @@ -1563,23 +1582,8 @@ static void planetary_map(std::unique_ptr&& bytes) const auto& owner = response.owneruserid(); if (!owner.empty()) { - auto profileManager = UserProfileManager::Instance(); - if (profileManager == nullptr) { - spdlog::error("Failed to get UserProfileManager instance"); - return; - } - - static auto& class_helper = profileManager->get_class_helper(); - static auto fn = class_helper.GetMethodInfo("GetLocalUserProfile"); - - if (fn == nullptr ) { - spdlog::error("Failed to get method info for GetLocalUserProfile"); - return; - } - - Il2CppException* exception = nullptr; - auto localProfile = reinterpret_cast(il2cpp_runtime_invoke(fn, profileManager, nullptr, &exception)); - if (exception != nullptr || localProfile == nullptr) { + auto localProfile = local_user_profile(); + if (localProfile == nullptr) { spdlog::error("Failed to get local user profile"); return; } @@ -1602,6 +1606,70 @@ static void planetary_map(std::unique_ptr&& bytes) } } +// The local player's user id, or "" if the profile is not available. Game thread only. +static std::string local_user_id(UserProfile* profile) +{ + if (profile == nullptr) { + return {}; + } + + const auto* const user_id = profile->UserId; + return user_id == nullptr ? std::string{} : to_string(user_id); +} + +// Sends the local player's own profile; `self` and `level` come from the game thread. Only the record whose user id +// is `self` is read. The server record carries level 0, so the in-memory Level is used: it lags one own-record +// response after a level-up. +static void own_profile(std::unique_ptr&& bytes, const std::string& self, int32_t level) +{ + using json = nlohmann::json; + + static std::tuple last_sent; + static bool has_sent = false; + static std::mutex last_sent_mtx; + + auto response = Digit::PrimeServer::Models::UserProfilesResponse(); + if (!response.ParseFromString(*bytes)) { + spdlog::error("Failed to parse own user profile"); + return; + } + + const Digit::PrimeServer::Models::UserProfile* own = nullptr; + for (const auto& profile : response.userprofiles()) { + if (profile.userid() == self) { + own = &profile; + break; + } + } + + if (own == nullptr) { + return; + } + + // XP rides along but is not part of the key: an XP-only change waits for the next keyed change. + const auto key = std::make_tuple(level, own->militarymight(), own->planetarybaseunlocked()); + bool first_sync; + { + std::scoped_lock lk(last_sent_mtx); + first_sync = !has_sent; + if (!first_sync && last_sent == key) { + return; + } + last_sent = key; + has_sent = true; + } + + auto profile_array = json::array(); + profile_array.push_back({{"type", SyncConfig::Type::Profile}, + {"uid", self}, + {"level", level}, + {"xp", own->xp()}, + {"military_might", own->militarymight()}, + {"planetary_base_unlocked", own->planetarybaseunlocked()}}); + + workers::queue_data(SyncConfig::Type::Profile, profile_array, first_sync); +} + static void player_inventories(std::unique_ptr&& bytes) { using json = nlohmann::json; @@ -2532,6 +2600,15 @@ static void HandleEntityGroup(EntityGroup* entity_group) if (sync_options.battlelogs) { submit_async(trackers::names::cache_player_names); } + if (sync_options.profile) { + // Skipped if the local profile is not ready; a later response retries. + auto* const local = processors::local_user_profile(); + if (auto self = processors::local_user_id(local); !self.empty()) { + submit_async([self = std::move(self), level = local->Level](std::unique_ptr&& bytes) { + processors::own_profile(std::move(bytes), self, level); + }); + } + } break; case EntityGroup::Type::AllianceProfiles: if (sync_options.battlelogs) { diff --git a/mods/src/prime/UserProfile.h b/mods/src/prime/UserProfile.h index d202a9592..6335c6bb4 100644 --- a/mods/src/prime/UserProfile.h +++ b/mods/src/prime/UserProfile.h @@ -7,6 +7,7 @@ struct UserProfile { __declspec(property(get = __get_LocaId)) long LocaId; __declspec(property(get = __get_Name)) Il2CppString* Name; __declspec(property(get = __get_UserId)) Il2CppString* UserId; + __declspec(property(get = __get_Level)) int32_t Level; private: static IL2CppClassHelper& get_class_helper() @@ -34,4 +35,10 @@ struct UserProfile { static auto prop = get_class_helper().GetProperty("UserId"); return prop.GetRaw(this); } + + int32_t __get_Level() + { + static auto prop = get_class_helper().GetProperty("Level"); + return *prop.Get(this); + } };