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
4 changes: 4 additions & 0 deletions example_community_patch_settings_en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions mods/src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class SyncConfig
Jobs,
Missions,
Officer,
Profile,
Research,
Resources,
Ships,
Expand Down Expand Up @@ -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;
Expand All @@ -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},
Expand Down
1 change: 1 addition & 0 deletions mods/src/defaultconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
111 changes: 94 additions & 17 deletions mods/src/patches/parts/sync.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include <string>
#include <string_view>
#include <thread>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
Expand Down Expand Up @@ -1556,30 +1557,33 @@ static void planetary_map_prosperity(std::unique_ptr<std::string>&& 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<UserProfile*>(il2cpp_runtime_invoke(fn, manager, nullptr, &exception));
return exception == nullptr ? profile : nullptr;
}

static void planetary_map(std::unique_ptr<std::string>&& bytes)
{
if (auto response = Digit::PrimeServer::Models::PlanetaryMapResponse(); response.ParseFromString(*bytes)) {
http::logging::trace("PROCESS", "planetary map", STR_FORMAT("Processing {} haven structures", response.mapdata().buildings_size()));

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<UserProfile*>(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;
}
Expand All @@ -1602,6 +1606,70 @@ static void planetary_map(std::unique_ptr<std::string>&& 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<std::string>&& bytes, const std::string& self, int32_t level)
{
using json = nlohmann::json;

static std::tuple<int32_t, uint64_t, bool> 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<std::string>&& bytes)
{
using json = nlohmann::json;
Expand Down Expand Up @@ -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<std::string>&& bytes) {
processors::own_profile(std::move(bytes), self, level);
});
}
}
break;
case EntityGroup::Type::AllianceProfiles:
if (sync_options.battlelogs) {
Expand Down
7 changes: 7 additions & 0 deletions mods/src/prime/UserProfile.h
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -34,4 +35,10 @@ struct UserProfile {
static auto prop = get_class_helper().GetProperty("UserId");
return prop.GetRaw<Il2CppString>(this);
}

int32_t __get_Level()
{
static auto prop = get_class_helper().GetProperty("Level");
return *prop.Get<int32_t>(this);
}
};
Loading