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); + } };