diff --git a/src/skate3_user_settings.cpp b/src/skate3_user_settings.cpp new file mode 100644 index 0000000..8a93f23 --- /dev/null +++ b/src/skate3_user_settings.cpp @@ -0,0 +1,212 @@ +#include "skate3_user_settings.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace skate3 { +namespace { + +std::string MakeProfileId(std::string_view gamertag) { + std::string id; + id.reserve(gamertag.size()); + bool previous_dash = false; + for (char c : gamertag) { + char out = 0; + if (c >= 'A' && c <= 'Z') { + out = static_cast(c - 'A' + 'a'); + } else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { + out = c; + } else if (!previous_dash) { + out = '-'; + } + if (out) { + id.push_back(out); + previous_dash = out == '-'; + } + } + while (!id.empty() && id.back() == '-') { + id.pop_back(); + } + if (id.empty()) { + id = "default"; + } + return id; +} + +uint64_t StableXuidForId(std::string_view id) { + uint64_t hash = 1469598103934665603ull; + for (char c : id) { + hash ^= static_cast(c); + hash *= 1099511628211ull; + } + return 0xB13E000000000000ull | (hash & 0x00003FFFFFFFFFFFull); +} + +uint64_t ParseXuidString(std::string_view value) { + std::string text(value); + if (text.starts_with("0x") || text.starts_with("0X")) { + text.erase(0, 2); + } + uint64_t parsed = 0; + auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), parsed, 16); + if (ec == std::errc() && ptr == text.data() + text.size()) { + return parsed; + } + parsed = 0; + std::from_chars(value.data(), value.data() + value.size(), parsed, 10); + return parsed; +} + +std::string TomlString(std::string_view value) { + std::string out = "\""; + for (char c : value) { + if (c == '\\' || c == '"') { + out.push_back('\\'); + } + out.push_back(c); + } + out.push_back('"'); + return out; +} + +} // namespace + +std::filesystem::path ProfilesFilePath(const std::filesystem::path& user_data_root) { + return user_data_root / "profiles" / "profiles.toml"; +} + +LocalProfileStore LoadProfiles(const std::filesystem::path& profiles_path) { + LocalProfileStore store; + if (!std::filesystem::exists(profiles_path)) { + return store; + } + try { + auto table = toml::parse_file(profiles_path.string()); + store.selected_profile = table["selected_profile"].value_or(std::string{}); + if (auto profiles = table["profiles"].as_array()) { + profiles->for_each([&](toml::table& profile_table) { + LocalProfile profile; + profile.id = profile_table["id"].value_or(std::string{}); + profile.gamertag = profile_table["gamertag"].value_or(std::string{}); + profile.signed_in = profile_table["signed_in"].value_or(true); + profile.live_signed_in = profile_table["live_signed_in"].value_or(false); + if (auto xuid = profile_table["xuid"].value()) { + profile.xuid = ParseXuidString(*xuid); + } else if (auto xuid_int = profile_table["xuid"].value()) { + profile.xuid = static_cast(*xuid_int); + } + if (!profile.id.empty() && !profile.gamertag.empty() && profile.xuid != 0) { + store.profiles.push_back(std::move(profile)); + } + }); + } + } catch (const toml::parse_error&) { + store = {}; + } + return store; +} + +bool SaveProfiles(const std::filesystem::path& profiles_path, const LocalProfileStore& store) { + std::error_code ec; + std::filesystem::create_directories(profiles_path.parent_path(), ec); + if (ec) { + return false; + } + + std::ofstream file(profiles_path, std::ios::trunc); + if (!file) { + return false; + } + + file << "selected_profile = " << TomlString(store.selected_profile) << "\n\n"; + for (const auto& profile : store.profiles) { + file << "[[profiles]]\n"; + file << "id = " << TomlString(profile.id) << "\n"; + file << "gamertag = " << TomlString(profile.gamertag) << "\n"; + file << "xuid = " << TomlString(FormatXuid(profile.xuid)) << "\n"; + file << "signed_in = " << (profile.signed_in ? "true" : "false") << "\n"; + file << "live_signed_in = " << (profile.live_signed_in ? "true" : "false") << "\n\n"; + } + return true; +} + +LocalProfile MakeDefaultProfile(std::string gamertag) { + if (gamertag.empty()) { + gamertag = "Player"; + } + LocalProfile profile; + profile.id = MakeProfileId(gamertag); + profile.gamertag = std::move(gamertag); + profile.xuid = StableXuidForId(profile.id); + return profile; +} + +LocalProfile* FindSelectedProfile(LocalProfileStore& store) { + auto it = std::find_if(store.profiles.begin(), store.profiles.end(), + [&](const LocalProfile& profile) { + return profile.id == store.selected_profile; + }); + if (it != store.profiles.end()) { + return &*it; + } + return store.profiles.empty() ? nullptr : &store.profiles.front(); +} + +const LocalProfile* FindSelectedProfile(const LocalProfileStore& store) { + auto it = std::find_if(store.profiles.begin(), store.profiles.end(), + [&](const LocalProfile& profile) { + return profile.id == store.selected_profile; + }); + if (it != store.profiles.end()) { + return &*it; + } + return store.profiles.empty() ? nullptr : &store.profiles.front(); +} + +void EnsureUsableProfileStore(LocalProfileStore& store, std::string default_gamertag) { + if (store.profiles.empty()) { + auto profile = MakeDefaultProfile(std::move(default_gamertag)); + store.selected_profile = profile.id; + store.profiles.push_back(std::move(profile)); + return; + } + if (!FindSelectedProfile(store)) { + store.selected_profile = store.profiles.front().id; + } +} + +void ApplyProfileCvars(const LocalProfile& profile) { + rex::cvar::SetFlagByName("selected_user_profile", profile.id); + rex::cvar::SetFlagByName("user_profile_name", profile.gamertag); + rex::cvar::SetFlagByName("user_profile_xuid", FormatXuid(profile.xuid)); + rex::cvar::SetFlagByName("user_profile_signed_in", profile.signed_in ? "true" : "false"); + rex::cvar::SetFlagByName("user_live_signed_in", + profile.signed_in && profile.live_signed_in ? "true" : "false"); +} + +void ApplyVideoCvars(int resolution_scale, double refresh_rate) { + resolution_scale = std::clamp(resolution_scale, 1, 8); + refresh_rate = std::clamp(refresh_rate, 24.0, 240.0); + const auto scale = std::to_string(resolution_scale); + rex::cvar::SetFlagByName("resolution_scale", scale); + rex::cvar::SetFlagByName("draw_resolution_scale_x", scale); + rex::cvar::SetFlagByName("draw_resolution_scale_y", scale); + rex::cvar::SetFlagByName("video_mode_refresh_rate", std::to_string(refresh_rate)); +} + +std::string FormatXuid(uint64_t xuid) { + std::ostringstream stream; + stream << std::uppercase << std::hex << std::setw(16) << std::setfill('0') << xuid; + return stream.str(); +} + +} // namespace skate3 diff --git a/src/skate3_user_settings.h b/src/skate3_user_settings.h new file mode 100644 index 0000000..3fdc520 --- /dev/null +++ b/src/skate3_user_settings.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +namespace skate3 { + +struct LocalProfile { + std::string id; + std::string gamertag; + uint64_t xuid = 0; + bool signed_in = true; + bool live_signed_in = false; +}; + +struct LocalProfileStore { + std::string selected_profile; + std::vector profiles; +}; + +std::filesystem::path ProfilesFilePath(const std::filesystem::path& user_data_root); +LocalProfileStore LoadProfiles(const std::filesystem::path& profiles_path); +bool SaveProfiles(const std::filesystem::path& profiles_path, const LocalProfileStore& store); +LocalProfile MakeDefaultProfile(std::string gamertag); +LocalProfile* FindSelectedProfile(LocalProfileStore& store); +const LocalProfile* FindSelectedProfile(const LocalProfileStore& store); +void EnsureUsableProfileStore(LocalProfileStore& store, std::string default_gamertag); +void ApplyProfileCvars(const LocalProfile& profile); +void ApplyVideoCvars(int resolution_scale, double refresh_rate); +std::string FormatXuid(uint64_t xuid); + +} // namespace skate3