[XAM/User] Add support for multiple signed-in users/profiles

This adds support for multiple profiles to Xenia, profiles can be configured with the [Profiles] user_*_xuid / user_*_state config settings.
If state is non-zero (1 = offline, 2 = LIVE), the profile will be counted as logged-on - either with a generated XeniaUser gamertag, or if the XUID is set to 1 the first available profile will be loaded.
The XUID can also be set to the offline-XUID (E000...) of an existing profile, to sign in the user as that profile.
(Profiles should be stored in the Xenia content/FFFE07D1/00010000/ folder, either as an STFS package or an extracted folder)

All the XamUser* functions have been updated to support multiple user_index's provided to them too.
(there's still issues with weird indexes like 0xFF, 0x7FF9... being given though, still dunno what's with that, the KernelState::user_profile() code will treat 0xFF as 0)

I'm not really sure if this is the most ideal way to do things though, but it does appear to work fine, at least Halo 3 does detect the profiles with state > 0 fine.

TODO: look into changing up xam_content to make use of user_index & profiles.
It shouldn't be too difficult now to emulate the same content paths X360 uses (seperating content by XUID etc)
Would probably be a good idea, since it'd probably be needed for us to support multiple profiles properly, so that they don't all share savegames etc...
This commit is contained in:
emoose 2020-01-03 00:05:00 +00:00 committed by Gliniak
parent dbaa00cef0
commit ffed5eda3a
8 changed files with 450 additions and 214 deletions

View File

@ -702,7 +702,13 @@ X_STATUS Emulator::CompleteLaunch(const std::wstring& path,
if (spa.Read(module->memory()->TranslateVirtual(resource_data), if (spa.Read(module->memory()->TranslateVirtual(resource_data),
resource_size)) { resource_size)) {
// Set title SPA and get title name/icon // Set title SPA and get title name/icon
kernel_state_->user_profile()->SetTitleSpaData(spa); for (uint32_t i = 0; i < kernel_state_->num_profiles(); i++) {
auto profile = kernel_state_->user_profile(i);
if (profile) {
profile->SetTitleSpaData(spa);
}
}
game_title_ = xe::to_wstring(spa.GetTitleName()); game_title_ = xe::to_wstring(spa.GetTitleName());
auto icon_block = spa.GetIcon(); auto icon_block = spa.GetIcon();
if (icon_block) { if (icon_block) {

View File

@ -53,7 +53,7 @@ KernelState::KernelState(Emulator* emulator)
content_root = xe::to_absolute_path(content_root); content_root = xe::to_absolute_path(content_root);
content_manager_ = std::make_unique<xam::ContentManager>(this, content_root); content_manager_ = std::make_unique<xam::ContentManager>(this, content_root);
user_profile_ = std::make_unique<xam::UserProfile>(this); xam::UserProfile::CreateUsers(this, user_profiles_);
assert_null(shared_kernel_state_); assert_null(shared_kernel_state_);
shared_kernel_state_ = this; shared_kernel_state_ = this;

View File

@ -102,7 +102,42 @@ class KernelState {
xam::ContentManager* content_manager() const { xam::ContentManager* content_manager() const {
return content_manager_.get(); return content_manager_.get();
} }
xam::UserProfile* user_profile() const { return user_profile_.get(); }
// Returns pointer to UserProfile for the given user_index
// Returns nullptr if user_index is invalid, or user isn't signed in
// (unless allow_signed_out is true, to allow for code dealing with sign-ins
// etc)
xam::UserProfile* user_profile(uint32_t user_index,
bool allow_signed_out = false) const {
if (user_index == 0xFF) {
user_index = 0;
}
if (user_index >= xam::kMaxNumUsers) {
return nullptr;
}
auto user = &user_profiles_[user_index];
if (!allow_signed_out) {
if (!user->get()->signed_in()) {
return nullptr;
}
}
return user->get();
}
uint32_t num_profiles() const { return xam::kMaxNumUsers; }
xam::UserProfile* user_profile(uint64_t xuid) const {
for (int i = 0; i < xam::kMaxNumUsers; i++) {
auto profile = user_profiles_[i].get();
if (profile->xuid() == xuid || profile->xuid_offline() == xuid) {
return profile;
}
}
return nullptr;
}
// Access must be guarded by the global critical region. // Access must be guarded by the global critical region.
util::ObjectTable* object_table() { return &object_table_; } util::ObjectTable* object_table() { return &object_table_; }
@ -188,7 +223,8 @@ class KernelState {
std::unique_ptr<xam::AppManager> app_manager_; std::unique_ptr<xam::AppManager> app_manager_;
std::unique_ptr<xam::ContentManager> content_manager_; std::unique_ptr<xam::ContentManager> content_manager_;
std::unique_ptr<xam::UserProfile> user_profile_;
std::unique_ptr<xam::UserProfile> user_profiles_[xam::kMaxNumUsers];
xe::global_critical_region global_critical_region_; xe::global_critical_region global_critical_region_;

View File

@ -61,17 +61,21 @@ X_RESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
XELOGD("XGIUserWriteAchievements(%.8X, %.8X)", achievement_count, XELOGD("XGIUserWriteAchievements(%.8X, %.8X)", achievement_count,
achievements_ptr); achievements_ptr);
auto* game_gpd = kernel_state_->user_profile()->GetTitleGpd();
if (!game_gpd) {
XELOGE("XGIUserWriteAchievements failed, no game GPD set?");
return X_ERROR_SUCCESS;
}
bool modified = false; bool modified = false;
auto* achievement = auto* achievement =
(X_XUSER_ACHIEVEMENT*)memory_->TranslateVirtual(achievements_ptr); (X_XUSER_ACHIEVEMENT*)memory_->TranslateVirtual(achievements_ptr);
xdbf::Achievement ach; xdbf::Achievement ach;
for (uint32_t i = 0; i < achievement_count; i++, achievement++) { for (uint32_t i = 0; i < achievement_count; i++, achievement++) {
auto user_profile = kernel_state_->user_profile(achievement->user_idx);
if (!user_profile) {
continue;
}
auto game_gpd = user_profile->GetTitleGpd();
if (!game_gpd) {
continue;
}
if (game_gpd->GetAchievement(achievement->achievement_id, &ach)) { if (game_gpd->GetAchievement(achievement->achievement_id, &ach)) {
if (!ach.IsUnlocked()) { if (!ach.IsUnlocked()) {
XELOGI("Achievement Unlocked! %ws (%d gamerscore) - %ws", XELOGI("Achievement Unlocked! %ws (%d gamerscore) - %ws",
@ -81,9 +85,10 @@ X_RESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
modified = true; modified = true;
} }
} }
}
if (modified) { if (modified) {
kernel_state_->user_profile()->UpdateTitleGpd(); user_profile->UpdateTitleGpd();
}
} }
return X_ERROR_SUCCESS; return X_ERROR_SUCCESS;

View File

@ -24,12 +24,138 @@
DECLARE_int32(license_mask); DECLARE_int32(license_mask);
DEFINE_int64(profile_xuid, -1, "XUID of the profile to load in (E0...)", "XAM"); DEFINE_string(user_0_xuid, "", "XUID of the profile to use for user 0 (E0...",
"Profiles");
DEFINE_int32(user_0_state, 1, "User 0 signin state (0/1/2)", "Profiles");
DEFINE_string(user_1_xuid, "", "XUID of the profile to use for user 1 (E0...",
"Profiles");
DEFINE_int32(user_1_state, 0, "User 1 signin state (0/1/2)", "Profiles");
DEFINE_string(user_2_xuid, "", "XUID of the profile to use for user 2 (E0...",
"Profiles");
DEFINE_int32(user_2_state, 0, "User 2 signin state (0/1/2)", "Profiles");
DEFINE_string(user_3_xuid, "", "XUID of the profile to use for user 3 (E0...",
"Profiles");
DEFINE_int32(user_3_state, 0, "User 3 signin state (0/1/2)", "Profiles");
namespace xe { namespace xe {
namespace kernel { namespace kernel {
namespace xam { namespace xam {
void UserProfile::CreateUsers(KernelState* kernel_state,
std::unique_ptr<UserProfile>* profiles) {
std::string xuids[] = {cvars::user_0_xuid, cvars::user_1_xuid,
cvars::user_2_xuid, cvars::user_3_xuid};
int32_t states[] = {cvars::user_0_state, cvars::user_1_state,
cvars::user_2_state, cvars::user_3_state};
// Create UserProfile instances for all 4 user slots
// But only login the slots the player has already configured
for (int i = 0; i < kMaxNumUsers; i++) {
auto xuid_str = xuids[i];
int32_t state = states[i];
uint64_t xuid = 0;
if (!xuid_str.empty()) {
xuid = std::strtoull(xuid_str.c_str(), 0, 16);
}
auto profile = std::make_unique<UserProfile>(kernel_state);
profile->signin_state(state);
if (state > 0) {
profile->Login(xuid);
}
profiles[i] = std::move(profile);
}
}
uint64_t UserProfile::XuidFromPath(const std::wstring& path) {
filesystem::FileInfo info;
if (!filesystem::GetInfo(path, &info)) {
return -1;
}
auto fname = info.name;
size_t len = fname.length();
// Get substring of name that has valid hex chars
// TODO: atm this just stops at . if the filename has it..
// it should probably check each char is valid hex instead
size_t i = 0;
for (auto c : fname) {
if (c == '.') {
len = i;
break;
}
i++;
}
auto xuid_str = xe::to_string(fname.substr(0, len));
return std::strtoull(xuid_str.c_str(), 0, 16);
}
std::wstring UserProfile::base_path(KernelState* state) {
return xe::to_absolute_path(
state->content_manager()->ResolvePackageRoot(0x10000, 0xFFFE07D1));
}
std::map<uint64_t, std::tuple<std::wstring, X_XAMACCOUNTINFO>>
UserProfile::Enumerate(KernelState* state, bool exclude_signed_in) {
std::map<uint64_t, std::tuple<std::wstring, X_XAMACCOUNTINFO>> map;
auto files = filesystem::ListFiles(base_path(state));
for (auto f : files) {
auto profile_path = f.path + f.name;
// use .dir for this profile if it exists
auto dir_path = profile_path + L".dir\\";
if (filesystem::PathExists(dir_path)) {
profile_path = dir_path;
}
auto xuid = XuidFromPath(profile_path);
if (!xuid) {
continue; // could cause infinite loop, so skip any invalid ones
}
if (map.count(xuid)) {
continue; // already added this xuid!
}
bool in_use = false;
if (exclude_signed_in) {
// check if this xuid is signed in already
for (uint32_t i = 0; i < state->num_profiles(); i++) {
auto profile = state->user_profile(i);
if (!profile) {
continue;
}
if (profile->xuid() == xuid || profile->xuid_offline() == xuid) {
in_use = true;
break;
}
}
}
if (!in_use) {
// Login with UserProfile so we can retrieve info about it
auto profile = std::make_unique<UserProfile>(state);
if (!profile->Login(xuid)) {
continue;
}
map[xuid] = std::tuple<std::wstring, X_XAMACCOUNTINFO>(profile_path,
profile->account_);
}
}
return map;
}
std::string X_XAMACCOUNTINFO::GetGamertagString() const { std::string X_XAMACCOUNTINFO::GetGamertagString() const {
return xe::to_string(std::wstring(gamertag)); return xe::to_string(std::wstring(gamertag));
} }
@ -120,37 +246,27 @@ void UserProfile::EncryptAccountFile(const X_XAMACCOUNTINFO* input,
UserProfile::UserProfile(KernelState* kernel_state) UserProfile::UserProfile(KernelState* kernel_state)
: kernel_state_(kernel_state), dash_gpd_(kDashboardID) { : kernel_state_(kernel_state), dash_gpd_(kDashboardID) {
base_path_ = base_path_ = base_path(kernel_state); // store path for later
xe::to_absolute_path(kernel_state_->content_manager()->ResolvePackageRoot(
0x10000, 0xFFFE07D1));
account_.xuid_online = cvars::profile_xuid;
wcscpy(account_.gamertag, L"XeniaUser");
// Try loading profile GPD files...
LoadProfile();
} }
std::wstring UserProfile::MountProfile(const std::wstring& path) { std::wstring UserProfile::ExtractProfile(const std::wstring& path) {
auto package = path; auto package = path;
auto package_dir = package + L".dir\\"; auto package_dir = package + L".dir\\";
if (filesystem::PathExists(package_dir)) { if (filesystem::PathExists(package_dir)) {
return package_dir; return package_dir;
} }
// Get info about the path
filesystem::FileInfo info; filesystem::FileInfo info;
// Get info for the original path too, in case we changed to the .dir above
if (!filesystem::GetInfo(package, &info)) { if (!filesystem::GetInfo(package, &info)) {
return L""; // Path doesn't exist - return the .dir version to create later
return package_dir;
} }
auto file_system = kernel_state_->file_system();
auto file_name = xe::to_string(info.name); // If path points to a file, open it as STFS and extract
auto mount_path = "\\Device\\Profile_" + file_name;
// If package still points to a file, open it as STFS and extract
if (info.type == filesystem::FileInfo::Type::kFile) { if (info.type == filesystem::FileInfo::Type::kFile) {
XELOGI("MountProfile: extracting STFS profile %S", package.c_str()); XELOGI("MountProfile: extracting STFS profile %S", package.c_str());
auto mount_path = "\\Device\\Profile_" + xe::to_string(info.name);
// Register the container in the virtual filesystem. // Register the container in the virtual filesystem.
auto device = auto device =
@ -166,63 +282,141 @@ std::wstring UserProfile::MountProfile(const std::wstring& path) {
return package_dir; return package_dir;
} }
// Must be an existing directory, just return the path for it
return package + L"\\"; return package + L"\\";
} }
bool UserProfile::LoadProfile() { bool UserProfile::Login(uint64_t offline_xuid) {
auto profile_path = path(cvars::profile_xuid); xuid_offline_ = offline_xuid;
if (cvars::profile_xuid == -1) { auto profile_path = path(xuid_offline_);
auto files = filesystem::ListFiles(path());
// TODO: allow choosing by index maybe? if (xuid_offline_ == 1) {
for (auto f : files) { // XUID = 1, login as the first non-signed-in profile
profile_path = f.path + f.name; profile_path.clear();
break; // use first found dir/package as profile
auto profiles = Enumerate(kernel_state_, true);
if (profiles.size() > 0) {
auto& profile = profiles.begin();
xuid_offline_ = profile->first;
profile_path = std::get<0>(profile->second);
} }
} }
profile_path_ = MountProfile(profile_path); if (!xuid_offline_ || profile_path.empty()) {
XELOGW(
"UserProfile::Login: Couldn't find available profile to login to, "
"using temp. profile");
memset(&account_, 0, sizeof(X_XAMACCOUNTINFO));
// Create some unique IDs for the profile
static int num_xuids = 0;
xuid_offline_ = 0xE0000000BEEFCAFE + num_xuids;
account_.xuid_online = 0x09000000BEEFCAFE + num_xuids;
swprintf_s(account_.gamertag, L"XeniaUser%d", num_xuids);
num_xuids++;
profile_path = path(xuid_offline_);
}
// Try extracting profile to a folder, so we can modify any contents
profile_path_ = ExtractProfile(profile_path);
if (profile_path_.empty()) { if (profile_path_.empty()) {
XELOGW("UserProfile::Login: Failed to extract profile from %S!",
profile_path.c_str());
return false; return false;
} }
auto mmap_ = if (!filesystem::PathExists(profile_path_)) {
MappedMemory::Open(path() + L"Account", MappedMemory::Mode::kRead); // Profile path doesn't exist - create new profile!
if (mmap_) { if (!filesystem::CreateFolder(profile_path_)) {
XELOGI("Loading Account file from path %SAccount", path().c_str()); XELOGE("UserProfile::Login: Failed to create profile for '%S' at %S!",
account_.gamertag, path().c_str());
X_XAMACCOUNTINFO tmp_acct;
bool success = DecryptAccountFile(mmap_->data(), &tmp_acct);
if (!success) {
success = DecryptAccountFile(mmap_->data(), &tmp_acct, true);
}
if (!success) {
XELOGW("Failed to decrypt Account file data");
} else { } else {
std::memcpy(&account_, &tmp_acct, sizeof(X_XAMACCOUNTINFO)); // Write out an account file
XELOGI("Loaded Account \"%s\" successfully!", name().c_str()); filesystem::CreateFile(
path() + L"Account"); // MappedMemory needs an existing file...
auto mmap_ = MappedMemory::Open(path() + L"Account",
MappedMemory::Mode::kReadWrite, 0,
sizeof(X_XAMACCOUNTINFO) + 0x18);
if (!mmap_) {
XELOGE("UserProfile::Login: Failed to create new Account at %S!",
path().c_str());
} else {
XELOGI("Writing Account file for '%S' to path %SAccount",
account_.gamertag, path().c_str());
EncryptAccountFile(&account_, mmap_->data(), false);
mmap_->Close();
}
// Dash GPD will be updated below, after default settings are added
}
} else {
// Profile exists, load Account and any GPDs
auto mmap_ =
MappedMemory::Open(path() + L"Account", MappedMemory::Mode::kRead);
if (mmap_) {
XELOGI("Loading Account file from path %SAccount", path().c_str());
X_XAMACCOUNTINFO tmp_acct;
bool success = DecryptAccountFile(mmap_->data(), &tmp_acct);
if (!success) {
success = DecryptAccountFile(mmap_->data(), &tmp_acct, true);
}
if (!success) {
XELOGW("Failed to decrypt Account file data");
} else {
std::memcpy(&account_, &tmp_acct, sizeof(X_XAMACCOUNTINFO));
XELOGI("Loaded Account '%s' successfully!", name().c_str());
}
mmap_->Close();
} }
mmap_->Close(); XELOGD("Loading profile GPDs from path %S", path().c_str());
}
XELOGI("Loading profile GPDs from path %S", path().c_str()); mmap_ =
MappedMemory::Open(path() + L"FFFE07D1.gpd", MappedMemory::Mode::kRead);
if (mmap_) {
dash_gpd_.Read(mmap_->data(), mmap_->size());
mmap_->Close();
} else {
XELOGW("Failed to read dash GPD (FFFE07D1.gpd), using blank one");
mmap_ = // Create empty settings syncdata, helps tools identify this XDBF as a GPD
MappedMemory::Open(path() + L"FFFE07D1.gpd", MappedMemory::Mode::kRead); xdbf::Entry ent;
if (mmap_) { ent.info.section = static_cast<uint16_t>(xdbf::GpdSection::kSetting);
dash_gpd_.Read(mmap_->data(), mmap_->size()); ent.info.id = 0x200000000;
mmap_->Close(); ent.data.resize(0x18);
} else { memset(ent.data.data(), 0, 0x18);
XELOGW("Failed to read dash GPD (FFFE07D1.gpd), using blank one"); dash_gpd_.UpdateEntry(ent);
}
// Create empty settings syncdata, helps tools identify this XDBF as a GPD // Load in any extra game GPDs
xdbf::Entry ent; std::vector<xdbf::TitlePlayed> titles;
ent.info.section = static_cast<uint16_t>(xdbf::GpdSection::kSetting); dash_gpd_.GetTitles(&titles);
ent.info.id = 0x200000000;
ent.data.resize(0x18); for (auto title : titles) {
memset(ent.data.data(), 0, 0x18); wchar_t fname[256];
dash_gpd_.UpdateEntry(ent); swprintf(fname, 256, L"%X.gpd", title.title_id);
mmap_ = MappedMemory::Open(path() + fname, MappedMemory::Mode::kRead);
if (!mmap_) {
XELOGE("Failed to open GPD for title %X (%s)!", title.title_id,
xe::to_string(title.title_name).c_str());
continue;
}
xdbf::GpdFile title_gpd(title.title_id);
bool result = title_gpd.Read(mmap_->data(), mmap_->size());
mmap_->Close();
if (!result) {
XELOGE("Failed to read GPD for title %X (%s)!", title.title_id,
xe::to_string(title.title_name).c_str());
continue;
}
title_gpds_[title.title_id] = title_gpd;
}
} }
// Add some default settings games/apps usually ask for // Add some default settings games/apps usually ask for
@ -291,37 +485,14 @@ bool UserProfile::LoadProfile() {
// Make sure the dash GPD is up-to-date // Make sure the dash GPD is up-to-date
UpdateGpd(kDashboardID, dash_gpd_); UpdateGpd(kDashboardID, dash_gpd_);
// Load in any extra game GPDs
std::vector<xdbf::TitlePlayed> titles;
dash_gpd_.GetTitles(&titles);
for (auto title : titles) {
wchar_t fname[256];
swprintf(fname, 256, L"%X.gpd", title.title_id);
mmap_ = MappedMemory::Open(path() + fname, MappedMemory::Mode::kRead);
if (!mmap_) {
XELOGE("Failed to open GPD for title %X (%s)!", title.title_id,
xe::to_string(title.title_name).c_str());
continue;
}
xdbf::GpdFile title_gpd(title.title_id);
bool result = title_gpd.Read(mmap_->data(), mmap_->size());
mmap_->Close();
if (!result) {
XELOGE("Failed to read GPD for title %X (%s)!", title.title_id,
xe::to_string(title.title_name).c_str());
continue;
}
title_gpds_[title.title_id] = title_gpd;
}
XELOGI("Loaded %d profile GPDs", title_gpds_.size()); XELOGI("Loaded %d profile GPDs", title_gpds_.size());
return true; return true;
} }
void UserProfile::Logout() {
*this = UserProfile(kernel_state_); // Reset ourselves
}
xdbf::GpdFile* UserProfile::SetTitleSpaData(const xdbf::SpaFile& spa_data) { xdbf::GpdFile* UserProfile::SetTitleSpaData(const xdbf::SpaFile& spa_data) {
curr_title_id_ = 0; curr_title_id_ = 0;
curr_gpd_ = nullptr; curr_gpd_ = nullptr;
@ -335,6 +506,8 @@ xdbf::GpdFile* UserProfile::SetTitleSpaData(const xdbf::SpaFile& spa_data) {
// TODO: let user choose locale? // TODO: let user choose locale?
spa_data.GetAchievements(spa_data.GetDefaultLocale(), &spa_achievements); spa_data.GetAchievements(spa_data.GetDefaultLocale(), &spa_achievements);
// Check if title should be included in the dash GPD title list
// These checks should hopefully match the same checks X360 uses
bool title_included = bool title_included =
title_data.title_type == xdbf::X_XDBF_XTHD_DATA::TitleType::kFull || title_data.title_type == xdbf::X_XDBF_XTHD_DATA::TitleType::kFull ||
title_data.title_type == xdbf::X_XDBF_XTHD_DATA::TitleType::kDownload; title_data.title_type == xdbf::X_XDBF_XTHD_DATA::TitleType::kDownload;

View File

@ -28,6 +28,7 @@ namespace xe {
namespace kernel { namespace kernel {
namespace xam { namespace xam {
constexpr uint32_t kMaxNumUsers = 4;
constexpr uint32_t kDashboardID = 0xFFFE07D1; constexpr uint32_t kDashboardID = 0xFFFE07D1;
// https://github.com/jogolden/testdev/blob/master/xkelib/xam/_xamext.h#L68 // https://github.com/jogolden/testdev/blob/master/xkelib/xam/_xamext.h#L68
@ -165,19 +166,33 @@ struct X_XAMACCOUNTINFO {
class UserProfile { class UserProfile {
public: public:
static void CreateUsers(KernelState* kernel_state,
std::unique_ptr<UserProfile>* profiles);
// Returns map of OfflineXuid -> Path & AccountInfo pairs
static std::map<uint64_t, std::tuple<std::wstring, X_XAMACCOUNTINFO>>
Enumerate(KernelState* state, bool exclude_signed_in = false);
static uint64_t XuidFromPath(const std::wstring& path);
static bool DecryptAccountFile(const uint8_t* data, X_XAMACCOUNTINFO* output, static bool DecryptAccountFile(const uint8_t* data, X_XAMACCOUNTINFO* output,
bool devkit = false); bool devkit = false);
static void EncryptAccountFile(const X_XAMACCOUNTINFO* input, uint8_t* output, static void EncryptAccountFile(const X_XAMACCOUNTINFO* input, uint8_t* output,
bool devkit = false); bool devkit = false);
static std::wstring base_path(KernelState* state);
UserProfile(KernelState* kernel_state); UserProfile(KernelState* kernel_state);
uint64_t xuid() const { return account_.xuid_online; } uint64_t xuid() const { return account_.xuid_online; }
uint64_t xuid_offline() const { return xuid_offline_; }
std::string name() const { return account_.GetGamertagString(); } std::string name() const { return account_.GetGamertagString(); }
std::wstring path() const; std::wstring path() const;
std::wstring path(uint64_t xuid) const; std::wstring path(uint64_t xuid) const;
// uint32_t signin_state() const { return 1; } uint32_t signin_state() const { return signin_state_; }
void signin_state(uint32_t state) { signin_state_ = state; }
bool signed_in() { return signin_state_ != 0 && xuid_offline_ != 0; }
xdbf::GpdFile* SetTitleSpaData(const xdbf::SpaFile& spa_data); xdbf::GpdFile* SetTitleSpaData(const xdbf::SpaFile& spa_data);
xdbf::GpdFile* GetTitleGpd(uint32_t title_id = -1); xdbf::GpdFile* GetTitleGpd(uint32_t title_id = -1);
@ -188,9 +203,16 @@ class UserProfile {
bool UpdateTitleGpd(uint32_t title_id = -1); bool UpdateTitleGpd(uint32_t title_id = -1);
bool UpdateAllGpds(); bool UpdateAllGpds();
// Tries logging this user into a profile
// If XUID == 0, will use Xenia generated profile
// If XUID == 1, will try loading from any available profile
// If XUID is any other ID, will try loading from the profile it belongs to
bool Login(uint64_t offline_xuid = 0);
void Logout();
private: private:
bool LoadProfile(); // Extracts profile package if needed - returns path to extracted folder
std::wstring MountProfile(const std::wstring& path); std::wstring ExtractProfile(const std::wstring& path);
bool UpdateGpd(uint32_t title_id, xdbf::GpdFile& gpd_data); bool UpdateGpd(uint32_t title_id, xdbf::GpdFile& gpd_data);
@ -200,7 +222,10 @@ class UserProfile {
std::wstring profile_path_; std::wstring profile_path_;
std::wstring base_path_; std::wstring base_path_;
X_XAMACCOUNTINFO account_;
uint64_t xuid_offline_ = 0;
uint32_t signin_state_ = 0;
X_XAMACCOUNTINFO account_ = {0};
std::unordered_map<uint32_t, xdbf::GpdFile> title_gpds_; std::unordered_map<uint32_t, xdbf::GpdFile> title_gpds_;
xdbf::GpdFile dash_gpd_; xdbf::GpdFile dash_gpd_;

View File

@ -386,19 +386,23 @@ dword_result_t XamContentGetCreator(dword_t user_index,
auto content_data = XCONTENT_DATA((uint8_t*)content_data_ptr); auto content_data = XCONTENT_DATA((uint8_t*)content_data_ptr);
if (content_data.content_type == 1) { auto user_profile = kernel_state()->user_profile(user_index);
// User always creates saves. if (!user_profile) {
*is_creator_ptr = 1; result = X_ERROR_NOT_LOGGED_ON; // TODO: find right error code
if (creator_xuid_ptr) {
*creator_xuid_ptr = kernel_state()->user_profile()->xuid();
}
} else { } else {
*is_creator_ptr = 0; if (content_data.content_type == 1) {
if (creator_xuid_ptr) { // User always creates saves.
*creator_xuid_ptr = 0; *is_creator_ptr = 1;
if (creator_xuid_ptr) {
*creator_xuid_ptr = user_profile->xuid();
}
} else {
*is_creator_ptr = 0;
if (creator_xuid_ptr) {
*creator_xuid_ptr = 0;
}
} }
} }
if (overlapped_ptr) { if (overlapped_ptr) {
kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, result); kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, result);
return X_ERROR_IO_PENDING; return X_ERROR_IO_PENDING;

View File

@ -19,8 +19,6 @@
#include "xenia/kernel/xthread.h" #include "xenia/kernel/xthread.h"
#include "xenia/xbox.h" #include "xenia/xbox.h"
DEFINE_bool(signin_state, true, "User signed in", "Kernel");
namespace xe { namespace xe {
namespace kernel { namespace kernel {
namespace xam { namespace xam {
@ -31,7 +29,7 @@ struct X_PROFILEENUMRESULT {
X_XAMACCOUNTINFO account; X_XAMACCOUNTINFO account;
xe::be<uint32_t> device_id; xe::be<uint32_t> device_id;
}; };
// static_assert_size(X_PROFILEENUMRESULT, 0x188); static_assert_size(X_PROFILEENUMRESULT, 0x188);
dword_result_t XamProfileCreateEnumerator(dword_t device_id, dword_result_t XamProfileCreateEnumerator(dword_t device_id,
lpdword_t handle_out) { lpdword_t handle_out) {
@ -42,18 +40,20 @@ dword_result_t XamProfileCreateEnumerator(dword_t device_id,
e->Initialize(); e->Initialize();
const auto& user_profile = kernel_state()->user_profile(); auto profiles = UserProfile::Enumerate(kernel_state(), false);
for (auto profile : profiles) {
auto result = (X_PROFILEENUMRESULT*)e->AppendItem();
memset(result, 0, sizeof(X_PROFILEENUMRESULT));
result->xuid_offline = profile.first;
auto& acc = std::get<1>(profile.second);
memcpy(&result->account, &acc, sizeof(X_XAMACCOUNTINFO));
X_PROFILEENUMRESULT* profile = (X_PROFILEENUMRESULT*)e->AppendItem(); // tag was swapped when account got loaded in, swap it back
memset(profile, 0, sizeof(X_PROFILEENUMRESULT)); xe::copy_and_swap<wchar_t>(result->account.gamertag,
profile->xuid_offline = user_profile->xuid(); result->account.gamertag, 0x10);
profile->device_id = 0x00000001;
auto tag = xe::to_wstring(user_profile->name()); result->device_id = 0x00000001;
XELOGI("XamProfileCreateEnumerator profile:(%S)", tag.c_str()); }
xe::copy_and_swap<wchar_t>(profile->account.gamertag, tag.c_str(),
tag.length());
profile->account.xuid_online = user_profile->xuid();
*handle_out = e->handle(); *handle_out = e->handle();
return X_ERROR_SUCCESS; return X_ERROR_SUCCESS;
@ -105,10 +105,10 @@ DECLARE_XAM_EXPORT1(XamProfileEnumerate, kUserProfiles, kImplemented);
X_HRESULT_result_t XamUserGetXUID(dword_t user_index, dword_t unk, X_HRESULT_result_t XamUserGetXUID(dword_t user_index, dword_t unk,
lpqword_t xuid_ptr) { lpqword_t xuid_ptr) {
if (user_index && user_index != 0xFF) { const auto& user_profile = kernel_state()->user_profile(user_index);
if (!user_profile) {
return X_E_NO_SUCH_USER; return X_E_NO_SUCH_USER;
} }
const auto& user_profile = kernel_state()->user_profile();
if (xuid_ptr) { if (xuid_ptr) {
*xuid_ptr = user_profile->xuid(); *xuid_ptr = user_profile->xuid();
} }
@ -124,12 +124,8 @@ dword_result_t XamUserGetSigninState(dword_t user_index) {
// This should keep games from asking us to sign in and also keep them // This should keep games from asking us to sign in and also keep them
// from initializing the network. // from initializing the network.
if (user_index == 0 || (user_index & 0xFF) == 0xFF) { auto user_profile = kernel_state()->user_profile(user_index);
const auto& user_profile = kernel_state()->user_profile(); return user_profile ? user_profile->signin_state() : 0;
return ((cvars::signin_state) ? 1 : 0);
} else {
return 0;
}
} }
DECLARE_XAM_EXPORT2(XamUserGetSigninState, kUserProfiles, kImplemented, DECLARE_XAM_EXPORT2(XamUserGetSigninState, kUserProfiles, kImplemented,
kHighFrequency); kHighFrequency);
@ -150,15 +146,15 @@ X_HRESULT_result_t XamUserGetSigninInfo(dword_t user_index, dword_t flags,
return X_E_INVALIDARG; return X_E_INVALIDARG;
} }
if (user_index && user_index != 0xFF) { std::memset(info, 0, sizeof(X_USER_SIGNIN_INFO));
const auto& user_profile = kernel_state()->user_profile(user_index);
if (!user_profile) {
return X_E_NO_SUCH_USER; return X_E_NO_SUCH_USER;
} }
std::memset(info, 0, sizeof(X_USER_SIGNIN_INFO));
const auto& user_profile = kernel_state()->user_profile();
info->xuid = user_profile->xuid(); info->xuid = user_profile->xuid();
info->signin_state = ((cvars::signin_state) ? 1 : 0); info->signin_state = user_profile->signin_state();
std::strncpy(info->name, user_profile->name().data(), 15); std::strncpy(info->name, user_profile->name().data(), 15);
return X_E_SUCCESS; return X_E_SUCCESS;
} }
@ -166,18 +162,15 @@ DECLARE_XAM_EXPORT1(XamUserGetSigninInfo, kUserProfiles, kImplemented);
dword_result_t XamUserGetName(dword_t user_index, lpstring_t buffer, dword_result_t XamUserGetName(dword_t user_index, lpstring_t buffer,
dword_t buffer_len) { dword_t buffer_len) {
XELOGI("XamUserGetName user_index:(%d) buffer_length:(%d)", user_index, buffer_len);
if (user_index && user_index != 0xFF) {
return X_ERROR_NO_SUCH_USER;
}
if (!buffer_len) { if (!buffer_len) {
return X_ERROR_SUCCESS; return X_ERROR_SUCCESS;
} }
const auto& user_profile = kernel_state()->user_profile(); const auto& user_profile = kernel_state()->user_profile(user_index);
if (!user_profile) {
return X_ERROR_NO_SUCH_USER;
}
const auto& user_name = user_profile->name(); const auto& user_name = user_profile->name();
// Real XAM will only copy a maximum of 15 characters out. // Real XAM will only copy a maximum of 15 characters out.
@ -191,15 +184,15 @@ DECLARE_XAM_EXPORT1(XamUserGetName, kUserProfiles, kImplemented);
dword_result_t XamUserGetGamerTag(dword_t user_index, lpwstring_t buffer, dword_result_t XamUserGetGamerTag(dword_t user_index, lpwstring_t buffer,
dword_t buffer_len) { dword_t buffer_len) {
if (user_index && user_index != 0xFF) {
return X_ERROR_NO_SUCH_USER;
}
if (!buffer_len) { if (!buffer_len) {
return X_ERROR_SUCCESS; return X_ERROR_SUCCESS;
} }
const auto& user_profile = kernel_state()->user_profile(); const auto& user_profile = kernel_state()->user_profile(user_index);
if (!user_profile) {
return X_ERROR_NO_SUCH_USER;
}
const auto& user_name = xe::to_wstring(user_profile->name()); const auto& user_name = xe::to_wstring(user_profile->name());
size_t copy_length = std::min({size_t(buffer_len * 2), user_name.size(), size_t copy_length = std::min({size_t(buffer_len * 2), user_name.size(),
@ -249,8 +242,8 @@ dword_result_t XamUserReadProfileSettings(
// Title ID = 0 means us. // Title ID = 0 means us.
// 0xfffe07d1 = profile? // 0xfffe07d1 = profile?
if (actual_user_index) { const auto& user_profile = kernel_state()->user_profile(actual_user_index);
// Only support user 0. if (!user_profile) {
if (overlapped_ptr) { if (overlapped_ptr) {
kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, kernel_state()->CompleteOverlappedImmediate(overlapped_ptr,
X_ERROR_NOT_FOUND); X_ERROR_NOT_FOUND);
@ -259,8 +252,6 @@ dword_result_t XamUserReadProfileSettings(
return X_ERROR_NOT_FOUND; return X_ERROR_NOT_FOUND;
} }
const auto& user_profile = kernel_state()->user_profile();
// First call asks for size (fill buffer_size_ptr). // First call asks for size (fill buffer_size_ptr).
// Second call asks for buffer contents with that size. // Second call asks for buffer contents with that size.
@ -387,13 +378,9 @@ dword_result_t XamUserWriteProfileSettings(
return X_ERROR_INVALID_PARAMETER; return X_ERROR_INVALID_PARAMETER;
} }
uint32_t actual_user_index = user_index; // Update and save settings.
if (actual_user_index == 255) { const auto& user_profile = kernel_state()->user_profile(user_index);
actual_user_index = 0; if (!user_profile) {
}
if (actual_user_index) {
// Only support user 0.
if (overlapped_ptr) { if (overlapped_ptr) {
kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, kernel_state()->CompleteOverlappedImmediate(overlapped_ptr,
X_ERROR_NOT_FOUND); X_ERROR_NOT_FOUND);
@ -402,9 +389,6 @@ dword_result_t XamUserWriteProfileSettings(
return X_ERROR_NOT_FOUND; return X_ERROR_NOT_FOUND;
} }
// Update and save settings.
const auto& user_profile = kernel_state()->user_profile();
auto gpd = user_profile->GetDashboardGpd(); auto gpd = user_profile->GetDashboardGpd();
if (title_id != 0xFFFE07D1) { if (title_id != 0xFFFE07D1) {
gpd = user_profile->GetTitleGpd(title_id); gpd = user_profile->GetTitleGpd(title_id);
@ -475,13 +459,8 @@ DECLARE_XAM_EXPORT1(XamUserWriteProfileSettings, kUserProfiles, kImplemented);
dword_result_t XamUserCheckPrivilege(dword_t user_index, dword_t mask, dword_result_t XamUserCheckPrivilege(dword_t user_index, dword_t mask,
lpdword_t out_value) { lpdword_t out_value) {
uint32_t actual_user_index = user_index; auto* user_profile = kernel_state()->user_profile(user_index);
if ((actual_user_index & 0xFF) == 0xFF) { if (!user_profile) {
// Always pin user to 0.
actual_user_index = 0;
}
if (actual_user_index) {
return X_ERROR_NO_SUCH_USER; return X_ERROR_NO_SUCH_USER;
} }
@ -493,13 +472,8 @@ DECLARE_XAM_EXPORT1(XamUserCheckPrivilege, kUserProfiles, kStub);
dword_result_t XamUserContentRestrictionGetFlags(dword_t user_index, dword_result_t XamUserContentRestrictionGetFlags(dword_t user_index,
lpdword_t out_flags) { lpdword_t out_flags) {
uint32_t actual_user_index = user_index; auto* user_profile = kernel_state()->user_profile(user_index);
if ((actual_user_index & 0xFF) == 0xFF) { if (!user_profile) {
// Always pin user to 0.
actual_user_index = 0;
}
if (actual_user_index) {
return X_ERROR_NO_SUCH_USER; return X_ERROR_NO_SUCH_USER;
} }
@ -513,13 +487,8 @@ dword_result_t XamUserContentRestrictionGetRating(dword_t user_index,
dword_t unk1, dword_t unk1,
lpdword_t out_unk2, lpdword_t out_unk2,
lpdword_t out_unk3) { lpdword_t out_unk3) {
uint32_t actual_user_index = user_index; auto* user_profile = kernel_state()->user_profile(user_index);
if ((actual_user_index & 0xFF) == 0xFF) { if (!user_profile) {
// Always pin user to 0.
actual_user_index = 0;
}
if (actual_user_index) {
return X_ERROR_NO_SUCH_USER; return X_ERROR_NO_SUCH_USER;
} }
@ -551,27 +520,20 @@ DECLARE_XAM_EXPORT1(XamUserContentRestrictionCheckAccess, kUserProfiles, kStub);
dword_result_t XamUserAreUsersFriends(dword_t user_index, dword_t unk1, dword_result_t XamUserAreUsersFriends(dword_t user_index, dword_t unk1,
dword_t unk2, lpdword_t out_value, dword_t unk2, lpdword_t out_value,
dword_t overlapped_ptr) { dword_t overlapped_ptr) {
uint32_t actual_user_index = user_index;
if ((actual_user_index & 0xFF) == 0xFF) {
// Always pin user to 0.
actual_user_index = 0;
}
*out_value = 0; *out_value = 0;
if (user_index) {
// Only support user 0.
return X_ERROR_NOT_LOGGED_ON;
}
X_RESULT result = X_ERROR_SUCCESS; X_RESULT result = X_ERROR_SUCCESS;
const auto& user_profile = kernel_state()->user_profile(); const auto& user_profile = kernel_state()->user_profile(user_index);
if (((cvars::signin_state) ? 1 : 0) == 0) { if (!user_profile) {
result = X_ERROR_NOT_LOGGED_ON; result = X_ERROR_NOT_LOGGED_ON;
} else { } else {
// No friends! if (!user_profile->signin_state()) {
*out_value = 0; result = X_ERROR_NOT_LOGGED_ON;
} else {
// No friends!
*out_value = 0;
}
} }
if (overlapped_ptr) { if (overlapped_ptr) {
@ -610,6 +572,11 @@ dword_result_t XamUserCreateAchievementEnumerator(dword_t title_id,
dword_t offset, dword_t count, dword_t offset, dword_t count,
lpdword_t buffer_size_ptr, lpdword_t buffer_size_ptr,
lpdword_t handle_ptr) { lpdword_t handle_ptr) {
auto user_profile = kernel_state()->user_profile(user_index);
if (!user_profile) {
return X_ERROR_INVALID_PARAMETER; // TODO: proper error code!
}
if (buffer_size_ptr) { if (buffer_size_ptr) {
*buffer_size_ptr = sizeof(X_XACHIEVEMENT_DETAILS) * count; *buffer_size_ptr = sizeof(X_XACHIEVEMENT_DETAILS) * count;
} }
@ -621,8 +588,7 @@ dword_result_t XamUserCreateAchievementEnumerator(dword_t title_id,
*handle_ptr = e->handle(); *handle_ptr = e->handle();
// Copy achievements into the enumerator if game GPD is loaded // Copy achievements into the enumerator if game GPD is loaded
auto* game_gpd = kernel_state()->user_profile()->GetTitleGpd( auto* game_gpd = user_profile->GetTitleGpd(title_id);
title_id ? title_id : kernel_state()->title_id());
if (!game_gpd) { if (!game_gpd) {
XELOGE( XELOGE(
"XamUserCreateAchievementEnumerator failed to find GPD for title %X!", "XamUserCreateAchievementEnumerator failed to find GPD for title %X!",
@ -723,12 +689,17 @@ dword_result_t XamUserCreateTitlesPlayedEnumerator(
// + 128 bytes for the 64-char titlename // + 128 bytes for the 64-char titlename
const uint32_t kEntrySize = sizeof(xdbf::X_XDBF_GPD_TITLEPLAYED) + 128; const uint32_t kEntrySize = sizeof(xdbf::X_XDBF_GPD_TITLEPLAYED) + 128;
auto user_profile = kernel_state()->user_profile(user_index);
if (!user_profile) {
return X_ERROR_INVALID_PARAMETER; // TODO: proper error code!
}
if (buffer_size_ptr) { if (buffer_size_ptr) {
*buffer_size_ptr = kEntrySize * games_count; *buffer_size_ptr = kEntrySize * games_count;
} }
std::vector<xdbf::TitlePlayed> titles; std::vector<xdbf::TitlePlayed> titles;
kernel_state()->user_profile()->GetDashboardGpd()->GetTitles(&titles); user_profile->GetDashboardGpd()->GetTitles(&titles);
// Sort titles by date played // Sort titles by date played
std::sort(titles.begin(), titles.end(), std::sort(titles.begin(), titles.end(),
@ -765,12 +736,12 @@ DECLARE_XAM_EXPORT1(XamUserCreateTitlesPlayedEnumerator, kUserProfiles,
kImplemented); kImplemented);
dword_result_t XamReadTile(dword_t tile_type, dword_t game_id, qword_t item_id, dword_result_t XamReadTile(dword_t tile_type, dword_t game_id, qword_t item_id,
dword_t offset, lpdword_t output_ptr, dword_t user_index, lpdword_t output_ptr,
lpdword_t buffer_size_ptr, dword_t overlapped_ptr) { lpdword_t buffer_size_ptr, dword_t overlapped_ptr) {
// Wrap function in a lambda func so we can use return to exit out when // Wrap function in a lambda func so we can use return to exit out when
// needed, but still always be able to set the xoverlapped value // needed, but still always be able to set the xoverlapped value
// this way we don't need a bunch of if/else nesting to accomplish the same // this way we don't need a bunch of if/else nesting to accomplish the same
auto main_fn = [tile_type, game_id, item_id, offset, output_ptr, auto main_fn = [tile_type, game_id, item_id, user_index, output_ptr,
buffer_size_ptr]() { buffer_size_ptr]() {
uint64_t image_id = item_id; uint64_t image_id = item_id;
@ -778,6 +749,11 @@ dword_result_t XamReadTile(dword_t tile_type, dword_t game_id, qword_t item_id,
size_t data_len = 0; size_t data_len = 0;
std::unique_ptr<MappedMemory> mmap; std::unique_ptr<MappedMemory> mmap;
auto user_profile = kernel_state()->user_profile(user_index);
if (!user_profile) {
return X_ERROR_INVALID_PARAMETER; // TODO: proper error code!
}
if (!output_ptr || !buffer_size_ptr) { if (!output_ptr || !buffer_size_ptr) {
return X_ERROR_FILE_NOT_FOUND; return X_ERROR_FILE_NOT_FOUND;
} }
@ -786,7 +762,7 @@ dword_result_t XamReadTile(dword_t tile_type, dword_t game_id, qword_t item_id,
if (kTileFileNames.count(type)) { if (kTileFileNames.count(type)) {
// image_id = XUID of profile to retrieve from // image_id = XUID of profile to retrieve from
auto file_path = kernel_state()->user_profile()->path(); auto file_path = user_profile->path();
file_path += kTileFileNames.at(type); file_path += kTileFileNames.at(type);
mmap = MappedMemory::Open(file_path, MappedMemory::Mode::kRead); mmap = MappedMemory::Open(file_path, MappedMemory::Mode::kRead);
@ -796,7 +772,7 @@ dword_result_t XamReadTile(dword_t tile_type, dword_t game_id, qword_t item_id,
data = mmap->data(); data = mmap->data();
data_len = mmap->size(); data_len = mmap->size();
} else { } else {
auto gpd = kernel_state()->user_profile()->GetTitleGpd(game_id.value()); auto gpd = user_profile->GetTitleGpd(game_id.value());
if (!gpd) { if (!gpd) {
return X_ERROR_FILE_NOT_FOUND; return X_ERROR_FILE_NOT_FOUND;
@ -856,16 +832,27 @@ DECLARE_XAM_EXPORT1(XamReadTileEx, kUserProfiles, kSketchy);
dword_result_t XamUserIsOnlineEnabled(dword_t user_index) { dword_result_t XamUserIsOnlineEnabled(dword_t user_index) {
// 0 - Offline // 0 - Offline
// 1 - Online // 1 - Online
return 1;
auto* user_profile = kernel_state()->user_profile(user_index);
if (!user_profile) {
return 0;
}
return user_profile->signin_state() == 2;
} }
DECLARE_XAM_EXPORT1(XamUserIsOnlineEnabled, kUserProfiles, kStub); DECLARE_XAM_EXPORT1(XamUserIsOnlineEnabled, kUserProfiles, kStub);
dword_result_t XamUserGetIndexFromXUID(qword_t xuid, dword_t r4, dword_result_t XamUserGetIndexFromXUID(qword_t xuid, dword_t r4,
lpdword_t user_index) { lpdword_t user_index) {
// TODO: support more than 1 user_index! for (uint32_t i = 0; i < kernel_state()->num_profiles(); i++) {
if (xuid == kernel_state()->user_profile()->xuid()) { auto profile = kernel_state()->user_profile(i);
*user_index = 0; if (!profile) {
return X_E_SUCCESS; continue;
}
if (profile->xuid() == xuid) {
*user_index = i;
return X_E_SUCCESS;
}
} }
return X_E_NO_SUCH_USER; return X_E_NO_SUCH_USER;