[XAM] Implementation of: Profile Manager & Profiles Support
Thanks Emoose for Account Encrypt/Decrypt code!
This commit is contained in:
parent
9ba0f18c82
commit
9bdd07590c
|
@ -36,7 +36,9 @@
|
|||
#include "xenia/gpu/d3d12/d3d12_command_processor.h"
|
||||
#include "xenia/gpu/graphics_system.h"
|
||||
#include "xenia/hid/input_system.h"
|
||||
#include "xenia/kernel/xam/profile_manager.h"
|
||||
#include "xenia/kernel/xam/xam_module.h"
|
||||
#include "xenia/kernel/xam/xam_state.h"
|
||||
#include "xenia/ui/file_picker.h"
|
||||
#include "xenia/ui/graphics_provider.h"
|
||||
#include "xenia/ui/imgui_dialog.h"
|
||||
|
@ -147,6 +149,14 @@ DEFINE_int32(recent_titles_entry_amount, 10,
|
|||
"recently played titles.",
|
||||
"General");
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
extern std::atomic<int> xam_dialogs_shown_;
|
||||
}
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
|
@ -250,6 +260,14 @@ void EmulatorWindow::ShutdownGraphicsSystemPresenterPainting() {
|
|||
}
|
||||
|
||||
void EmulatorWindow::OnEmulatorInitialized() {
|
||||
if (!emulator_->kernel_state()
|
||||
->xam_state()
|
||||
->profile_manager()
|
||||
->GetProfilesCount()) {
|
||||
new NoProfileDialog(imgui_drawer_.get(), this);
|
||||
disable_hotkeys_ = true;
|
||||
}
|
||||
|
||||
emulator_initialized_ = true;
|
||||
window_->SetMainMenuEnabled(true);
|
||||
// When the user can see that the emulator isn't initializing anymore (the
|
||||
|
@ -593,6 +611,15 @@ bool EmulatorWindow::Initialize() {
|
|||
}
|
||||
main_menu->AddChild(std::move(file_menu));
|
||||
|
||||
// Profile Menu
|
||||
auto profile_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Profile");
|
||||
{
|
||||
profile_menu->AddChild(MenuItem::Create(
|
||||
MenuItem::Type::kString, "&Show Profile Menu", "",
|
||||
std::bind(&EmulatorWindow::ToggleProfilesConfigDialog, this)));
|
||||
}
|
||||
main_menu->AddChild(std::move(profile_menu));
|
||||
|
||||
// CPU menu.
|
||||
auto cpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&CPU");
|
||||
{
|
||||
|
@ -1104,6 +1131,10 @@ void EmulatorWindow::InstallContent() {
|
|||
summary += "\n";
|
||||
}
|
||||
|
||||
if (content_installation_details.count(XContentType::kProfile)) {
|
||||
emulator_->kernel_state()->xam_state()->profile_manager()->ReloadProfiles();
|
||||
}
|
||||
|
||||
xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(),
|
||||
"Content Installation Summary", summary);
|
||||
}
|
||||
|
@ -1296,24 +1327,13 @@ void EmulatorWindow::CreateZarchive() {
|
|||
}
|
||||
|
||||
void EmulatorWindow::ShowContentDirectory() {
|
||||
std::filesystem::path target_path;
|
||||
|
||||
auto content_root = emulator_->content_root();
|
||||
if (!emulator_->is_title_open() || !emulator_->kernel_state()) {
|
||||
target_path = content_root;
|
||||
} else {
|
||||
// TODO(gibbed): expose this via ContentManager?
|
||||
auto title_id =
|
||||
fmt::format("{:08X}", emulator_->kernel_state()->title_id());
|
||||
auto package_root = content_root / title_id;
|
||||
target_path = package_root;
|
||||
|
||||
if (!std::filesystem::exists(content_root)) {
|
||||
std::filesystem::create_directories(content_root);
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(target_path)) {
|
||||
std::filesystem::create_directories(target_path);
|
||||
}
|
||||
|
||||
LaunchFileExplorer(target_path);
|
||||
LaunchFileExplorer(content_root);
|
||||
}
|
||||
|
||||
void EmulatorWindow::CpuTimeScalarReset() {
|
||||
|
@ -1382,6 +1402,23 @@ void EmulatorWindow::ToggleDisplayConfigDialog() {
|
|||
}
|
||||
}
|
||||
|
||||
void EmulatorWindow::ToggleProfilesConfigDialog() {
|
||||
if (!profile_config_dialog_) {
|
||||
disable_hotkeys_ = true;
|
||||
emulator_->kernel_state()->BroadcastNotification(kXNotificationIDSystemUI,
|
||||
1);
|
||||
profile_config_dialog_ =
|
||||
std::make_unique<ProfileConfigDialog>(imgui_drawer_.get(), this);
|
||||
kernel::xam::xam_dialogs_shown_++;
|
||||
} else {
|
||||
disable_hotkeys_ = false;
|
||||
emulator_->kernel_state()->BroadcastNotification(kXNotificationIDSystemUI,
|
||||
0);
|
||||
profile_config_dialog_.reset();
|
||||
kernel::xam::xam_dialogs_shown_--;
|
||||
}
|
||||
}
|
||||
|
||||
void EmulatorWindow::ToggleControllerVibration() {
|
||||
auto input_sys = emulator()->input_system();
|
||||
if (input_sys) {
|
||||
|
@ -1579,7 +1616,13 @@ EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey(
|
|||
// Default return value
|
||||
EmulatorWindow::ControllerHotKey Unknown_hotkey = {};
|
||||
|
||||
if (buttons == 0) return Unknown_hotkey;
|
||||
if (buttons == 0) {
|
||||
return Unknown_hotkey;
|
||||
}
|
||||
|
||||
if (disable_hotkeys_.load()) {
|
||||
return Unknown_hotkey;
|
||||
}
|
||||
|
||||
// Hotkey cool-down to prevent toggling too fast
|
||||
const std::chrono::milliseconds delay(75);
|
||||
|
@ -1790,7 +1833,8 @@ void EmulatorWindow::GamepadHotKeys() {
|
|||
while (true) {
|
||||
auto input_lock = input_sys->lock();
|
||||
|
||||
for (uint32_t user_index = 0; user_index < MAX_USERS; ++user_index) {
|
||||
for (uint32_t user_index = 0; user_index < XUserMaxUserCount;
|
||||
++user_index) {
|
||||
X_RESULT result = input_sys->GetState(user_index, &state);
|
||||
|
||||
// Release the lock before processing the hotkey
|
||||
|
@ -1962,6 +2006,17 @@ xe::X_STATUS EmulatorWindow::RunTitle(
|
|||
|
||||
auto result = emulator_->LaunchPath(abs_path);
|
||||
|
||||
disable_hotkeys_ = false;
|
||||
|
||||
if (profile_config_dialog_) {
|
||||
profile_config_dialog_.reset();
|
||||
kernel::xam::xam_dialogs_shown_--;
|
||||
}
|
||||
|
||||
if (display_config_dialog_) {
|
||||
display_config_dialog_.reset();
|
||||
}
|
||||
|
||||
imgui_drawer_.get()->ClearDialogs();
|
||||
|
||||
if (result) {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
#include "xenia/ui/windowed_app_context.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
#define MAX_USERS 4
|
||||
#include "xenia/app/profile_dialogs.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
@ -93,6 +93,12 @@ class EmulatorWindow {
|
|||
void SaveImage(const std::filesystem::path& path,
|
||||
const xe::ui::RawImage& image);
|
||||
|
||||
void ToggleProfilesConfigDialog();
|
||||
void SetHotkeysState(bool enabled) { disable_hotkeys_ = !enabled; }
|
||||
// We need to store it somewhere so there will be no situation when there are
|
||||
// multiple instances opened.
|
||||
std::unique_ptr<CreateProfileDialog> profile_creation_dialog_;
|
||||
|
||||
// Types of button functions for hotkeys.
|
||||
enum class ButtonFunctions {
|
||||
ToggleFullscreen,
|
||||
|
@ -257,12 +263,17 @@ class EmulatorWindow {
|
|||
std::unique_ptr<ui::ImmediateDrawer> immediate_drawer_;
|
||||
|
||||
bool emulator_initialized_ = false;
|
||||
std::atomic<bool> disable_hotkeys_ = false;
|
||||
|
||||
std::string base_title_;
|
||||
bool initializing_shader_storage_ = false;
|
||||
|
||||
std::unique_ptr<DisplayConfigDialog> display_config_dialog_;
|
||||
|
||||
// Storing pointers and toggling dialog state is useful for broadcasting
|
||||
// messages back to guest.
|
||||
std::unique_ptr<ProfileConfigDialog> profile_config_dialog_;
|
||||
|
||||
std::vector<RecentTitleEntry> recently_launched_titles_;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,333 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2024 Xenia Canary. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/app/profile_dialogs.h"
|
||||
#include <algorithm>
|
||||
#include "xenia/app/emulator_window.h"
|
||||
#include "xenia/base/system.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
void CreateProfileDialog::OnDraw(ImGuiIO& io) {
|
||||
auto profile_manager = emulator_window_->emulator()
|
||||
->kernel_state()
|
||||
->xam_state()
|
||||
->profile_manager();
|
||||
|
||||
const auto window_position =
|
||||
ImVec2(GetIO().DisplaySize.x * 0.40f, GetIO().DisplaySize.y * 0.44f);
|
||||
|
||||
ImGui::SetNextWindowPos(window_position, ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowBgAlpha(1.0f);
|
||||
|
||||
bool dialog_open = true;
|
||||
if (!ImGui::Begin("Create Profile", &dialog_open,
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_HorizontalScrollbar)) {
|
||||
ImGui::End();
|
||||
emulator_window_->profile_creation_dialog_.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
!ImGui::IsAnyItemActive() && !ImGui::IsMouseClicked(0)) {
|
||||
ImGui::SetKeyboardFocusHere(0);
|
||||
}
|
||||
|
||||
ImGui::TextUnformatted("Gamertag:");
|
||||
ImGui::InputText("##Gamertag", gamertag, sizeof(gamertag));
|
||||
|
||||
const std::string gamertag_string = std::string(gamertag);
|
||||
|
||||
if (profile_manager->IsGamertagValid(gamertag_string)) {
|
||||
if (ImGui::Button("Create")) {
|
||||
if (profile_manager->CreateProfile(gamertag_string, migration) &&
|
||||
migration) {
|
||||
emulator_window_->emulator()->DataMigration(0xB13EBABEBABEBABE);
|
||||
}
|
||||
std::fill(std::begin(gamertag), std::end(gamertag), '\0');
|
||||
dialog_open = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Cancel")) {
|
||||
std::fill(std::begin(gamertag), std::end(gamertag), '\0');
|
||||
dialog_open = false;
|
||||
}
|
||||
|
||||
if (!dialog_open) {
|
||||
ImGui::End();
|
||||
emulator_window_->profile_creation_dialog_.reset();
|
||||
return;
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void NoProfileDialog::OnDraw(ImGuiIO& io) {
|
||||
auto profile_manager = emulator_window_->emulator()
|
||||
->kernel_state()
|
||||
->xam_state()
|
||||
->profile_manager();
|
||||
|
||||
if (profile_manager->GetProfilesCount()) {
|
||||
delete this;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto window_position =
|
||||
ImVec2(GetIO().DisplaySize.x * 0.35f, GetIO().DisplaySize.y * 0.4f);
|
||||
|
||||
ImGui::SetNextWindowPos(window_position, ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowBgAlpha(1.0f);
|
||||
|
||||
bool dialog_open = true;
|
||||
if (!ImGui::Begin("No Profiles Found", &dialog_open,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_HorizontalScrollbar)) {
|
||||
ImGui::End();
|
||||
delete this;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string message =
|
||||
"There is no profile available! You will not be able to save without "
|
||||
"one.\n\nWould you like to create one?";
|
||||
|
||||
ImGui::TextUnformatted(message.c_str());
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::NewLine();
|
||||
|
||||
const auto content_files = xe::filesystem::ListDirectories(
|
||||
emulator_window_->emulator()->content_root());
|
||||
|
||||
if (content_files.empty()) {
|
||||
if (ImGui::Button("Create Profile") &&
|
||||
!emulator_window_->profile_creation_dialog_) {
|
||||
emulator_window_->profile_creation_dialog_ =
|
||||
std::make_unique<CreateProfileDialog>(
|
||||
emulator_window_->imgui_drawer(), emulator_window_);
|
||||
}
|
||||
} else {
|
||||
if (ImGui::Button("Create profile & migrate data") &&
|
||||
!emulator_window_->profile_creation_dialog_) {
|
||||
emulator_window_->profile_creation_dialog_ =
|
||||
std::make_unique<CreateProfileDialog>(
|
||||
emulator_window_->imgui_drawer(), emulator_window_, true);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Open profile menu")) {
|
||||
emulator_window_->ToggleProfilesConfigDialog();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Close") || !dialog_open) {
|
||||
emulator_window_->SetHotkeysState(true);
|
||||
ImGui::End();
|
||||
delete this;
|
||||
return;
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ProfileConfigDialog::OnDraw(ImGuiIO& io) {
|
||||
if (!emulator_window_->emulator() ||
|
||||
!emulator_window_->emulator()->kernel_state() ||
|
||||
!emulator_window_->emulator()->kernel_state()->xam_state()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto profile_manager = emulator_window_->emulator()
|
||||
->kernel_state()
|
||||
->xam_state()
|
||||
->profile_manager();
|
||||
if (!profile_manager) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto profiles = profile_manager->GetProfiles();
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(40, 40), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowBgAlpha(0.8f);
|
||||
|
||||
bool dialog_open = true;
|
||||
if (!ImGui::Begin("Profiles Menu", &dialog_open,
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_HorizontalScrollbar)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (profiles->empty()) {
|
||||
ImGui::TextUnformatted("No profiles found!");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
for (auto& [xuid, account] : *profiles) {
|
||||
ImGui::PushID(static_cast<int>(xuid));
|
||||
|
||||
const uint8_t user_index =
|
||||
profile_manager->GetUserIndexAssignedToProfile(xuid);
|
||||
|
||||
DrawProfileContent(xuid, user_index, &account);
|
||||
|
||||
ImGui::PopID();
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button("Create Profile") &&
|
||||
!emulator_window_->profile_creation_dialog_) {
|
||||
emulator_window_->profile_creation_dialog_ =
|
||||
std::make_unique<CreateProfileDialog>(emulator_window_->imgui_drawer(),
|
||||
emulator_window_);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
if (!dialog_open) {
|
||||
emulator_window_->ToggleProfilesConfigDialog();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool ProfileConfigDialog::DrawProfileContent(const uint64_t xuid,
|
||||
const uint8_t user_index,
|
||||
const X_XAMACCOUNTINFO* account) {
|
||||
auto profile_manager = emulator_window_->emulator()
|
||||
->kernel_state()
|
||||
->xam_state()
|
||||
->profile_manager();
|
||||
|
||||
const float default_image_size = 75.0f;
|
||||
auto position = ImGui::GetCursorPos();
|
||||
const float selectable_height =
|
||||
ImGui::GetTextLineHeight() *
|
||||
5; // 3 is for amount of lines of text behind image/object.
|
||||
const auto font = emulator_window_->imgui_drawer()->GetIO().Fonts->Fonts[0];
|
||||
|
||||
const auto text_size = font->CalcTextSizeA(
|
||||
font->FontSize, FLT_MAX, -1.0f,
|
||||
fmt::format("XUID: {:016X}\n", 0xB13EBABEBABEBABE).c_str());
|
||||
|
||||
const auto image_scale = selectable_height / default_image_size;
|
||||
const auto image_size = ImVec2(default_image_size * image_scale,
|
||||
default_image_size * image_scale);
|
||||
// This includes 10% to include empty spaces between border and elements.
|
||||
auto selectable_region_size =
|
||||
ImVec2((image_size.x + text_size.x) * 1.10f, selectable_height);
|
||||
|
||||
if (ImGui::Selectable("##Selectable", selected_xuid_ == xuid,
|
||||
ImGuiSelectableFlags_SpanAllColumns,
|
||||
selectable_region_size)) {
|
||||
selected_xuid_ = xuid;
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopupContextItem("Profile Menu")) {
|
||||
if (user_index == static_cast<uint8_t>(-1)) {
|
||||
if (ImGui::MenuItem("Login")) {
|
||||
profile_manager->Login(xuid);
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Login to slot:")) {
|
||||
for (uint8_t i = 0; i < XUserMaxUserCount; i++) {
|
||||
if (ImGui::MenuItem(fmt::format("slot {}", i).c_str())) {
|
||||
profile_manager->Login(xuid, i);
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
} else {
|
||||
if (ImGui::MenuItem("Logout")) {
|
||||
profile_manager->Logout(user_index);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::MenuItem("Modify (unsupported)");
|
||||
ImGui::MenuItem("Show Achievements (unsupported)");
|
||||
|
||||
if (ImGui::MenuItem("Show Content Directory")) {
|
||||
const auto path = profile_manager->GetProfileContentPath(
|
||||
xuid, emulator_window_->emulator()->kernel_state()->title_id());
|
||||
|
||||
if (!std::filesystem::exists(path)) {
|
||||
std::filesystem::create_directories(path);
|
||||
}
|
||||
|
||||
std::thread path_open(LaunchFileExplorer, path);
|
||||
path_open.detach();
|
||||
}
|
||||
|
||||
if (!emulator_window_->emulator()->is_title_open()) {
|
||||
ImGui::Separator();
|
||||
if (ImGui::BeginMenu("Delete Profile")) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(
|
||||
fmt::format("You're about to delete profile: {} (XUID: {:016X}). "
|
||||
"This will remove all data assigned to this profile "
|
||||
"including savefiles. Are you sure?",
|
||||
account->GetGamertagString(), xuid)
|
||||
.c_str());
|
||||
ImGui::EndTooltip();
|
||||
|
||||
if (ImGui::MenuItem("Yes, delete it!")) {
|
||||
profile_manager->DeleteProfile(xuid);
|
||||
ImGui::EndMenu();
|
||||
ImGui::EndPopup();
|
||||
return false;
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPos(position);
|
||||
|
||||
// In the future it can be replaced with profile icon.
|
||||
ImGui::Image(user_index < XUserMaxUserCount
|
||||
? imgui_drawer()->GetNotificationIcon(user_index)
|
||||
: nullptr,
|
||||
ImVec2(default_image_size * image_scale,
|
||||
default_image_size * image_scale));
|
||||
|
||||
ImGui::SameLine();
|
||||
position = ImGui::GetCursorPos();
|
||||
ImGui::TextUnformatted(
|
||||
fmt::format("User: {}\n", account->GetGamertagString()).c_str());
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPos(position);
|
||||
ImGui::SetCursorPosY(position.y + ImGui::GetTextLineHeight());
|
||||
ImGui::TextUnformatted(fmt::format("XUID: {:016X}\n", xuid).c_str());
|
||||
|
||||
if (user_index != static_cast<uint8_t>(-1)) {
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPos(position);
|
||||
ImGui::SetCursorPosY(position.y + 2 * ImGui::GetTextLineHeight());
|
||||
ImGui::TextUnformatted(
|
||||
fmt::format("Assigned to slot: {}\n", user_index).c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2024 Xenia Canary. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_APP_PROFILE_DIALOGS_H_
|
||||
#define XENIA_APP_PROFILE_DIALOGS_H_
|
||||
|
||||
#include "xenia/ui/imgui_dialog.h"
|
||||
#include "xenia/ui/imgui_drawer.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
class EmulatorWindow;
|
||||
|
||||
class CreateProfileDialog final : public ui::ImGuiDialog {
|
||||
public:
|
||||
CreateProfileDialog(ui::ImGuiDrawer* imgui_drawer,
|
||||
EmulatorWindow* emulator_window,
|
||||
bool with_migration = false)
|
||||
: ui::ImGuiDialog(imgui_drawer),
|
||||
emulator_window_(emulator_window),
|
||||
migration(with_migration) {
|
||||
memset(gamertag, 0, sizeof(gamertag));
|
||||
}
|
||||
|
||||
protected:
|
||||
void OnDraw(ImGuiIO& io) override;
|
||||
|
||||
bool migration = false;
|
||||
char gamertag[16] = "";
|
||||
EmulatorWindow* emulator_window_;
|
||||
};
|
||||
|
||||
class NoProfileDialog final : public ui::ImGuiDialog {
|
||||
public:
|
||||
NoProfileDialog(ui::ImGuiDrawer* imgui_drawer,
|
||||
EmulatorWindow* emulator_window)
|
||||
: ui::ImGuiDialog(imgui_drawer), emulator_window_(emulator_window) {}
|
||||
|
||||
protected:
|
||||
void OnDraw(ImGuiIO& io) override;
|
||||
|
||||
EmulatorWindow* emulator_window_;
|
||||
};
|
||||
|
||||
class ProfileConfigDialog final : public ui::ImGuiDialog {
|
||||
public:
|
||||
ProfileConfigDialog(ui::ImGuiDrawer* imgui_drawer,
|
||||
EmulatorWindow* emulator_window)
|
||||
: ui::ImGuiDialog(imgui_drawer), emulator_window_(emulator_window) {
|
||||
memset(gamertag, 0, sizeof(gamertag));
|
||||
}
|
||||
|
||||
protected:
|
||||
void OnDraw(ImGuiIO& io) override;
|
||||
|
||||
private:
|
||||
bool DrawProfileContent(const uint64_t xuid, const uint8_t user_index,
|
||||
const X_XAMACCOUNTINFO* account);
|
||||
|
||||
uint64_t selected_xuid_ = 0;
|
||||
char gamertag[16] = "";
|
||||
EmulatorWindow* emulator_window_;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -50,6 +50,7 @@
|
|||
#include "xenia/ui/file_picker.h"
|
||||
#include "xenia/ui/imgui_dialog.h"
|
||||
#include "xenia/ui/imgui_drawer.h"
|
||||
#include "xenia/ui/imgui_host_notification.h"
|
||||
#include "xenia/ui/window.h"
|
||||
#include "xenia/ui/windowed_app_context.h"
|
||||
#include "xenia/vfs/device.h"
|
||||
|
@ -594,6 +595,163 @@ X_STATUS Emulator::LaunchDefaultModule(const std::filesystem::path& path) {
|
|||
return result;
|
||||
}
|
||||
|
||||
X_STATUS Emulator::DataMigration(const uint64_t xuid) {
|
||||
uint32_t failure_count = 0;
|
||||
const std::string xuid_string = fmt::format("{:016X}", xuid);
|
||||
const std::string common_xuid_string = fmt::format("{:016X}", 0);
|
||||
const std::filesystem::path path_to_profile_data =
|
||||
content_root_ / xuid_string / "FFFE07D1" / "00010000" / xuid_string;
|
||||
// Filter directories inside. First we need to find any content type
|
||||
// directories.
|
||||
// Savefiles must go to user specific directory
|
||||
// Everything else goes to common
|
||||
const auto titles_to_move = xe::filesystem::FilterByName(
|
||||
xe::filesystem::ListDirectories(content_root_),
|
||||
std::regex("[A-F0-9]{8}"));
|
||||
|
||||
for (const auto& title : titles_to_move) {
|
||||
if (xe::path_to_utf8(title.name) == "FFFE07D1" ||
|
||||
xe::path_to_utf8(title.name) == "00000000") {
|
||||
// SKip any dashboard/profile related data that was previously installed
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto content_type_dirs = xe::filesystem::FilterByName(
|
||||
xe::filesystem::ListDirectories(title.path / title.name),
|
||||
std::regex("[A-F0-9]{8}"));
|
||||
|
||||
for (const auto& content_type : content_type_dirs) {
|
||||
const std::string used_xuid =
|
||||
xe::path_to_utf8(content_type.name) == "00000001"
|
||||
? xuid_string
|
||||
: common_xuid_string;
|
||||
|
||||
const auto previous_path = content_root_ / title.name / content_type.name;
|
||||
const auto path = content_root_ / used_xuid / title.name;
|
||||
|
||||
if (!std::filesystem::exists(path)) {
|
||||
std::filesystem::create_directories(path);
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(previous_path, path / content_type.name, ec);
|
||||
|
||||
if (ec) {
|
||||
failure_count++;
|
||||
XELOGW("{}: Moving from: {} to: {} failed! Error message: {} ({:08X})",
|
||||
__func__, xe::path_to_utf8(previous_path),
|
||||
xe::path_to_utf8(path / content_type.name), ec.message(),
|
||||
ec.value());
|
||||
}
|
||||
}
|
||||
// Other directories:
|
||||
// Headers - Just copy everything to both common and xuid locations
|
||||
// profile - ?
|
||||
if (std::filesystem::exists(title.path / title.name / "Headers")) {
|
||||
const auto xuid_path =
|
||||
content_root_ / xuid_string / title.name / "Headers";
|
||||
|
||||
std::filesystem::create_directories(xuid_path);
|
||||
|
||||
std::error_code ec;
|
||||
// Copy to specific user
|
||||
std::filesystem::copy(title.path / title.name / "Headers", xuid_path,
|
||||
std::filesystem::copy_options::recursive |
|
||||
std::filesystem::copy_options::skip_existing,
|
||||
ec);
|
||||
if (ec) {
|
||||
failure_count++;
|
||||
XELOGW("{}: Copying from: {} to: {} failed! Error message: {} ({:08X})",
|
||||
__func__, xe::path_to_utf8(title.path / title.name / "Headers"),
|
||||
xe::path_to_utf8(xuid_path), ec.message(), ec.value());
|
||||
}
|
||||
|
||||
const auto header_types =
|
||||
xe::filesystem::ListDirectories(title.path / title.name / "Headers");
|
||||
|
||||
if (!(header_types.size() == 1 &&
|
||||
header_types.at(0).name == "00000001")) {
|
||||
const auto common_path =
|
||||
content_root_ / common_xuid_string / title.name / "Headers";
|
||||
|
||||
std::filesystem::create_directories(common_path);
|
||||
|
||||
// Copy to common, skip cases where only savefile header is available
|
||||
std::filesystem::copy(title.path / title.name / "Headers", common_path,
|
||||
std::filesystem::copy_options::recursive |
|
||||
std::filesystem::copy_options::skip_existing,
|
||||
ec);
|
||||
if (ec) {
|
||||
failure_count++;
|
||||
XELOGW(
|
||||
"{}: Copying from: {} to: {} failed! Error message: {} ({:08X})",
|
||||
__func__, xe::path_to_utf8(title.path / title.name / "Headers"),
|
||||
xe::path_to_utf8(common_path), ec.message(), ec.value());
|
||||
}
|
||||
}
|
||||
|
||||
if (!ec) {
|
||||
// Remove previous directory
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(title.path / title.name / "Headers", ec);
|
||||
}
|
||||
}
|
||||
|
||||
if (std::filesystem::exists(title.path / title.name / "profile")) {
|
||||
// Find directory with previous username. There should be only one!
|
||||
const auto old_profile_data =
|
||||
xe::filesystem::ListDirectories(title.path / title.name / "profile");
|
||||
|
||||
xe::filesystem::FileInfo& entry_to_copy = xe::filesystem::FileInfo();
|
||||
if (old_profile_data.size() != 1) {
|
||||
for (const auto& entry : old_profile_data) {
|
||||
if (entry.name == "User") {
|
||||
entry_to_copy = entry;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entry_to_copy = old_profile_data.front();
|
||||
}
|
||||
|
||||
const auto path_from =
|
||||
title.path / title.name / "profile" / entry_to_copy.name;
|
||||
std::error_code ec;
|
||||
// Move files from inside to outside for convenience
|
||||
std::filesystem::rename(path_from, path_to_profile_data / title.name, ec);
|
||||
if (ec) {
|
||||
failure_count++;
|
||||
XELOGW("{}: Moving from: {} to: {} failed! Error message: {} ({:08X})",
|
||||
__func__, xe::path_to_utf8(path_from),
|
||||
xe::path_to_utf8(path_to_profile_data / title.name),
|
||||
ec.message(), ec.value());
|
||||
} else {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(title.path / title.name / "profile", ec);
|
||||
}
|
||||
}
|
||||
|
||||
const auto remaining_file_list =
|
||||
xe::filesystem::ListDirectories(title.path / title.name);
|
||||
|
||||
if (remaining_file_list.empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(title.path / title.name, ec);
|
||||
}
|
||||
}
|
||||
|
||||
std::string migration_status_message =
|
||||
fmt::format("Migration finished with {} {}.", failure_count,
|
||||
failure_count == 1 ? "error" : "errors");
|
||||
|
||||
if (failure_count) {
|
||||
migration_status_message.append(
|
||||
" For more information check xenia.log file.");
|
||||
}
|
||||
new xe::ui::HostNotificationWindow(imgui_drawer_, "Migration Status",
|
||||
migration_status_message, 0);
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
X_STATUS Emulator::InstallContentPackage(
|
||||
const std::filesystem::path& path,
|
||||
ContentInstallationInfo& installation_info) {
|
||||
|
@ -613,16 +771,18 @@ X_STATUS Emulator::InstallContentPackage(
|
|||
(vfs::XContentContainerDevice*)device.get();
|
||||
|
||||
std::filesystem::path installation_path =
|
||||
content_root() / fmt::format("{:08X}", dev->title_id()) /
|
||||
content_root() / fmt::format("{:016X}", dev->xuid()) /
|
||||
fmt::format("{:08X}", dev->title_id()) /
|
||||
fmt::format("{:08X}", dev->content_type()) / path.filename();
|
||||
|
||||
std::filesystem::path header_path =
|
||||
content_root() / fmt::format("{:08X}", dev->title_id()) / "Headers" /
|
||||
content_root() / fmt::format("{:016X}", dev->xuid()) /
|
||||
fmt::format("{:08X}", dev->title_id()) / "Headers" /
|
||||
fmt::format("{:08X}", dev->content_type()) / path.filename();
|
||||
|
||||
installation_info.installation_path =
|
||||
fmt::format("{:08X}/{:08X}/{}", dev->title_id(), dev->content_type(),
|
||||
xe::path_to_utf8(path.filename()));
|
||||
fmt::format("{:016X}/{:08X}/{:08X}/{}", dev->xuid(), dev->title_id(),
|
||||
dev->content_type(), xe::path_to_utf8(path.filename()));
|
||||
|
||||
installation_info.content_name =
|
||||
xe::to_utf8(dev->content_header().display_name());
|
||||
|
|
|
@ -231,6 +231,9 @@ class Emulator {
|
|||
std::string content_name;
|
||||
};
|
||||
|
||||
// Migrates data from content to content/xuid with respect to common data.
|
||||
X_STATUS DataMigration(const uint64_t xuid);
|
||||
|
||||
// Extract content of package to content specific directory.
|
||||
X_STATUS InstallContentPackage(const std::filesystem::path& path,
|
||||
ContentInstallationInfo& installation_info);
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include "xenia/base/profiling.h"
|
||||
#include "xenia/hid/hid_flags.h"
|
||||
#include "xenia/hid/input_driver.h"
|
||||
#include "xenia/kernel/util/shim_utils.h"
|
||||
|
||||
namespace xe {
|
||||
namespace hid {
|
||||
|
@ -39,7 +40,7 @@ void InputSystem::AddDriver(std::unique_ptr<InputDriver> driver) {
|
|||
|
||||
void InputSystem::UpdateUsedSlot(InputDriver* driver, uint8_t slot,
|
||||
bool connected) {
|
||||
if (slot == 0xFF) {
|
||||
if (slot == XUserIndexAny) {
|
||||
slot = 0;
|
||||
}
|
||||
|
||||
|
@ -50,6 +51,10 @@ void InputSystem::UpdateUsedSlot(InputDriver* driver, uint8_t slot,
|
|||
|
||||
XELOGI(controller_slot_state_change_message[connected].c_str(), slot);
|
||||
connected_slots.flip(slot);
|
||||
if (kernel::kernel_state()) {
|
||||
kernel::kernel_state()->BroadcastNotification(
|
||||
kXNotificationIDSystemInputDevicesChanged, 0);
|
||||
}
|
||||
|
||||
if (driver) {
|
||||
X_INPUT_CAPABILITIES capabilities = {};
|
||||
|
@ -147,15 +152,14 @@ void InputSystem::ToggleVibration() {
|
|||
// Send instant update to vibration state to prevent awaiting for next tick.
|
||||
X_INPUT_VIBRATION vibration = X_INPUT_VIBRATION();
|
||||
|
||||
for (uint8_t user_index = 0; user_index < max_allowed_controllers;
|
||||
user_index++) {
|
||||
for (uint8_t user_index = 0; user_index < XUserMaxUserCount; user_index++) {
|
||||
SetState(user_index, &vibration);
|
||||
}
|
||||
}
|
||||
|
||||
void InputSystem::AdjustDeadzoneLevels(const uint8_t slot,
|
||||
X_INPUT_GAMEPAD* gamepad) {
|
||||
if (slot > max_allowed_controllers) {
|
||||
if (slot > XUserMaxUserCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,6 @@ class Window;
|
|||
namespace xe {
|
||||
namespace hid {
|
||||
|
||||
static constexpr uint8_t max_allowed_controllers = 4;
|
||||
|
||||
class InputSystem {
|
||||
public:
|
||||
explicit InputSystem(xe::ui::Window* window);
|
||||
|
@ -51,7 +49,7 @@ class InputSystem {
|
|||
|
||||
void ToggleVibration();
|
||||
|
||||
const std::bitset<max_allowed_controllers> GetConnectedSlots() const {
|
||||
const std::bitset<XUserMaxUserCount> GetConnectedSlots() const {
|
||||
return connected_slots;
|
||||
}
|
||||
|
||||
|
@ -72,8 +70,8 @@ class InputSystem {
|
|||
|
||||
std::vector<std::unique_ptr<InputDriver>> drivers_;
|
||||
|
||||
std::bitset<max_allowed_controllers> connected_slots = {};
|
||||
std::array<std::pair<joystick_value, joystick_value>, max_allowed_controllers>
|
||||
std::bitset<XUserMaxUserCount> connected_slots = {};
|
||||
std::array<std::pair<joystick_value, joystick_value>, XUserMaxUserCount>
|
||||
controllers_max_joystick_value = {};
|
||||
|
||||
xe_unlikely_mutex lock_;
|
||||
|
|
|
@ -282,7 +282,7 @@ X_RESULT SDLInputDriver::GetKeystroke(uint32_t users, uint32_t flags,
|
|||
// TODO(JoelLinn): Figure out the flags
|
||||
// https://github.com/evilC/UCR/blob/0489929e2a8e39caa3484c67f3993d3fba39e46f/Libraries/XInput.ahk#L85-L98
|
||||
assert(sdl_events_initialized_ && sdl_gamecontroller_initialized_);
|
||||
bool user_any = users == 0xFF;
|
||||
bool user_any = users == XUserIndexAny;
|
||||
if (users >= HID_SDL_USER_COUNT && !user_any) {
|
||||
return X_ERROR_BAD_ARGUMENTS;
|
||||
}
|
||||
|
|
|
@ -208,7 +208,7 @@ X_RESULT XInputInputDriver::GetKeystroke(uint32_t user_index, uint32_t flags,
|
|||
// we are not passing back an uninitialized X_INPUT_KEYSTROKE structure.
|
||||
// If any user (0xFF) is polled this bug does not occur but GetCapabilities
|
||||
// would fail so we need to skip it.
|
||||
if (user_index != 0xFF) {
|
||||
if (user_index != XUserIndexAny) {
|
||||
XINPUT_CAPABILITIES caps;
|
||||
auto xigc = (decltype(&XInputGetCapabilities))XInputGetCapabilities_;
|
||||
result = xigc(user_index, 0, &caps);
|
||||
|
|
|
@ -106,6 +106,10 @@ KernelState::~KernelState() {
|
|||
KernelState* KernelState::shared() { return shared_kernel_state_; }
|
||||
|
||||
uint32_t KernelState::title_id() const {
|
||||
if (!executable_module_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
assert_not_null(executable_module_);
|
||||
|
||||
xex2_opt_execution_info* exec_info = 0;
|
||||
|
@ -577,7 +581,7 @@ std::vector<xam::XCONTENT_AGGREGATE_DATA> KernelState::FindTitleUpdate(
|
|||
}
|
||||
|
||||
return xam_state_->content_manager()->ListContent(
|
||||
1, xe::XContentType::kInstaller, title_id);
|
||||
1, 0, title_id, xe::XContentType::kInstaller);
|
||||
}
|
||||
|
||||
const object_ref<UserModule> KernelState::LoadTitleUpdate(
|
||||
|
@ -589,7 +593,7 @@ const object_ref<UserModule> KernelState::LoadTitleUpdate(
|
|||
}
|
||||
|
||||
X_RESULT open_status =
|
||||
content_manager()->OpenContent("UPDATE", *title_update, disc_number);
|
||||
content_manager()->OpenContent("UPDATE", 0, *title_update, disc_number);
|
||||
|
||||
// Use the corresponding patch for the launch module
|
||||
std::filesystem::path patch_xexp;
|
||||
|
|
|
@ -27,8 +27,6 @@ namespace kernel {
|
|||
namespace xam {
|
||||
|
||||
static const char* kThumbnailFileName = "__thumbnail.png";
|
||||
|
||||
static const char* kGameUserContentDirName = "profile";
|
||||
static const char* kGameContentHeaderDirName = "Headers";
|
||||
|
||||
static int content_device_id_ = 0;
|
||||
|
@ -62,24 +60,28 @@ ContentManager::ContentManager(KernelState* kernel_state,
|
|||
ContentManager::~ContentManager() = default;
|
||||
|
||||
std::filesystem::path ContentManager::ResolvePackageRoot(
|
||||
XContentType content_type, uint32_t title_id) {
|
||||
uint64_t xuid, uint32_t title_id, XContentType content_type) const {
|
||||
if (title_id == kCurrentlyRunningTitleId) {
|
||||
title_id = kernel_state_->title_id();
|
||||
}
|
||||
|
||||
auto xuid_str = fmt::format("{:016X}", xuid);
|
||||
auto title_id_str = fmt::format("{:08X}", title_id);
|
||||
auto content_type_str = fmt::format("{:08X}", uint32_t(content_type));
|
||||
auto content_type_str =
|
||||
fmt::format("{:08X}", static_cast<uint32_t>(content_type));
|
||||
|
||||
// Package root path:
|
||||
// content_root/title_id/content_type/
|
||||
return root_path_ / title_id_str / content_type_str;
|
||||
return root_path_ / xuid_str / title_id_str / content_type_str;
|
||||
}
|
||||
|
||||
std::filesystem::path ContentManager::ResolvePackagePath(
|
||||
const XCONTENT_AGGREGATE_DATA& data, const uint32_t disc_number) {
|
||||
const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data,
|
||||
const uint32_t disc_number) {
|
||||
// Content path:
|
||||
// content_root/title_id/content_type/data_file_name/
|
||||
auto get_package_path = [&, data, disc_number](const uint32_t title_id) {
|
||||
auto package_root = ResolvePackageRoot(data.content_type, title_id);
|
||||
auto package_root = ResolvePackageRoot(xuid, title_id, data.content_type);
|
||||
std::string final_name = xe::string_util::trim(data.file_name());
|
||||
std::filesystem::path package_path = package_root / xe::to_path(final_name);
|
||||
|
||||
|
@ -91,7 +93,7 @@ std::filesystem::path ContentManager::ResolvePackagePath(
|
|||
|
||||
if (data.content_type == XContentType::kPublisher) {
|
||||
const std::unordered_set<uint32_t> title_ids =
|
||||
FindPublisherTitleIds(data.title_id);
|
||||
FindPublisherTitleIds(xuid, data.title_id);
|
||||
|
||||
for (const auto& title_id : title_ids) {
|
||||
auto package_path = get_package_path(title_id);
|
||||
|
@ -108,24 +110,30 @@ std::filesystem::path ContentManager::ResolvePackagePath(
|
|||
}
|
||||
|
||||
std::filesystem::path ContentManager::ResolvePackageHeaderPath(
|
||||
const std::string_view file_name, XContentType content_type,
|
||||
uint32_t title_id) {
|
||||
const std::string_view file_name, uint64_t xuid, uint32_t title_id,
|
||||
const XContentType content_type) const {
|
||||
if (title_id == kCurrentlyRunningTitleId) {
|
||||
title_id = kernel_state_->title_id();
|
||||
}
|
||||
|
||||
if (content_type == XContentType::kMarketplaceContent) {
|
||||
xuid = 0;
|
||||
}
|
||||
|
||||
auto xuid_str = fmt::format("{:016X}", xuid);
|
||||
auto title_id_str = fmt::format("{:08X}", title_id);
|
||||
auto content_type_str = fmt::format("{:08X}", uint32_t(content_type));
|
||||
std::string final_name =
|
||||
xe::string_util::trim(std::string(file_name)) + ".header";
|
||||
|
||||
// Header root path:
|
||||
// content_root/title_id/Headers/content_type/
|
||||
return root_path_ / title_id_str / kGameContentHeaderDirName /
|
||||
// content_root/xuid/title_id/Headers/content_type/
|
||||
return root_path_ / xuid_str / title_id_str / kGameContentHeaderDirName /
|
||||
content_type_str / final_name;
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> ContentManager::FindPublisherTitleIds(
|
||||
uint32_t base_title_id) const {
|
||||
const uint64_t xuid, uint32_t base_title_id) const {
|
||||
if (base_title_id == kCurrentlyRunningTitleId) {
|
||||
base_title_id = kernel_state_->title_id();
|
||||
}
|
||||
|
@ -134,9 +142,10 @@ std::unordered_set<uint32_t> ContentManager::FindPublisherTitleIds(
|
|||
std::string publisher_id_regex =
|
||||
fmt::format("^{:04X}.*", static_cast<uint16_t>(base_title_id >> 16));
|
||||
// Get all publisher entries
|
||||
auto publisher_entries =
|
||||
xe::filesystem::FilterByName(xe::filesystem::ListDirectories(root_path_),
|
||||
std::regex(publisher_id_regex));
|
||||
auto publisher_entries = xe::filesystem::FilterByName(
|
||||
xe::filesystem::ListDirectories(root_path_ /
|
||||
fmt::format("{:016X}", xuid)),
|
||||
std::regex(publisher_id_regex));
|
||||
|
||||
for (const auto& entry : publisher_entries) {
|
||||
std::filesystem::path path_to_publisher_dir =
|
||||
|
@ -159,23 +168,20 @@ std::unordered_set<uint32_t> ContentManager::FindPublisherTitleIds(
|
|||
}
|
||||
|
||||
std::vector<XCONTENT_AGGREGATE_DATA> ContentManager::ListContent(
|
||||
uint32_t device_id, XContentType content_type, uint32_t title_id) {
|
||||
const uint32_t device_id, const uint64_t xuid, const uint32_t title_id,
|
||||
const XContentType content_type) const {
|
||||
std::vector<XCONTENT_AGGREGATE_DATA> result;
|
||||
|
||||
if (title_id == kCurrentlyRunningTitleId) {
|
||||
title_id = kernel_state_->title_id();
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> title_ids = {title_id};
|
||||
|
||||
if (content_type == XContentType::kPublisher) {
|
||||
title_ids = FindPublisherTitleIds(title_id);
|
||||
title_ids = FindPublisherTitleIds(xuid, title_id);
|
||||
}
|
||||
|
||||
for (const uint32_t& title_id : title_ids) {
|
||||
// Search path:
|
||||
// content_root/title_id/type_name/*
|
||||
auto package_root = ResolvePackageRoot(content_type, title_id);
|
||||
// content_root/xuid/title_id/type_name/*
|
||||
auto package_root = ResolvePackageRoot(xuid, title_id, content_type);
|
||||
auto file_infos = xe::filesystem::ListFiles(package_root);
|
||||
|
||||
for (const auto& file_info : file_infos) {
|
||||
|
@ -186,8 +192,8 @@ std::vector<XCONTENT_AGGREGATE_DATA> ContentManager::ListContent(
|
|||
|
||||
XCONTENT_AGGREGATE_DATA content_data;
|
||||
if (XSUCCEEDED(ReadContentHeaderFile(xe::path_to_utf8(file_info.name),
|
||||
content_type, content_data,
|
||||
title_id))) {
|
||||
xuid, title_id, content_type,
|
||||
content_data))) {
|
||||
result.emplace_back(std::move(content_data));
|
||||
} else {
|
||||
content_data.device_id = device_id;
|
||||
|
@ -195,6 +201,7 @@ std::vector<XCONTENT_AGGREGATE_DATA> ContentManager::ListContent(
|
|||
content_data.set_display_name(xe::path_to_utf16(file_info.name));
|
||||
content_data.set_file_name(xe::path_to_utf8(file_info.name));
|
||||
content_data.title_id = title_id;
|
||||
content_data.xuid = xuid;
|
||||
result.emplace_back(std::move(content_data));
|
||||
}
|
||||
}
|
||||
|
@ -203,9 +210,9 @@ std::vector<XCONTENT_AGGREGATE_DATA> ContentManager::ListContent(
|
|||
}
|
||||
|
||||
std::unique_ptr<ContentPackage> ContentManager::ResolvePackage(
|
||||
const std::string_view root_name, const XCONTENT_AGGREGATE_DATA& data,
|
||||
const uint32_t disc_number) {
|
||||
auto package_path = ResolvePackagePath(data, disc_number);
|
||||
const std::string_view root_name, const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data, const uint32_t disc_number) {
|
||||
auto package_path = ResolvePackagePath(xuid, data, disc_number);
|
||||
if (!std::filesystem::exists(package_path)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
@ -217,15 +224,16 @@ std::unique_ptr<ContentPackage> ContentManager::ResolvePackage(
|
|||
return package;
|
||||
}
|
||||
|
||||
bool ContentManager::ContentExists(const XCONTENT_AGGREGATE_DATA& data) {
|
||||
auto path = ResolvePackagePath(data);
|
||||
bool ContentManager::ContentExists(const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data) {
|
||||
auto path = ResolvePackagePath(xuid, data);
|
||||
return std::filesystem::exists(path);
|
||||
}
|
||||
|
||||
X_RESULT ContentManager::WriteContentHeaderFile(
|
||||
const XCONTENT_AGGREGATE_DATA* data) {
|
||||
const uint64_t xuid, const XCONTENT_AGGREGATE_DATA* data) {
|
||||
auto header_path = ResolvePackageHeaderPath(
|
||||
data->file_name(), data->content_type, data->title_id);
|
||||
data->file_name(), xuid, data->title_id, data->content_type);
|
||||
auto parent_path = header_path.parent_path();
|
||||
|
||||
if (!std::filesystem::exists(parent_path)) {
|
||||
|
@ -245,12 +253,12 @@ X_RESULT ContentManager::WriteContentHeaderFile(
|
|||
return X_STATUS_NO_SUCH_FILE;
|
||||
}
|
||||
|
||||
X_RESULT ContentManager::ReadContentHeaderFile(const std::string_view file_name,
|
||||
XContentType content_type,
|
||||
XCONTENT_AGGREGATE_DATA& data,
|
||||
const uint32_t title_id) {
|
||||
X_RESULT ContentManager::ReadContentHeaderFile(
|
||||
const std::string_view file_name, const uint64_t xuid,
|
||||
const uint32_t title_id, XContentType content_type,
|
||||
XCONTENT_AGGREGATE_DATA& data) const {
|
||||
auto header_file_path =
|
||||
ResolvePackageHeaderPath(file_name, content_type, title_id);
|
||||
ResolvePackageHeaderPath(file_name, xuid, title_id, content_type);
|
||||
constexpr uint32_t header_size = sizeof(XCONTENT_AGGREGATE_DATA);
|
||||
|
||||
if (std::filesystem::exists(header_file_path)) {
|
||||
|
@ -275,15 +283,15 @@ X_RESULT ContentManager::ReadContentHeaderFile(const std::string_view file_name,
|
|||
// usually requires title_id to be provided
|
||||
// Kinda simple workaround for that, but still assumption
|
||||
data.title_id = title_id;
|
||||
data.xuid = kernel_state_->xam_state()
|
||||
->GetUserProfile(static_cast<uint32_t>(0))
|
||||
->xuid();
|
||||
data.xuid = xuid;
|
||||
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
return X_STATUS_NO_SUCH_FILE;
|
||||
}
|
||||
|
||||
X_RESULT ContentManager::CreateContent(const std::string_view root_name,
|
||||
const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data) {
|
||||
auto global_lock = global_critical_region_.Acquire();
|
||||
|
||||
|
@ -292,7 +300,7 @@ X_RESULT ContentManager::CreateContent(const std::string_view root_name,
|
|||
return X_ERROR_ALREADY_EXISTS;
|
||||
}
|
||||
|
||||
auto package_path = ResolvePackagePath(data);
|
||||
auto package_path = ResolvePackagePath(xuid, data);
|
||||
if (std::filesystem::exists(package_path)) {
|
||||
// Exists, must not!
|
||||
return X_ERROR_ALREADY_EXISTS;
|
||||
|
@ -302,7 +310,7 @@ X_RESULT ContentManager::CreateContent(const std::string_view root_name,
|
|||
return X_ERROR_ACCESS_DENIED;
|
||||
}
|
||||
|
||||
auto package = ResolvePackage(root_name, data);
|
||||
auto package = ResolvePackage(root_name, xuid, data);
|
||||
assert_not_null(package);
|
||||
|
||||
open_packages_.insert({string_key::create(root_name), package.release()});
|
||||
|
@ -311,6 +319,7 @@ X_RESULT ContentManager::CreateContent(const std::string_view root_name,
|
|||
}
|
||||
|
||||
X_RESULT ContentManager::OpenContent(const std::string_view root_name,
|
||||
const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data,
|
||||
const uint32_t disc_number) {
|
||||
auto global_lock = global_critical_region_.Acquire();
|
||||
|
@ -320,14 +329,14 @@ X_RESULT ContentManager::OpenContent(const std::string_view root_name,
|
|||
return X_ERROR_ALREADY_EXISTS;
|
||||
}
|
||||
|
||||
auto package_path = ResolvePackagePath(data, disc_number);
|
||||
auto package_path = ResolvePackagePath(xuid, data, disc_number);
|
||||
if (!std::filesystem::exists(package_path)) {
|
||||
// Does not exist, must be created.
|
||||
return X_ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Open package.
|
||||
auto package = ResolvePackage(root_name, data, disc_number);
|
||||
auto package = ResolvePackage(root_name, xuid, data, disc_number);
|
||||
assert_not_null(package);
|
||||
|
||||
open_packages_.insert({string_key::create(root_name), package.release()});
|
||||
|
@ -352,9 +361,11 @@ X_RESULT ContentManager::CloseContent(const std::string_view root_name) {
|
|||
}
|
||||
|
||||
X_RESULT ContentManager::GetContentThumbnail(
|
||||
const XCONTENT_AGGREGATE_DATA& data, std::vector<uint8_t>* buffer) {
|
||||
const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data,
|
||||
std::vector<uint8_t>* buffer) {
|
||||
auto global_lock = global_critical_region_.Acquire();
|
||||
auto package_path = ResolvePackagePath(data);
|
||||
|
||||
auto package_path = ResolvePackagePath(xuid, data);
|
||||
auto thumb_path = package_path / kThumbnailFileName;
|
||||
if (std::filesystem::exists(thumb_path)) {
|
||||
auto file = xe::filesystem::OpenFile(thumb_path, "rb");
|
||||
|
@ -369,9 +380,10 @@ X_RESULT ContentManager::GetContentThumbnail(
|
|||
}
|
||||
|
||||
X_RESULT ContentManager::SetContentThumbnail(
|
||||
const XCONTENT_AGGREGATE_DATA& data, std::vector<uint8_t> buffer) {
|
||||
const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data,
|
||||
std::vector<uint8_t> buffer) {
|
||||
auto global_lock = global_critical_region_.Acquire();
|
||||
auto package_path = ResolvePackagePath(data);
|
||||
auto package_path = ResolvePackagePath(xuid, data);
|
||||
std::filesystem::create_directories(package_path);
|
||||
if (std::filesystem::exists(package_path)) {
|
||||
auto thumb_path = package_path / kThumbnailFileName;
|
||||
|
@ -384,7 +396,8 @@ X_RESULT ContentManager::SetContentThumbnail(
|
|||
}
|
||||
}
|
||||
|
||||
X_RESULT ContentManager::DeleteContent(const XCONTENT_AGGREGATE_DATA& data) {
|
||||
X_RESULT ContentManager::DeleteContent(const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data) {
|
||||
auto global_lock = global_critical_region_.Acquire();
|
||||
|
||||
if (IsContentOpen(data)) {
|
||||
|
@ -392,7 +405,7 @@ X_RESULT ContentManager::DeleteContent(const XCONTENT_AGGREGATE_DATA& data) {
|
|||
return X_ERROR_ACCESS_DENIED;
|
||||
}
|
||||
|
||||
auto package_path = ResolvePackagePath(data);
|
||||
auto package_path = ResolvePackagePath(xuid, data);
|
||||
if (std::filesystem::remove_all(package_path) > 0) {
|
||||
return X_ERROR_SUCCESS;
|
||||
} else {
|
||||
|
@ -400,15 +413,13 @@ X_RESULT ContentManager::DeleteContent(const XCONTENT_AGGREGATE_DATA& data) {
|
|||
}
|
||||
}
|
||||
|
||||
std::filesystem::path ContentManager::ResolveGameUserContentPath() {
|
||||
std::filesystem::path ContentManager::ResolveGameUserContentPath(
|
||||
const uint64_t xuid) {
|
||||
auto xuid_str = fmt::format("{:016X}", xuid);
|
||||
auto title_id = fmt::format("{:08X}", kernel_state_->title_id());
|
||||
auto user_name = xe::to_path(kernel_state_->xam_state()
|
||||
->GetUserProfile(static_cast<uint32_t>(0))
|
||||
->name());
|
||||
|
||||
// Per-game per-profile data location:
|
||||
// content_root/title_id/profile/user_name
|
||||
return root_path_ / title_id / kGameUserContentDirName / user_name;
|
||||
return root_path_ / xuid_str / kDashboardStringID / "00010000" / xuid_str /
|
||||
title_id;
|
||||
}
|
||||
|
||||
bool ContentManager::IsContentOpen(const XCONTENT_AGGREGATE_DATA& data) const {
|
||||
|
|
|
@ -142,45 +142,52 @@ class ContentManager {
|
|||
const std::filesystem::path& root_path);
|
||||
~ContentManager();
|
||||
|
||||
std::vector<XCONTENT_AGGREGATE_DATA> ListContent(uint32_t device_id,
|
||||
XContentType content_type,
|
||||
uint32_t title_id = -1);
|
||||
std::vector<XCONTENT_AGGREGATE_DATA> ListContent(
|
||||
const uint32_t device_id, const uint64_t xuid, const uint32_t title_id,
|
||||
const XContentType content_type) const;
|
||||
|
||||
std::unique_ptr<ContentPackage> ResolvePackage(
|
||||
const std::string_view root_name, const XCONTENT_AGGREGATE_DATA& data,
|
||||
const uint32_t disc_number = -1);
|
||||
const std::string_view root_name, const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data, const uint32_t disc_number = -1);
|
||||
|
||||
bool ContentExists(const XCONTENT_AGGREGATE_DATA& data);
|
||||
X_RESULT WriteContentHeaderFile(const XCONTENT_AGGREGATE_DATA* data_raw);
|
||||
bool ContentExists(const uint64_t xuid, const XCONTENT_AGGREGATE_DATA& data);
|
||||
X_RESULT WriteContentHeaderFile(const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA* data_raw);
|
||||
X_RESULT ReadContentHeaderFile(const std::string_view file_name,
|
||||
const uint64_t xuid, const uint32_t title_id,
|
||||
XContentType content_type,
|
||||
XCONTENT_AGGREGATE_DATA& data,
|
||||
const uint32_t title_id = -1);
|
||||
X_RESULT CreateContent(const std::string_view root_name,
|
||||
XCONTENT_AGGREGATE_DATA& data) const;
|
||||
X_RESULT CreateContent(const std::string_view root_name, const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data);
|
||||
X_RESULT OpenContent(const std::string_view root_name,
|
||||
X_RESULT OpenContent(const std::string_view root_name, const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data,
|
||||
const uint32_t disc_number = -1);
|
||||
X_RESULT CloseContent(const std::string_view root_name);
|
||||
X_RESULT GetContentThumbnail(const XCONTENT_AGGREGATE_DATA& data,
|
||||
X_RESULT GetContentThumbnail(const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data,
|
||||
std::vector<uint8_t>* buffer);
|
||||
X_RESULT SetContentThumbnail(const XCONTENT_AGGREGATE_DATA& data,
|
||||
X_RESULT SetContentThumbnail(const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data,
|
||||
std::vector<uint8_t> buffer);
|
||||
X_RESULT DeleteContent(const XCONTENT_AGGREGATE_DATA& data);
|
||||
std::filesystem::path ResolveGameUserContentPath();
|
||||
X_RESULT DeleteContent(const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data);
|
||||
std::filesystem::path ResolveGameUserContentPath(const uint64_t xuid);
|
||||
bool IsContentOpen(const XCONTENT_AGGREGATE_DATA& data) const;
|
||||
void CloseOpenedFilesFromContent(const std::string_view root_name);
|
||||
|
||||
private:
|
||||
std::filesystem::path ResolvePackageRoot(XContentType content_type,
|
||||
uint32_t title_id = -1);
|
||||
std::filesystem::path ResolvePackagePath(const XCONTENT_AGGREGATE_DATA& data,
|
||||
std::filesystem::path ResolvePackageRoot(
|
||||
const uint64_t xuid, const uint32_t title_id,
|
||||
const XContentType content_type) const;
|
||||
std::filesystem::path ResolvePackagePath(const uint64_t xuid,
|
||||
const XCONTENT_AGGREGATE_DATA& data,
|
||||
const uint32_t disc_number = -1);
|
||||
std::filesystem::path ResolvePackageHeaderPath(
|
||||
const std::string_view file_name, XContentType content_type,
|
||||
uint32_t title_id = -1);
|
||||
const std::string_view file_name, uint64_t xuid, uint32_t title_id,
|
||||
const XContentType content_type) const;
|
||||
|
||||
std::unordered_set<uint32_t> FindPublisherTitleIds(
|
||||
const uint64_t xuid,
|
||||
uint32_t base_title_id = kCurrentlyRunningTitleId) const;
|
||||
|
||||
KernelState* kernel_state_;
|
||||
|
|
|
@ -0,0 +1,534 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2024 Xenia Canary. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/kernel/xam/profile_manager.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/util/crypto_utils.h"
|
||||
#include "xenia/vfs/devices/host_path_device.h"
|
||||
|
||||
DEFINE_string(logged_profile_slot_0_xuid, "",
|
||||
"XUID of the profile to load on boot in slot 0", "Profiles");
|
||||
DEFINE_string(logged_profile_slot_1_xuid, "",
|
||||
"XUID of the profile to load on boot in slot 1", "Profiles");
|
||||
DEFINE_string(logged_profile_slot_2_xuid, "",
|
||||
"XUID of the profile to load on boot in slot 2", "Profiles");
|
||||
DEFINE_string(logged_profile_slot_3_xuid, "",
|
||||
"XUID of the profile to load on boot in slot 3", "Profiles");
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
|
||||
bool ProfileManager::DecryptAccountFile(const uint8_t* data,
|
||||
X_XAMACCOUNTINFO* output, bool devkit) {
|
||||
const uint8_t* key = util::GetXeKey(0x19, devkit);
|
||||
if (!key) {
|
||||
return false; // this shouldn't happen...
|
||||
}
|
||||
|
||||
// Generate RC4 key from data hash
|
||||
uint8_t rc4_key[0x14];
|
||||
util::HmacSha(key, 0x10, data, 0x10, 0, 0, 0, 0, rc4_key, 0x14);
|
||||
|
||||
uint8_t dec_data[sizeof(X_XAMACCOUNTINFO) + 8];
|
||||
|
||||
// Decrypt data
|
||||
util::RC4(rc4_key, 0x10, data + 0x10, sizeof(dec_data), dec_data,
|
||||
sizeof(dec_data));
|
||||
|
||||
// Verify decrypted data against hash
|
||||
uint8_t data_hash[0x14];
|
||||
util::HmacSha(key, 0x10, dec_data, sizeof(dec_data), 0, 0, 0, 0, data_hash,
|
||||
0x14);
|
||||
|
||||
if (std::memcmp(data, data_hash, 0x10) == 0) {
|
||||
// Copy account data to output
|
||||
std::memcpy(output, dec_data + 8, sizeof(X_XAMACCOUNTINFO));
|
||||
|
||||
// Swap gamertag endian
|
||||
xe::copy_and_swap<char16_t>(output->gamertag, output->gamertag, 0x10);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ProfileManager::EncryptAccountFile(const X_XAMACCOUNTINFO* input,
|
||||
uint8_t* output, bool devkit) {
|
||||
const uint8_t* key = util::GetXeKey(0x19, devkit);
|
||||
if (!key) {
|
||||
return; // this shouldn't happen...
|
||||
}
|
||||
|
||||
X_XAMACCOUNTINFO* output_acct =
|
||||
reinterpret_cast<X_XAMACCOUNTINFO*>(output + 0x18);
|
||||
std::memcpy(output_acct, input, sizeof(X_XAMACCOUNTINFO));
|
||||
|
||||
// Swap gamertag endian
|
||||
xe::copy_and_swap<char16_t>(output_acct->gamertag, output_acct->gamertag,
|
||||
0x10);
|
||||
|
||||
// Set confounder, should be random but meh
|
||||
std::memset(output + 0x10, 0xFD, 8);
|
||||
|
||||
// Encrypted data = xam account info + 8 byte confounder
|
||||
uint32_t enc_data_size = sizeof(X_XAMACCOUNTINFO) + 8;
|
||||
|
||||
// Set data hash
|
||||
uint8_t data_hash[0x14];
|
||||
util::HmacSha(key, 0x10, output + 0x10, enc_data_size, 0, 0, 0, 0, data_hash,
|
||||
0x14);
|
||||
|
||||
std::memcpy(output, data_hash, 0x10);
|
||||
|
||||
// Generate RC4 key from data hash
|
||||
uint8_t rc4_key[0x14];
|
||||
util::HmacSha(key, 0x10, data_hash, 0x10, 0, 0, 0, 0, rc4_key, 0x14);
|
||||
|
||||
// Encrypt data
|
||||
util::RC4(rc4_key, 0x10, output + 0x10, enc_data_size, output + 0x10,
|
||||
enc_data_size);
|
||||
}
|
||||
|
||||
ProfileManager::ProfileManager(KernelState* kernel_state)
|
||||
: kernel_state_(kernel_state) {
|
||||
logged_profiles_.clear();
|
||||
accounts_.clear();
|
||||
|
||||
LoadAccounts(FindProfiles());
|
||||
|
||||
if (!cvars::logged_profile_slot_0_xuid.empty()) {
|
||||
Login(xe::string_util::from_string<uint64_t>(
|
||||
cvars::logged_profile_slot_0_xuid, true),
|
||||
0);
|
||||
}
|
||||
|
||||
if (!cvars::logged_profile_slot_1_xuid.empty()) {
|
||||
Login(xe::string_util::from_string<uint64_t>(
|
||||
cvars::logged_profile_slot_1_xuid, true),
|
||||
1);
|
||||
}
|
||||
|
||||
if (!cvars::logged_profile_slot_2_xuid.empty()) {
|
||||
Login(xe::string_util::from_string<uint64_t>(
|
||||
cvars::logged_profile_slot_2_xuid, true),
|
||||
2);
|
||||
}
|
||||
|
||||
if (!cvars::logged_profile_slot_3_xuid.empty()) {
|
||||
Login(xe::string_util::from_string<uint64_t>(
|
||||
cvars::logged_profile_slot_3_xuid, true),
|
||||
3);
|
||||
}
|
||||
}
|
||||
|
||||
ProfileManager::~ProfileManager() {}
|
||||
|
||||
void ProfileManager::ReloadProfiles() { LoadAccounts(FindProfiles()); }
|
||||
|
||||
UserProfile* ProfileManager::GetProfile(const uint64_t xuid) const {
|
||||
const uint8_t user_index = GetUserIndexAssignedToProfile(xuid);
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return GetProfile(user_index);
|
||||
}
|
||||
|
||||
UserProfile* ProfileManager::GetProfile(uint8_t user_index) const {
|
||||
if (user_index == XUserIndexNone) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (user_index == XUserIndexAny) {
|
||||
for (uint8_t i = 0; i < XUserMaxUserCount; i++) {
|
||||
if (!logged_profiles_.count(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return logged_profiles_.at(i).get();
|
||||
}
|
||||
}
|
||||
|
||||
if (!logged_profiles_.count(user_index)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return logged_profiles_.at(user_index).get();
|
||||
}
|
||||
|
||||
bool ProfileManager::LoadAccount(const uint64_t xuid) {
|
||||
const std::string xuid_as_string = fmt::format("{:016X}", xuid);
|
||||
|
||||
XELOGI("{}: Loading Account: {}", __func__, xuid_as_string);
|
||||
|
||||
MountProfile(xuid);
|
||||
|
||||
const std::string guest_path = xuid_as_string + ":\\Account";
|
||||
|
||||
xe::vfs::File* output_file;
|
||||
xe::vfs::FileAction action = {};
|
||||
auto status = kernel_state_->file_system()->OpenFile(
|
||||
nullptr, guest_path, xe::vfs::FileDisposition::kOpen,
|
||||
xe::vfs::FileAccess::kFileReadData, false, true, &output_file, &action);
|
||||
|
||||
if (XFAILED(status) || !output_file || !output_file->entry()) {
|
||||
XELOGI("{}: Failed to open Account file: {:08X}", __func__, status);
|
||||
DismountProfile(xuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> file_data;
|
||||
file_data.resize(output_file->entry()->size());
|
||||
|
||||
size_t bytes_read = 0;
|
||||
output_file->ReadSync(file_data.data(), output_file->entry()->size(), 0,
|
||||
&bytes_read);
|
||||
output_file->Destroy();
|
||||
|
||||
if (bytes_read < sizeof(X_XAMACCOUNTINFO)) {
|
||||
DismountProfile(xuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
X_XAMACCOUNTINFO tmp_acct;
|
||||
if (!ProfileManager::DecryptAccountFile(file_data.data(), &tmp_acct)) {
|
||||
if (!ProfileManager::DecryptAccountFile(file_data.data(), &tmp_acct,
|
||||
true)) {
|
||||
XELOGW("Failed to decrypt account data file for XUID: {}",
|
||||
xuid_as_string);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't need profile to be mounted anymore, so for now let's close it.
|
||||
// We need it only when we want to login into this account!
|
||||
DismountProfile(xuid);
|
||||
|
||||
accounts_.insert({xuid, tmp_acct});
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProfileManager::LoadAccounts(const std::vector<uint64_t> profiles_xuids) {
|
||||
for (const auto& path : profiles_xuids) {
|
||||
LoadAccount(path);
|
||||
}
|
||||
}
|
||||
|
||||
bool ProfileManager::MountProfile(const uint64_t xuid) {
|
||||
std::filesystem::path profile_path = GetProfilePath(xuid);
|
||||
std::string mount_path = fmt::format("{:016X}", xuid) + ':';
|
||||
|
||||
auto device =
|
||||
std::make_unique<vfs::HostPathDevice>(mount_path, profile_path, false);
|
||||
if (!device->Initialize()) {
|
||||
XELOGE(
|
||||
"MountProfile: Unable to mount {} profile; file not found or "
|
||||
"corrupted.",
|
||||
xe::path_to_utf8(profile_path));
|
||||
return false;
|
||||
}
|
||||
return kernel_state_->file_system()->RegisterDevice(std::move(device));
|
||||
}
|
||||
|
||||
bool ProfileManager::DismountProfile(const uint64_t xuid) {
|
||||
return kernel_state_->file_system()->UnregisterDevice(
|
||||
fmt::format("{:016X}", xuid) + ':');
|
||||
}
|
||||
|
||||
void ProfileManager::Login(const uint64_t xuid, const uint8_t user_index) {
|
||||
if (logged_profiles_.size() >= 4 && user_index >= XUserMaxUserCount) {
|
||||
XELOGE(
|
||||
"Cannot login account with XUID: {:016X} due to lack of free slots "
|
||||
"(Max 4 accounts at once)",
|
||||
xuid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (user_index < XUserMaxUserCount) {
|
||||
const auto& profile = logged_profiles_.find(user_index);
|
||||
if (profile != logged_profiles_.cend()) {
|
||||
if (profile->second && profile->second->xuid() == xuid) {
|
||||
// Do nothing! User is already signed in to that slot.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find if xuid is already logged in. We might want to logout.
|
||||
for (auto& logged_profile : logged_profiles_) {
|
||||
if (logged_profile.second->xuid() == xuid) {
|
||||
Logout(logged_profile.first);
|
||||
}
|
||||
}
|
||||
|
||||
if (!accounts_.count(xuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& profile = accounts_[xuid];
|
||||
const uint8_t assigned_user_slot =
|
||||
user_index < XUserMaxUserCount ? user_index : FindFirstFreeProfileSlot();
|
||||
|
||||
XELOGI("Loaded {} (GUID: {:016X}) to slot {}", profile.GetGamertagString(),
|
||||
xuid, assigned_user_slot);
|
||||
|
||||
logged_profiles_[assigned_user_slot] =
|
||||
std::make_unique<UserProfile>(xuid, &profile);
|
||||
kernel_state_->BroadcastNotification(kXNotificationIDSystemSignInChanged,
|
||||
GetUsedUserSlots().to_ulong());
|
||||
UpdateConfig(xuid, assigned_user_slot);
|
||||
}
|
||||
|
||||
void ProfileManager::Logout(const uint8_t user_index) {
|
||||
auto profile = logged_profiles_.find(user_index);
|
||||
if (profile == logged_profiles_.cend()) {
|
||||
return;
|
||||
}
|
||||
DismountProfile(profile->second->xuid());
|
||||
logged_profiles_.erase(profile);
|
||||
kernel_state_->BroadcastNotification(kXNotificationIDSystemSignInChanged,
|
||||
GetUsedUserSlots().to_ulong());
|
||||
UpdateConfig(0, user_index);
|
||||
}
|
||||
|
||||
std::vector<uint64_t> ProfileManager::FindProfiles() const {
|
||||
// Info: Profile directory name is also it's offline xuid
|
||||
std::vector<uint64_t> profiles_xuids;
|
||||
|
||||
auto profiles_directory = xe::filesystem::FilterByName(
|
||||
xe::filesystem::ListDirectories(
|
||||
kernel_state_->emulator()->content_root()),
|
||||
std::regex("[0-9A-F]{16}"));
|
||||
|
||||
for (const auto& profile : profiles_directory) {
|
||||
const std::string profile_xuid = xe::path_to_utf8(profile.name);
|
||||
if (profile_xuid == fmt::format("{:016X}", 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(
|
||||
profile.path / profile.name / kDashboardStringID /
|
||||
fmt::format("{:08X}", XContentType::kProfile) / profile.name)) {
|
||||
XELOGE("Profile {} doesn't have profile package!", profile_xuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
XELOGE("{}: Adding profile {} to profile list", __func__, profile_xuid);
|
||||
profiles_xuids.push_back(
|
||||
xe::string_util::from_string<uint64_t>(profile_xuid, true));
|
||||
}
|
||||
|
||||
XELOGE("ProfileManager: Found {} Profiles", profiles_xuids.size());
|
||||
return profiles_xuids;
|
||||
}
|
||||
|
||||
uint8_t ProfileManager::FindFirstFreeProfileSlot() const {
|
||||
if (!IsAnyProfileSignedIn()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::bitset<XUserMaxUserCount> used_slots = {};
|
||||
for (const auto& [index, entry] : logged_profiles_) {
|
||||
used_slots.set(index);
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < used_slots.size(); ++i) {
|
||||
if (!used_slots[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::bitset<XUserMaxUserCount> ProfileManager::GetUsedUserSlots() const {
|
||||
std::bitset<XUserMaxUserCount> used_slots = {};
|
||||
for (const auto& [index, entry] : logged_profiles_) {
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
used_slots.set(index);
|
||||
}
|
||||
|
||||
return used_slots;
|
||||
}
|
||||
|
||||
uint8_t ProfileManager::GetUserIndexAssignedToProfile(
|
||||
const uint64_t xuid) const {
|
||||
for (const auto& [index, entry] : logged_profiles_) {
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry->xuid() != xuid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::filesystem::path ProfileManager::GetProfileContentPath(
|
||||
const uint64_t xuid, const uint32_t title_id) const {
|
||||
std::filesystem::path profile_content_path =
|
||||
kernel_state_->emulator()->content_root() / fmt::format("{:016X}", xuid);
|
||||
if (title_id != -1 && title_id != 0) {
|
||||
profile_content_path =
|
||||
profile_content_path / fmt::format("{:08X}", title_id);
|
||||
}
|
||||
return profile_content_path;
|
||||
}
|
||||
|
||||
std::filesystem::path ProfileManager::GetProfilePath(
|
||||
const uint64_t xuid) const {
|
||||
return GetProfilePath(fmt::format("{:016X}", xuid));
|
||||
}
|
||||
|
||||
std::filesystem::path ProfileManager::GetProfilePath(
|
||||
const std::string xuid) const {
|
||||
return kernel_state_->emulator()->content_root() / xuid / kDashboardStringID /
|
||||
fmt::format("{:08X}", XContentType::kProfile) / xuid;
|
||||
}
|
||||
|
||||
bool ProfileManager::CreateProfile(const std::string gamertag,
|
||||
bool default_xuid) {
|
||||
const auto xuid = !default_xuid ? GenerateXuid() : 0xB13EBABEBABEBABE;
|
||||
|
||||
if (!std::filesystem::create_directories(GetProfilePath(xuid))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MountProfile(xuid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool is_account_created = CreateAccount(xuid, gamertag);
|
||||
if (is_account_created && default_xuid) {
|
||||
Login(xuid);
|
||||
}
|
||||
return is_account_created;
|
||||
}
|
||||
|
||||
bool ProfileManager::CreateAccount(const uint64_t xuid,
|
||||
const std::string gamertag) {
|
||||
const std::string guest_path =
|
||||
xe::string_util::to_hex_string(xuid) + ":\\Account";
|
||||
|
||||
xe::vfs::File* output_file;
|
||||
xe::vfs::FileAction action = {};
|
||||
auto status = kernel_state_->file_system()->OpenFile(
|
||||
nullptr, guest_path, xe::vfs::FileDisposition::kCreate,
|
||||
xe::vfs::FileAccess::kFileWriteData, false, true, &output_file, &action);
|
||||
|
||||
if (XFAILED(status) || !output_file || !output_file->entry()) {
|
||||
XELOGI("{}: Failed to open Account file for creation: {:08X}", __func__,
|
||||
status);
|
||||
DismountProfile(xuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
X_XAMACCOUNTINFO account = {};
|
||||
std::u16string gamertag_u16 = xe::to_utf16(gamertag);
|
||||
|
||||
string_util::copy_truncating(account.gamertag, gamertag_u16,
|
||||
sizeof(account.gamertag));
|
||||
|
||||
std::vector<uint8_t> encrypted_data;
|
||||
encrypted_data.resize(sizeof(X_XAMACCOUNTINFO) + 0x18);
|
||||
EncryptAccountFile(&account, encrypted_data.data());
|
||||
|
||||
size_t written_bytes = 0;
|
||||
output_file->WriteSync(encrypted_data.data(), encrypted_data.size(), 0,
|
||||
&written_bytes);
|
||||
output_file->Destroy();
|
||||
|
||||
DismountProfile(xuid);
|
||||
|
||||
accounts_.insert({xuid, account});
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProfileManager::UpdateConfig(const uint64_t xuid, const uint8_t slot) {
|
||||
if (slot >= XUserMaxUserCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string hex_xuid = xe::string_util::to_hex_string(xuid);
|
||||
switch (slot) {
|
||||
case 0:
|
||||
OVERRIDE_string(logged_profile_slot_0_xuid, hex_xuid);
|
||||
break;
|
||||
case 1:
|
||||
OVERRIDE_string(logged_profile_slot_1_xuid, hex_xuid);
|
||||
break;
|
||||
case 2:
|
||||
OVERRIDE_string(logged_profile_slot_2_xuid, hex_xuid);
|
||||
break;
|
||||
case 3:
|
||||
OVERRIDE_string(logged_profile_slot_3_xuid, hex_xuid);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool ProfileManager::DeleteProfile(const uint64_t xuid) {
|
||||
const uint8_t user_index = GetUserIndexAssignedToProfile(xuid);
|
||||
|
||||
if (user_index < XUserMaxUserCount) {
|
||||
Logout(user_index);
|
||||
}
|
||||
|
||||
DismountProfile(xuid);
|
||||
|
||||
if (accounts_.count(xuid)) {
|
||||
accounts_.erase(xuid);
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(GetProfileContentPath(xuid), ec);
|
||||
if (ec) {
|
||||
XELOGE("Cannot remove profile: {}", ec.message());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProfileManager::IsGamertagValid(const std::string gamertag) {
|
||||
if (gamertag.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gamertag.length() > 15) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gamertag cannot start with a number.
|
||||
if (std::isdigit(gamertag.at(0))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::find_if(gamertag.cbegin(), gamertag.cend(), [](char c) {
|
||||
return !(std::isalnum(c) || (c == ' '));
|
||||
}) == gamertag.cend();
|
||||
}
|
||||
|
||||
} // namespace xam
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2024 Xenia Canary. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_KERNEL_XAM_PROFILE_MANAGER_H_
|
||||
#define XENIA_KERNEL_XAM_PROFILE_MANAGER_H_
|
||||
|
||||
#include <bitset>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include "xenia/base/string.h"
|
||||
#include "xenia/kernel/xam/user_profile.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
class KernelState;
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
|
||||
constexpr uint32_t kDashboardID = 0xFFFE07D1;
|
||||
const static std::string kDashboardStringID =
|
||||
fmt::format("{:08X}", kDashboardID);
|
||||
|
||||
enum class XTileType {
|
||||
kAchievement,
|
||||
kGameIcon,
|
||||
kGamerTile,
|
||||
kGamerTileSmall,
|
||||
kLocalGamerTile,
|
||||
kLocalGamerTileSmall,
|
||||
kBkgnd,
|
||||
kAwardedGamerTile,
|
||||
kAwardedGamerTileSmall,
|
||||
kGamerTileByImageId,
|
||||
kPersonalGamerTile,
|
||||
kPersonalGamerTileSmall,
|
||||
kGamerTileByKey,
|
||||
kAvatarGamerTile,
|
||||
kAvatarGamerTileSmall,
|
||||
kAvatarFullBody
|
||||
};
|
||||
|
||||
// TODO: find filenames of other tile types that are stored in profile
|
||||
static const std::map<XTileType, std::string> kTileFileNames = {
|
||||
{XTileType::kPersonalGamerTile, "tile_64.png"},
|
||||
{XTileType::kPersonalGamerTileSmall, "tile_32.png"},
|
||||
{XTileType::kAvatarGamerTile, "avtr_64.png"},
|
||||
{XTileType::kAvatarGamerTileSmall, "avtr_32.png"},
|
||||
};
|
||||
|
||||
class ProfileManager {
|
||||
public:
|
||||
static bool DecryptAccountFile(const uint8_t* data, X_XAMACCOUNTINFO* output,
|
||||
bool devkit = false);
|
||||
|
||||
static void EncryptAccountFile(const X_XAMACCOUNTINFO* input, uint8_t* output,
|
||||
bool devkit = false);
|
||||
|
||||
// Profile:
|
||||
// - Account
|
||||
// - GPDs (Dashboard, titles)
|
||||
|
||||
// Loading Profile means load everything
|
||||
// Loading Account means load basic data
|
||||
ProfileManager(KernelState* kernel_state);
|
||||
|
||||
~ProfileManager();
|
||||
|
||||
bool CreateProfile(const std::string gamertag, bool default_xuid = false);
|
||||
// bool CreateProfile(const X_XAMACCOUNTINFO* account_info);
|
||||
bool DeleteProfile(const uint64_t xuid);
|
||||
|
||||
bool MountProfile(const uint64_t xuid);
|
||||
bool DismountProfile(const uint64_t xuid);
|
||||
|
||||
void Login(const uint64_t xuid, const uint8_t user_index = -1);
|
||||
void Logout(const uint8_t user_index);
|
||||
|
||||
bool LoadAccount(const uint64_t xuid);
|
||||
void LoadAccounts(const std::vector<uint64_t> profiles_xuids);
|
||||
|
||||
void ReloadProfiles();
|
||||
|
||||
UserProfile* GetProfile(const uint64_t xuid) const;
|
||||
UserProfile* GetProfile(const uint8_t user_index) const;
|
||||
uint8_t GetUserIndexAssignedToProfile(const uint64_t xuid) const;
|
||||
|
||||
std::map<uint64_t, X_XAMACCOUNTINFO>* GetProfiles() { return &accounts_; }
|
||||
|
||||
uint32_t GetProfilesCount() const {
|
||||
return static_cast<uint32_t>(accounts_.size());
|
||||
}
|
||||
bool IsAnyProfileSignedIn() const { return !logged_profiles_.empty(); }
|
||||
|
||||
std::filesystem::path GetProfileContentPath(
|
||||
const uint64_t xuid, const uint32_t title_id = -1) const;
|
||||
|
||||
static bool IsGamertagValid(const std::string gamertag);
|
||||
|
||||
private:
|
||||
void UpdateConfig(const uint64_t xuid, const uint8_t slot);
|
||||
bool CreateAccount(const uint64_t xuid, const std::string gamertag);
|
||||
|
||||
std::filesystem::path GetProfilePath(const uint64_t xuid) const;
|
||||
std::filesystem::path GetProfilePath(const std::string xuid) const;
|
||||
|
||||
std::vector<uint64_t> FindProfiles() const;
|
||||
|
||||
uint8_t FindFirstFreeProfileSlot() const;
|
||||
std::bitset<XUserMaxUserCount> GetUsedUserSlots() const;
|
||||
|
||||
uint64_t GenerateXuid() const {
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
|
||||
return ((uint64_t)0xE03 << 52) + (gen() % (1 << 31));
|
||||
}
|
||||
|
||||
std::map<uint64_t, X_XAMACCOUNTINFO> accounts_;
|
||||
std::map<uint8_t, std::unique_ptr<UserProfile>> logged_profiles_;
|
||||
|
||||
KernelState* kernel_state_;
|
||||
};
|
||||
|
||||
} // namespace xam
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_KERNEL_XAM_PROFILE_MANAGER_H_
|
|
@ -19,15 +19,11 @@ namespace xe {
|
|||
namespace kernel {
|
||||
namespace xam {
|
||||
|
||||
UserProfile::UserProfile(uint8_t index) {
|
||||
UserProfile::UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info)
|
||||
: xuid_(xuid), account_info_(*account_info) {
|
||||
// 58410A1F checks the user XUID against a mask of 0x00C0000000000000 (3<<54),
|
||||
// if non-zero, it prevents the user from playing the game.
|
||||
// "You do not have permissions to perform this operation."
|
||||
xuid_ = 0xB13EBABEBABEBABE + index;
|
||||
name_ = "User";
|
||||
if (index) {
|
||||
name_ = "User_" + std::to_string(index);
|
||||
}
|
||||
|
||||
// https://cs.rin.ru/forum/viewtopic.php?f=38&t=60668&hilit=gfwl+live&start=195
|
||||
// https://github.com/arkem/py360/blob/master/py360/constants.py
|
||||
|
@ -136,7 +132,7 @@ UserSetting* UserProfile::GetSetting(uint32_t setting_id) {
|
|||
void UserProfile::LoadSetting(UserSetting* setting) {
|
||||
if (setting->is_title_specific()) {
|
||||
const std::filesystem::path content_dir =
|
||||
kernel_state()->content_manager()->ResolveGameUserContentPath();
|
||||
kernel_state()->content_manager()->ResolveGameUserContentPath(xuid_);
|
||||
const std::string setting_id_str =
|
||||
fmt::format("{:08X}", setting->GetSettingId());
|
||||
const std::filesystem::path file_path = content_dir / setting_id_str;
|
||||
|
@ -182,7 +178,7 @@ void UserProfile::SaveSetting(UserSetting* setting) {
|
|||
if (setting->is_title_specific() &&
|
||||
setting->GetSettingSource() == X_USER_PROFILE_SETTING_SOURCE::TITLE) {
|
||||
const std::filesystem::path content_dir =
|
||||
kernel_state()->content_manager()->ResolveGameUserContentPath();
|
||||
kernel_state()->content_manager()->ResolveGameUserContentPath(xuid_);
|
||||
|
||||
std::filesystem::create_directories(content_dir);
|
||||
|
||||
|
|
|
@ -153,10 +153,10 @@ class UserSetting {
|
|||
|
||||
class UserProfile {
|
||||
public:
|
||||
UserProfile(uint8_t index);
|
||||
UserProfile(uint64_t xuid, X_XAMACCOUNTINFO* account_info);
|
||||
|
||||
uint64_t xuid() const { return xuid_; }
|
||||
std::string name() const { return name_; }
|
||||
std::string name() const { return account_info_.GetGamertagString(); }
|
||||
uint32_t signin_state() const { return 1; }
|
||||
uint32_t type() const { return 1 | 2; /* local | online profile? */ }
|
||||
|
||||
|
@ -170,7 +170,8 @@ class UserProfile {
|
|||
|
||||
private:
|
||||
uint64_t xuid_;
|
||||
std::string name_;
|
||||
X_XAMACCOUNTINFO account_info_;
|
||||
|
||||
std::vector<std::unique_ptr<UserSetting>> setting_list_;
|
||||
std::unordered_map<uint32_t, UserSetting*> settings_;
|
||||
|
||||
|
|
|
@ -92,28 +92,66 @@ dword_result_t XamContentCreateEnumerator_entry(
|
|||
*buffer_size_ptr = sizeof(XCONTENT_DATA) * items_per_enumerate;
|
||||
}
|
||||
|
||||
uint64_t xuid = 0;
|
||||
if (user_index != XUserIndexNone) {
|
||||
const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
if (!user) {
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
xuid = user->xuid();
|
||||
}
|
||||
|
||||
auto e = make_object<XStaticEnumerator<XCONTENT_DATA>>(kernel_state(),
|
||||
items_per_enumerate);
|
||||
auto result = e->Initialize(0xFF, 0xFE, 0x20005, 0x20007, 0);
|
||||
auto result = e->Initialize(XUserIndexAny, 0xFE, 0x20005, 0x20007, 0);
|
||||
if (XFAILED(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<XCONTENT_AGGREGATE_DATA> enumerated_content = {};
|
||||
|
||||
if (!device_info || device_info->device_id == DummyDeviceId::HDD) {
|
||||
// Get all content data.
|
||||
auto content_datas = kernel_state()->content_manager()->ListContent(
|
||||
static_cast<uint32_t>(DummyDeviceId::HDD),
|
||||
XContentType(uint32_t(content_type)));
|
||||
for (const auto& content_data : content_datas) {
|
||||
auto item = e->AppendItem();
|
||||
*item = content_data;
|
||||
if (xuid) {
|
||||
auto user_enumerated_data =
|
||||
kernel_state()->content_manager()->ListContent(
|
||||
static_cast<uint32_t>(DummyDeviceId::HDD), xuid,
|
||||
kernel_state()->title_id(), XContentType(uint32_t(content_type)));
|
||||
|
||||
enumerated_content.insert(enumerated_content.end(),
|
||||
user_enumerated_data.cbegin(),
|
||||
user_enumerated_data.cend());
|
||||
}
|
||||
|
||||
if (!(content_flags & 0x00001000)) {
|
||||
auto common_enumerated_data =
|
||||
kernel_state()->content_manager()->ListContent(
|
||||
static_cast<uint32_t>(DummyDeviceId::HDD), 0,
|
||||
kernel_state()->title_id(), XContentType(uint32_t(content_type)));
|
||||
|
||||
enumerated_content.insert(enumerated_content.end(),
|
||||
common_enumerated_data.cbegin(),
|
||||
common_enumerated_data.cend());
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
enumerated_content.erase(
|
||||
std::unique(enumerated_content.begin(), enumerated_content.end()),
|
||||
enumerated_content.end());
|
||||
}
|
||||
|
||||
if (!device_info || device_info->device_id == DummyDeviceId::ODD) {
|
||||
// TODO(gibbed): disc drive content
|
||||
}
|
||||
|
||||
for (const auto& content_data : enumerated_content) {
|
||||
auto item = e->AppendItem();
|
||||
*item = content_data;
|
||||
XELOGI("{}: Adding: {} (Filename: {}) to enumerator result", __func__,
|
||||
xe::to_utf8(content_data.display_name()), content_data.file_name());
|
||||
}
|
||||
|
||||
XELOGD("XamContentCreateEnumerator: added {} items to enumerator",
|
||||
e->item_count());
|
||||
|
||||
|
@ -131,6 +169,17 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name,
|
|||
lpdword_t license_mask_ptr,
|
||||
dword_t cache_size, qword_t content_size,
|
||||
lpvoid_t overlapped_ptr) {
|
||||
uint64_t xuid = 0;
|
||||
if (user_index != XUserIndexNone) {
|
||||
const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
if (!user) {
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
xuid = user->xuid();
|
||||
}
|
||||
|
||||
XCONTENT_AGGREGATE_DATA content_data;
|
||||
if (content_data_size == sizeof(XCONTENT_DATA)) {
|
||||
content_data = *content_data_ptr.as<XCONTENT_DATA*>();
|
||||
|
@ -141,13 +190,17 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name,
|
|||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (content_data.content_type == XContentType::kMarketplaceContent) {
|
||||
xuid = 0;
|
||||
}
|
||||
|
||||
auto content_manager = kernel_state()->content_manager();
|
||||
|
||||
if (overlapped_ptr && disposition_ptr) {
|
||||
*disposition_ptr = 0;
|
||||
}
|
||||
|
||||
auto run = [content_manager, root_name = root_name.value(), flags,
|
||||
auto run = [content_manager, xuid, root_name = root_name.value(), flags,
|
||||
content_data, disposition_ptr, license_mask_ptr, overlapped_ptr](
|
||||
uint32_t& extended_error, uint32_t& length) -> X_RESULT {
|
||||
X_RESULT result = X_ERROR_INVALID_PARAMETER;
|
||||
|
@ -155,7 +208,7 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name,
|
|||
switch (flags & 0xF) {
|
||||
case 1: // CREATE_NEW
|
||||
// Fail if exists.
|
||||
if (content_manager->ContentExists(content_data)) {
|
||||
if (content_manager->ContentExists(xuid, content_data)) {
|
||||
result = X_ERROR_ALREADY_EXISTS;
|
||||
} else {
|
||||
disposition = kDispositionState::Create;
|
||||
|
@ -163,14 +216,14 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name,
|
|||
break;
|
||||
case 2: // CREATE_ALWAYS
|
||||
// Overwrite existing, if any.
|
||||
if (content_manager->ContentExists(content_data)) {
|
||||
content_manager->DeleteContent(content_data);
|
||||
if (content_manager->ContentExists(xuid, content_data)) {
|
||||
content_manager->DeleteContent(xuid, content_data);
|
||||
}
|
||||
disposition = kDispositionState::Create;
|
||||
break;
|
||||
case 3: // OPEN_EXISTING
|
||||
// Open only if exists.
|
||||
if (!content_manager->ContentExists(content_data)) {
|
||||
if (!content_manager->ContentExists(xuid, content_data)) {
|
||||
result = X_ERROR_PATH_NOT_FOUND;
|
||||
} else {
|
||||
disposition = kDispositionState::Open;
|
||||
|
@ -178,7 +231,7 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name,
|
|||
break;
|
||||
case 4: // OPEN_ALWAYS
|
||||
// Create if needed.
|
||||
if (!content_manager->ContentExists(content_data)) {
|
||||
if (!content_manager->ContentExists(xuid, content_data)) {
|
||||
disposition = kDispositionState::Create;
|
||||
} else {
|
||||
disposition = kDispositionState::Open;
|
||||
|
@ -186,10 +239,10 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name,
|
|||
break;
|
||||
case 5: // TRUNCATE_EXISTING
|
||||
// Fail if doesn't exist, if does exist delete and recreate.
|
||||
if (!content_manager->ContentExists(content_data)) {
|
||||
if (!content_manager->ContentExists(xuid, content_data)) {
|
||||
result = X_ERROR_PATH_NOT_FOUND;
|
||||
} else {
|
||||
content_manager->DeleteContent(content_data);
|
||||
content_manager->DeleteContent(xuid, content_data);
|
||||
disposition = kDispositionState::Create;
|
||||
}
|
||||
break;
|
||||
|
@ -199,12 +252,12 @@ dword_result_t xeXamContentCreate(dword_t user_index, lpstring_t root_name,
|
|||
}
|
||||
|
||||
if (disposition == kDispositionState::Create) {
|
||||
result = content_manager->CreateContent(root_name, content_data);
|
||||
result = content_manager->CreateContent(root_name, xuid, content_data);
|
||||
if (XSUCCEEDED(result)) {
|
||||
content_manager->WriteContentHeaderFile(&content_data);
|
||||
content_manager->WriteContentHeaderFile(xuid, &content_data);
|
||||
}
|
||||
} else if (disposition == kDispositionState::Open) {
|
||||
result = content_manager->OpenContent(root_name, content_data);
|
||||
result = content_manager->OpenContent(root_name, xuid, content_data);
|
||||
}
|
||||
|
||||
if (license_mask_ptr && XSUCCEEDED(result)) {
|
||||
|
@ -265,7 +318,7 @@ dword_result_t XamContentCreateInternal_entry(
|
|||
lpstring_t root_name, lpvoid_t content_data_ptr, dword_t flags,
|
||||
lpdword_t disposition_ptr, lpdword_t license_mask_ptr, dword_t cache_size,
|
||||
qword_t content_size, lpvoid_t overlapped_ptr) {
|
||||
return xeXamContentCreate(0xFE, root_name, content_data_ptr,
|
||||
return xeXamContentCreate(XUserIndexNone, root_name, content_data_ptr,
|
||||
sizeof(XCONTENT_AGGREGATE_DATA), flags,
|
||||
disposition_ptr, license_mask_ptr, cache_size,
|
||||
content_size, overlapped_ptr);
|
||||
|
@ -319,15 +372,21 @@ dword_result_t XamContentGetCreator_entry(dword_t user_index,
|
|||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
if (!user) {
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
XCONTENT_AGGREGATE_DATA content_data = *content_data_ptr.as<XCONTENT_DATA*>();
|
||||
|
||||
auto run = [content_data, user_index, is_creator_ptr, creator_xuid_ptr,
|
||||
overlapped_ptr](uint32_t& extended_error,
|
||||
uint32_t& length) -> X_RESULT {
|
||||
auto run = [content_data, xuid = user->xuid(), user_index, is_creator_ptr,
|
||||
creator_xuid_ptr, overlapped_ptr](uint32_t& extended_error,
|
||||
uint32_t& length) -> X_RESULT {
|
||||
X_RESULT result = X_ERROR_SUCCESS;
|
||||
|
||||
bool content_exists =
|
||||
kernel_state()->content_manager()->ContentExists(content_data);
|
||||
kernel_state()->content_manager()->ContentExists(xuid, content_data);
|
||||
|
||||
if (content_exists) {
|
||||
if (content_data.content_type == XContentType::kSavedGame) {
|
||||
|
@ -377,6 +436,12 @@ dword_result_t XamContentGetThumbnail_entry(dword_t user_index,
|
|||
lpvoid_t buffer_ptr,
|
||||
lpdword_t buffer_size_ptr,
|
||||
lpunknown_t overlapped_ptr) {
|
||||
const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
if (!user) {
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
assert_not_null(buffer_size_ptr);
|
||||
uint32_t buffer_size = *buffer_size_ptr;
|
||||
XCONTENT_AGGREGATE_DATA content_data = *content_data_ptr.as<XCONTENT_DATA*>();
|
||||
|
@ -384,7 +449,7 @@ dword_result_t XamContentGetThumbnail_entry(dword_t user_index,
|
|||
// Get thumbnail (if it exists).
|
||||
std::vector<uint8_t> buffer;
|
||||
auto result = kernel_state()->content_manager()->GetContentThumbnail(
|
||||
content_data, &buffer);
|
||||
user->xuid(), content_data, &buffer);
|
||||
|
||||
*buffer_size_ptr = uint32_t(buffer.size());
|
||||
|
||||
|
@ -416,13 +481,19 @@ dword_result_t XamContentSetThumbnail_entry(dword_t user_index,
|
|||
lpvoid_t buffer_ptr,
|
||||
dword_t buffer_size,
|
||||
lpunknown_t overlapped_ptr) {
|
||||
const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
if (!user) {
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
XCONTENT_AGGREGATE_DATA content_data = *content_data_ptr.as<XCONTENT_DATA*>();
|
||||
|
||||
// Buffer is PNG data.
|
||||
auto buffer = std::vector<uint8_t>((uint8_t*)buffer_ptr,
|
||||
(uint8_t*)buffer_ptr + buffer_size);
|
||||
auto result = kernel_state()->content_manager()->SetContentThumbnail(
|
||||
content_data, std::move(buffer));
|
||||
user->xuid(), content_data, std::move(buffer));
|
||||
|
||||
if (overlapped_ptr) {
|
||||
kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, result);
|
||||
|
@ -436,9 +507,20 @@ DECLARE_XAM_EXPORT1(XamContentSetThumbnail, kContent, kImplemented);
|
|||
dword_result_t XamContentDelete_entry(dword_t user_index,
|
||||
lpvoid_t content_data_ptr,
|
||||
lpunknown_t overlapped_ptr) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_ERROR_ACCESS_DENIED;
|
||||
}
|
||||
|
||||
const auto& user = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
if (!user) {
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
XCONTENT_AGGREGATE_DATA content_data = *content_data_ptr.as<XCONTENT_DATA*>();
|
||||
|
||||
auto result = kernel_state()->content_manager()->DeleteContent(content_data);
|
||||
auto result = kernel_state()->content_manager()->DeleteContent(user->xuid(),
|
||||
content_data);
|
||||
|
||||
if (overlapped_ptr) {
|
||||
kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, result);
|
||||
|
@ -453,7 +535,8 @@ dword_result_t XamContentDeleteInternal_entry(lpvoid_t content_data_ptr,
|
|||
lpunknown_t overlapped_ptr) {
|
||||
// INFO: Analysis of xam.xex shows that "internal" functions are wrappers with
|
||||
// 0xFE as user_index
|
||||
return XamContentDelete_entry(0xFE, content_data_ptr, overlapped_ptr);
|
||||
return XamContentDelete_entry(XUserIndexNone, content_data_ptr,
|
||||
overlapped_ptr);
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XamContentDeleteInternal, kContent, kImplemented);
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ dword_result_t XamContentAggregateCreateEnumerator_entry(qword_t xuid,
|
|||
auto e = make_object<XStaticEnumerator<XCONTENT_AGGREGATE_DATA>>(
|
||||
kernel_state(), 1);
|
||||
X_KENUMERATOR_CONTENT_AGGREGATE* extra;
|
||||
auto result = e->Initialize(0xFF, 0xFE, 0x2000E, 0x20010, 0, &extra);
|
||||
auto result = e->Initialize(XUserIndexAny, 0xFE, 0x2000E, 0x20010, 0, &extra);
|
||||
if (XFAILED(result)) {
|
||||
return result;
|
||||
}
|
||||
|
@ -115,8 +115,8 @@ dword_result_t XamContentAggregateCreateEnumerator_entry(qword_t xuid,
|
|||
for (auto& title_id : title_ids) {
|
||||
// Get all content data.
|
||||
auto content_datas = kernel_state()->content_manager()->ListContent(
|
||||
static_cast<uint32_t>(DummyDeviceId::HDD), content_type_enum,
|
||||
title_id);
|
||||
static_cast<uint32_t>(DummyDeviceId::HDD), xuid == -1 ? 0 : xuid,
|
||||
title_id, content_type_enum);
|
||||
for (const auto& content_data : content_datas) {
|
||||
auto item = e->AppendItem();
|
||||
assert_not_null(item);
|
||||
|
|
|
@ -159,7 +159,7 @@ dword_result_t XamContentCreateDeviceEnumerator_entry(dword_t content_type,
|
|||
|
||||
auto e = make_object<XStaticEnumerator<X_CONTENT_DEVICE_DATA>>(kernel_state(),
|
||||
max_count);
|
||||
auto result = e->Initialize(0xFE, 0xFE, 0x2000A, 0x20009, 0);
|
||||
auto result = e->Initialize(XUserIndexNone, 0xFE, 0x2000A, 0x20009, 0);
|
||||
if (XFAILED(result)) {
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ constexpr uint32_t XINPUT_FLAG_ANYDEVICE = 0xFF;
|
|||
constexpr uint32_t XINPUT_FLAG_ANY_USER = 1 << 30;
|
||||
|
||||
dword_result_t XAutomationpUnbindController_entry(dword_t user_index) {
|
||||
if (user_index > 4) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,8 @@ dword_result_t XamInputGetCapabilitiesEx_entry(
|
|||
}
|
||||
|
||||
uint32_t actual_user_index = user_index;
|
||||
if ((actual_user_index & 0xFF) == 0xFF || (flags & XINPUT_FLAG_ANY_USER)) {
|
||||
if ((actual_user_index & XUserIndexAny) == XUserIndexAny ||
|
||||
(flags & XINPUT_FLAG_ANY_USER)) {
|
||||
// Always pin user to 0.
|
||||
actual_user_index = 0;
|
||||
}
|
||||
|
@ -105,7 +106,7 @@ dword_result_t XamInputGetState_entry(dword_t user_index, dword_t flags,
|
|||
if (input_state) {
|
||||
memset((void*)input_state.host_address(), 0, sizeof(X_INPUT_STATE));
|
||||
}
|
||||
if (user_index >= 4) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_ERROR_DEVICE_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
|
@ -118,7 +119,8 @@ dword_result_t XamInputGetState_entry(dword_t user_index, dword_t flags,
|
|||
|
||||
uint32_t actual_user_index = user_index;
|
||||
// chrispy: change this, logic is not right
|
||||
if ((actual_user_index & 0xFF) == 0xFF || (flags & XINPUT_FLAG_ANY_USER)) {
|
||||
if ((actual_user_index & XUserIndexAny) == XUserIndexAny ||
|
||||
(flags & XINPUT_FLAG_ANY_USER)) {
|
||||
// Always pin user to 0.
|
||||
actual_user_index = 0;
|
||||
}
|
||||
|
@ -134,19 +136,13 @@ dword_result_t XamInputSetState_entry(
|
|||
dword_t user_index,
|
||||
dword_t flags, /* flags, as far as i can see, is not used*/
|
||||
pointer_t<X_INPUT_VIBRATION> vibration) {
|
||||
if (user_index >= 4) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_E_DEVICE_NOT_CONNECTED;
|
||||
}
|
||||
if (!vibration) {
|
||||
return X_ERROR_BAD_ARGUMENTS;
|
||||
}
|
||||
|
||||
uint32_t actual_user_index = user_index;
|
||||
if ((user_index & 0xFF) == 0xFF) {
|
||||
// Always pin user to 0.
|
||||
actual_user_index = 0;
|
||||
}
|
||||
|
||||
auto input_system = kernel_state()->emulator()->input_system();
|
||||
auto lock = input_system->lock();
|
||||
return input_system->SetState(user_index, vibration);
|
||||
|
@ -170,7 +166,8 @@ dword_result_t XamInputGetKeystroke_entry(
|
|||
}
|
||||
|
||||
uint32_t actual_user_index = user_index;
|
||||
if ((actual_user_index & 0xFF) == 0xFF || (flags & XINPUT_FLAG_ANY_USER)) {
|
||||
if ((actual_user_index & XUserIndexAny) == XUserIndexAny ||
|
||||
(flags & XINPUT_FLAG_ANY_USER)) {
|
||||
// Always pin user to 0.
|
||||
actual_user_index = 0;
|
||||
}
|
||||
|
@ -197,7 +194,7 @@ dword_result_t XamInputGetKeystrokeEx_entry(
|
|||
uint32_t user_index = *user_index_ptr;
|
||||
auto input_system = kernel_state()->emulator()->input_system();
|
||||
auto lock = input_system->lock();
|
||||
if ((user_index & 0xFF) == 0xFF) {
|
||||
if ((user_index & XUserIndexAny) == XUserIndexAny) {
|
||||
// Always pin user to 0.
|
||||
user_index = 0;
|
||||
}
|
||||
|
@ -206,7 +203,7 @@ dword_result_t XamInputGetKeystrokeEx_entry(
|
|||
// That flag means we should iterate over every connected controller and
|
||||
// check which one have pending request.
|
||||
auto result = X_ERROR_DEVICE_NOT_CONNECTED;
|
||||
for (uint32_t i = 0; i < 4; i++) {
|
||||
for (uint32_t i = 0; i < XUserMaxUserCount; i++) {
|
||||
auto result = input_system->GetKeystroke(i, flags, keystroke);
|
||||
|
||||
// Return result from first user that have pending request
|
||||
|
@ -235,7 +232,7 @@ X_HRESULT_result_t XamUserGetDeviceContext_entry(dword_t user_index,
|
|||
// set zero just to be safe.
|
||||
*out_ptr = 0;
|
||||
if (kernel_state()->xam_state()->IsUserSignedIn(user_index) ||
|
||||
(user_index & 0xFF) == 0xFF) {
|
||||
(user_index & XUserIndexAny) == XUserIndexAny) {
|
||||
*out_ptr = (uint32_t)user_index;
|
||||
return X_E_SUCCESS;
|
||||
} else {
|
||||
|
|
|
@ -10,10 +10,6 @@
|
|||
#include "xenia/kernel/xam/xam_state.h"
|
||||
#include "xenia/emulator.h"
|
||||
|
||||
DEFINE_uint32(max_signed_profiles, 4,
|
||||
"Limits how many profiles can be assigned. Possible values: 1-4",
|
||||
"Kernel");
|
||||
|
||||
namespace xe {
|
||||
namespace kernel {
|
||||
namespace xam {
|
||||
|
@ -29,8 +25,7 @@ XamState::XamState(Emulator* emulator, KernelState* kernel_state)
|
|||
content_manager_ =
|
||||
std::make_unique<ContentManager>(kernel_state, content_root);
|
||||
|
||||
user_profiles_.emplace(0, std::make_unique<xam::UserProfile>(0));
|
||||
|
||||
profile_manager_ = std::make_unique<ProfileManager>(kernel_state);
|
||||
achievement_manager_ = std::make_unique<AchievementManager>();
|
||||
|
||||
AppManager::RegisterApps(kernel_state, app_manager_.get());
|
||||
|
@ -43,47 +38,20 @@ XamState::~XamState() {
|
|||
}
|
||||
|
||||
UserProfile* XamState::GetUserProfile(uint32_t user_index) const {
|
||||
if (!IsUserSignedIn(user_index)) {
|
||||
if (user_index >= XUserMaxUserCount && user_index < XUserIndexLatest) {
|
||||
return nullptr;
|
||||
}
|
||||
return user_profiles_.at(user_index).get();
|
||||
|
||||
return profile_manager_->GetProfile(static_cast<uint8_t>(user_index));
|
||||
}
|
||||
|
||||
UserProfile* XamState::GetUserProfile(uint64_t xuid) const {
|
||||
for (const auto& [key, value] : user_profiles_) {
|
||||
if (value->xuid() == xuid) {
|
||||
return user_profiles_.at(key).get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void XamState::UpdateUsedUserProfiles() {
|
||||
const std::bitset<4> used_slots = kernel_state_->GetConnectedUsers();
|
||||
|
||||
const uint32_t signed_profile_count =
|
||||
std::max(static_cast<uint32_t>(1),
|
||||
std::min(static_cast<uint32_t>(4), cvars::max_signed_profiles));
|
||||
|
||||
for (uint32_t i = 1; i < signed_profile_count; i++) {
|
||||
bool is_used = used_slots.test(i);
|
||||
|
||||
if (IsUserSignedIn(i) && !is_used) {
|
||||
user_profiles_.erase(i);
|
||||
kernel_state_->BroadcastNotification(
|
||||
kXNotificationIDSystemInputDevicesChanged, 0);
|
||||
}
|
||||
|
||||
if (!IsUserSignedIn(i) && is_used) {
|
||||
user_profiles_.emplace(i, std::make_unique<xam::UserProfile>(i));
|
||||
kernel_state_->BroadcastNotification(
|
||||
kXNotificationIDSystemInputDevicesChanged, 0);
|
||||
}
|
||||
}
|
||||
return profile_manager_->GetProfile(xuid);
|
||||
}
|
||||
|
||||
bool XamState::IsUserSignedIn(uint32_t user_index) const {
|
||||
return user_profiles_.find(user_index) != user_profiles_.cend();
|
||||
return profile_manager_->GetProfile(static_cast<uint8_t>(user_index)) !=
|
||||
nullptr;
|
||||
}
|
||||
|
||||
bool XamState::IsUserSignedIn(uint64_t xuid) const {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
#include "xenia/kernel/xam/achievement_manager.h"
|
||||
#include "xenia/kernel/xam/app_manager.h"
|
||||
#include "xenia/kernel/xam/content_manager.h"
|
||||
#include "xenia/kernel/xam/user_profile.h"
|
||||
#include "xenia/kernel/xam/profile_manager.h"
|
||||
|
||||
namespace xe {
|
||||
class Emulator;
|
||||
|
@ -40,12 +40,11 @@ class XamState {
|
|||
AchievementManager* achievement_manager() const {
|
||||
return achievement_manager_.get();
|
||||
}
|
||||
ProfileManager* profile_manager() const { return profile_manager_.get(); }
|
||||
|
||||
UserProfile* GetUserProfile(uint32_t user_index) const;
|
||||
UserProfile* GetUserProfile(uint64_t xuid) const;
|
||||
|
||||
void UpdateUsedUserProfiles();
|
||||
|
||||
bool IsUserSignedIn(uint32_t user_index) const;
|
||||
bool IsUserSignedIn(uint64_t xuid) const;
|
||||
|
||||
|
@ -55,8 +54,7 @@ class XamState {
|
|||
std::unique_ptr<AppManager> app_manager_;
|
||||
std::unique_ptr<ContentManager> content_manager_;
|
||||
std::unique_ptr<AchievementManager> achievement_manager_;
|
||||
|
||||
std::map<uint8_t, std::unique_ptr<UserProfile>> user_profiles_;
|
||||
std::unique_ptr<ProfileManager> profile_manager_;
|
||||
};
|
||||
|
||||
} // namespace xam
|
||||
|
|
|
@ -565,13 +565,13 @@ dword_result_t XamShowDeviceSelectorUI_entry(
|
|||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if ((user_index >= 4 && user_index != 0xFF) ||
|
||||
if ((user_index >= XUserMaxUserCount && user_index != XUserIndexAny) ||
|
||||
(content_flags & 0x83F00008) != 0 || !device_id_ptr) {
|
||||
XOverlappedSetExtendedError(overlapped, X_ERROR_INVALID_PARAMETER);
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (user_index != 0xFF &&
|
||||
if (user_index != XUserIndexAny &&
|
||||
!kernel_state()->xam_state()->IsUserSignedIn(user_index)) {
|
||||
kernel_state()->CompleteOverlappedImmediate(overlapped,
|
||||
X_ERROR_NO_SUCH_USER);
|
||||
|
@ -676,7 +676,7 @@ dword_result_t XamShowMarketplaceUI_entry(dword_t user_index, dword_t ui_type,
|
|||
// 1 - view content specified by offer id
|
||||
// content_types:
|
||||
// game specific, usually just -1
|
||||
if (user_index >= 4) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
|
@ -752,7 +752,7 @@ dword_result_t XamShowMarketplaceDownloadItemsUI_entry(
|
|||
// ui_type:
|
||||
// 1000 - free
|
||||
// 1001 - paid
|
||||
if (user_index >= 4 || !offers || num_offers > 6) {
|
||||
if (user_index >= XUserMaxUserCount || !offers || num_offers > 6) {
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
|
@ -819,6 +819,39 @@ dword_result_t XamShowMarketplaceDownloadItemsUI_entry(
|
|||
}
|
||||
DECLARE_XAM_EXPORT1(XamShowMarketplaceDownloadItemsUI, kUI, kSketchy);
|
||||
|
||||
dword_result_t XamShowSigninUI_entry(dword_t users_needed, dword_t unk_mask) {
|
||||
// XN_SYS_UI (on)
|
||||
kernel_state()->BroadcastNotification(kXNotificationIDSystemUI, 1);
|
||||
// Mask values vary. Probably matching user types? Local/remote?
|
||||
// Games seem to sit and loop until we trigger this notification:
|
||||
|
||||
auto run = [users_needed]() -> void {
|
||||
uint32_t user_mask = 0;
|
||||
uint32_t active_users = 0;
|
||||
|
||||
for (uint32_t i = 0; i < XUserMaxUserCount; i++) {
|
||||
if (kernel_state()->xam_state()->IsUserSignedIn(i)) {
|
||||
user_mask |= (1 << i);
|
||||
active_users++;
|
||||
if (active_users >= users_needed) break;
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(150));
|
||||
// XN_SYS_SIGNINCHANGED (players)
|
||||
kernel_state()->BroadcastNotification(kXNotificationIDSystemSignInChanged,
|
||||
user_mask);
|
||||
// XN_SYS_UI (off)
|
||||
kernel_state()->BroadcastNotification(kXNotificationIDSystemUI, 0);
|
||||
};
|
||||
|
||||
std::thread thread(run);
|
||||
thread.detach();
|
||||
|
||||
return X_ERROR_SUCCESS;
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XamShowSigninUI, kUserProfiles, kStub);
|
||||
|
||||
} // namespace xam
|
||||
} // namespace kernel
|
||||
} // namespace xe
|
||||
|
|
|
@ -33,25 +33,32 @@ X_HRESULT_result_t XamUserGetXUID_entry(dword_t user_index, dword_t type_mask,
|
|||
if (!xuid_ptr) {
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
|
||||
*xuid_ptr = 0;
|
||||
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
|
||||
if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) {
|
||||
return X_E_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
const auto& user_profile =
|
||||
kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
uint32_t result = X_E_NO_SUCH_USER;
|
||||
uint64_t xuid = 0;
|
||||
if (user_index < 4) {
|
||||
if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) {
|
||||
const auto& user_profile =
|
||||
kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
auto type = user_profile->type() & type_mask;
|
||||
if (type & (2 | 4)) {
|
||||
// maybe online profile?
|
||||
xuid = user_profile->xuid();
|
||||
result = X_E_SUCCESS;
|
||||
} else if (type & 1) {
|
||||
// maybe offline profile?
|
||||
xuid = user_profile->xuid();
|
||||
result = X_E_SUCCESS;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = X_E_INVALIDARG;
|
||||
|
||||
auto type = user_profile->type() & type_mask;
|
||||
if (type & (2 | 4)) {
|
||||
// maybe online profile?
|
||||
xuid = user_profile->xuid();
|
||||
result = X_E_SUCCESS;
|
||||
} else if (type & 1) {
|
||||
// maybe offline profile?
|
||||
xuid = user_profile->xuid();
|
||||
result = X_E_SUCCESS;
|
||||
}
|
||||
*xuid_ptr = xuid;
|
||||
return result;
|
||||
|
@ -62,12 +69,14 @@ dword_result_t XamUserGetSigninState_entry(dword_t user_index) {
|
|||
// Yield, as some games spam this.
|
||||
xe::threading::MaybeYield();
|
||||
uint32_t signin_state = 0;
|
||||
if (user_index < 4) {
|
||||
if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) {
|
||||
const auto& user_profile =
|
||||
kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
signin_state = user_profile->signin_state();
|
||||
}
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return signin_state;
|
||||
}
|
||||
|
||||
if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) {
|
||||
const auto& user_profile =
|
||||
kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
signin_state = user_profile->signin_state();
|
||||
}
|
||||
return signin_state;
|
||||
}
|
||||
|
@ -91,12 +100,10 @@ X_HRESULT_result_t XamUserGetSigninInfo_entry(
|
|||
}
|
||||
|
||||
std::memset(info, 0, sizeof(X_USER_SIGNIN_INFO));
|
||||
if (user_index > 3) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_E_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
kernel_state()->xam_state()->UpdateUsedUserProfiles();
|
||||
|
||||
if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) {
|
||||
const auto& user_profile =
|
||||
kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
@ -113,7 +120,7 @@ DECLARE_XAM_EXPORT1(XamUserGetSigninInfo, kUserProfiles, kImplemented);
|
|||
|
||||
dword_result_t XamUserGetName_entry(dword_t user_index, lpstring_t buffer,
|
||||
dword_t buffer_len) {
|
||||
if (user_index >= 4) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
|
@ -134,7 +141,7 @@ DECLARE_XAM_EXPORT1(XamUserGetName, kUserProfiles, kImplemented);
|
|||
dword_result_t XamUserGetGamerTag_entry(dword_t user_index,
|
||||
lpu16string_t buffer,
|
||||
dword_t buffer_len) {
|
||||
if (user_index >= 4) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
|
||||
|
@ -231,9 +238,9 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index,
|
|||
return X_ERROR_INSUFFICIENT_BUFFER;
|
||||
}
|
||||
|
||||
// Title ID = 0 means us.
|
||||
// 0xfffe07d1 = profile?
|
||||
if (!kernel_state()->xam_state()->IsUserSignedIn(user_index) && !xuids) {
|
||||
auto user_profile = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
if (!user_profile && !xuids) {
|
||||
if (overlapped) {
|
||||
kernel_state()->CompleteOverlappedImmediate(
|
||||
kernel_state()->memory()->HostToGuestVirtual(overlapped),
|
||||
|
@ -243,8 +250,6 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index,
|
|||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
auto user_profile = kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
if (xuids) {
|
||||
uint64_t user_xuid = static_cast<uint64_t>(xuids[0]);
|
||||
if (!kernel_state()->xam_state()->IsUserSignedIn(user_xuid)) {
|
||||
|
@ -259,6 +264,10 @@ uint32_t XamUserReadProfileSettingsEx(uint32_t title_id, uint32_t user_index,
|
|||
user_profile = kernel_state()->xam_state()->GetUserProfile(user_xuid);
|
||||
}
|
||||
|
||||
if (!user_profile) {
|
||||
return X_ERROR_NO_SUCH_USER;
|
||||
}
|
||||
|
||||
// First call asks for size (fill buffer_size_ptr).
|
||||
// Second call asks for buffer contents with that size.
|
||||
|
||||
|
@ -355,9 +364,12 @@ dword_result_t XamUserWriteProfileSettings_entry(
|
|||
if (!setting_count || !settings) {
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
// Update and save settings.
|
||||
const auto& user_profile =
|
||||
kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
// Skip writing data about users with id != 0 they're not supported
|
||||
if (user_index > 0) {
|
||||
if (!user_profile) {
|
||||
if (overlapped) {
|
||||
kernel_state()->CompleteOverlappedImmediate(
|
||||
kernel_state()->memory()->HostToGuestVirtual(overlapped),
|
||||
|
@ -366,9 +378,6 @@ dword_result_t XamUserWriteProfileSettings_entry(
|
|||
}
|
||||
return X_ERROR_SUCCESS;
|
||||
}
|
||||
// Update and save settings.
|
||||
const auto& user_profile =
|
||||
kernel_state()->xam_state()->GetUserProfile(user_index);
|
||||
|
||||
for (uint32_t n = 0; n < setting_count; ++n) {
|
||||
const X_USER_PROFILE_SETTING& setting = settings[n];
|
||||
|
@ -431,8 +440,8 @@ DECLARE_XAM_EXPORT1(XamUserWriteProfileSettings, kUserProfiles, kImplemented);
|
|||
dword_result_t XamUserCheckPrivilege_entry(dword_t user_index, dword_t mask,
|
||||
lpdword_t out_value) {
|
||||
// checking all users?
|
||||
if (user_index != 0xFF) {
|
||||
if (user_index >= 4) {
|
||||
if (user_index != XUserIndexAny) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
|
@ -494,7 +503,7 @@ dword_result_t XamUserIsOnlineEnabled_entry(dword_t user_index) { return 1; }
|
|||
DECLARE_XAM_EXPORT1(XamUserIsOnlineEnabled, kUserProfiles, kStub);
|
||||
|
||||
dword_result_t XamUserGetMembershipTier_entry(dword_t user_index) {
|
||||
if (user_index >= 4) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
|
@ -511,7 +520,7 @@ dword_result_t XamUserAreUsersFriends_entry(dword_t user_index, dword_t unk1,
|
|||
uint32_t are_friends = 0;
|
||||
X_RESULT result;
|
||||
|
||||
if (user_index >= 4) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
result = X_ERROR_INVALID_PARAMETER;
|
||||
} else {
|
||||
if (kernel_state()->xam_state()->IsUserSignedIn(user_index)) {
|
||||
|
@ -550,40 +559,6 @@ dword_result_t XamUserAreUsersFriends_entry(dword_t user_index, dword_t unk1,
|
|||
}
|
||||
DECLARE_XAM_EXPORT1(XamUserAreUsersFriends, kUserProfiles, kStub);
|
||||
|
||||
dword_result_t XamShowSigninUI_entry(dword_t users_needed, dword_t unk_mask) {
|
||||
// XN_SYS_UI (on)
|
||||
kernel_state()->BroadcastNotification(kXNotificationIDSystemUI, 1);
|
||||
kernel_state()->xam_state()->UpdateUsedUserProfiles();
|
||||
// Mask values vary. Probably matching user types? Local/remote?
|
||||
// Games seem to sit and loop until we trigger this notification:
|
||||
|
||||
auto run = [users_needed]() -> void {
|
||||
uint32_t user_mask = 0;
|
||||
uint32_t active_users = 0;
|
||||
|
||||
for (uint32_t i = 0; i < 4; i++) {
|
||||
if (kernel_state()->xam_state()->IsUserSignedIn(i)) {
|
||||
user_mask |= (1 << i);
|
||||
active_users++;
|
||||
if (active_users >= users_needed) break;
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(150));
|
||||
// XN_SYS_SIGNINCHANGED (players)
|
||||
kernel_state()->BroadcastNotification(kXNotificationIDSystemSignInChanged,
|
||||
user_mask);
|
||||
// XN_SYS_UI (off)
|
||||
kernel_state()->BroadcastNotification(kXNotificationIDSystemUI, 0);
|
||||
};
|
||||
|
||||
std::thread thread(run);
|
||||
thread.detach();
|
||||
|
||||
return X_ERROR_SUCCESS;
|
||||
}
|
||||
DECLARE_XAM_EXPORT1(XamShowSigninUI, kUserProfiles, kStub);
|
||||
|
||||
dword_result_t XamUserCreateAchievementEnumerator_entry(
|
||||
dword_t title_id, dword_t user_index, dword_t xuid, dword_t flags,
|
||||
dword_t offset, dword_t count, lpdword_t buffer_size_ptr,
|
||||
|
@ -592,7 +567,7 @@ dword_result_t XamUserCreateAchievementEnumerator_entry(
|
|||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (user_index >= 4) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
|
@ -724,7 +699,7 @@ dword_result_t XamUserGetSubscriptionType_entry(dword_t user_index,
|
|||
dword_t unk2, dword_t unk3,
|
||||
dword_t unk4, dword_t unk5,
|
||||
dword_t unk6) {
|
||||
if (!unk2 || !unk3 || user_index > 4) {
|
||||
if (!unk2 || !unk3 || user_index >= XUserMaxUserCount) {
|
||||
return X_E_INVALIDARG;
|
||||
}
|
||||
|
||||
|
@ -749,7 +724,7 @@ dword_result_t XamUserCreateStatsEnumerator_entry(
|
|||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (user_index >= 4) {
|
||||
if (user_index >= XUserMaxUserCount) {
|
||||
return X_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
|
|
|
@ -340,7 +340,8 @@ struct XContentMetadata {
|
|||
} description_ex_raw;
|
||||
|
||||
std::u16string display_name(XLanguage language) const {
|
||||
uint32_t lang_id = uint32_t(language) - 1;
|
||||
uint32_t lang_id =
|
||||
language == XLanguage::kInvalid ? 1 : uint32_t(language) - 1;
|
||||
|
||||
if (lang_id >= kNumLanguagesV2) {
|
||||
assert_always();
|
||||
|
|
|
@ -53,6 +53,8 @@ class XContentContainerDevice : public Device {
|
|||
return files_total_size_ - sizeof(XContentContainerHeader);
|
||||
}
|
||||
|
||||
uint64_t xuid() const { return header_->content_metadata.profile_id; }
|
||||
|
||||
uint32_t title_id() const {
|
||||
return header_->content_metadata.execution_info.title_id;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include <string>
|
||||
|
||||
#include "xenia/base/memory.h"
|
||||
#include "xenia/base/string.h"
|
||||
|
||||
// TODO(benvanik): split this header, cleanup, etc.
|
||||
// clang-format off
|
||||
|
@ -259,6 +260,12 @@ struct X_UNICODE_STRING {
|
|||
};
|
||||
static_assert_size(X_UNICODE_STRING, 8);
|
||||
|
||||
constexpr uint8_t XUserMaxUserCount = 4;
|
||||
|
||||
constexpr uint8_t XUserIndexLatest = 0xFD;
|
||||
constexpr uint8_t XUserIndexNone = 0xFE;
|
||||
constexpr uint8_t XUserIndexAny = 0xFF;
|
||||
|
||||
// https://github.com/ThirteenAG/Ultimate-ASI-Loader/blob/master/source/xlive/xliveless.h
|
||||
typedef uint32_t XNotificationID;
|
||||
enum : XNotificationID {
|
||||
|
@ -539,6 +546,84 @@ enum class XDeploymentType : uint32_t {
|
|||
kUnknown = 0xFF,
|
||||
};
|
||||
|
||||
#pragma pack(push, 4)
|
||||
struct X_XAMACCOUNTINFO {
|
||||
enum AccountReservedFlags {
|
||||
kPasswordProtected = 0x10000000,
|
||||
kLiveEnabled = 0x20000000,
|
||||
kRecovering = 0x40000000,
|
||||
kVersionMask = 0x000000FF
|
||||
};
|
||||
|
||||
enum AccountUserFlags {
|
||||
kPaymentInstrumentCreditCard = 1,
|
||||
|
||||
kCountryMask = 0xFF00,
|
||||
kSubscriptionTierMask = 0xF00000,
|
||||
kLanguageMask = 0x3E000000,
|
||||
|
||||
kParentalControlEnabled = 0x1000000,
|
||||
};
|
||||
|
||||
enum AccountSubscriptionTier {
|
||||
kSubscriptionTierSilver = 3,
|
||||
kSubscriptionTierGold = 6,
|
||||
kSubscriptionTierFamilyGold = 9
|
||||
};
|
||||
|
||||
enum AccountLiveFlags { kAcctRequiresManagement = 1 };
|
||||
|
||||
xe::be<uint32_t> reserved_flags;
|
||||
xe::be<uint32_t> live_flags;
|
||||
char16_t gamertag[0x10];
|
||||
xe::be<uint64_t> xuid_online; // 09....
|
||||
xe::be<uint32_t> cached_user_flags;
|
||||
xe::be<uint32_t> network_id;
|
||||
char passcode[4];
|
||||
char online_domain[0x14];
|
||||
char online_kerberos_realm[0x18];
|
||||
char online_key[0x10];
|
||||
char passport_membername[0x72];
|
||||
char passport_password[0x20];
|
||||
char owner_passport_membername[0x72];
|
||||
|
||||
bool IsPasscodeEnabled() {
|
||||
return static_cast<bool>(reserved_flags &
|
||||
AccountReservedFlags::kPasswordProtected);
|
||||
}
|
||||
|
||||
bool IsLiveEnabled() {
|
||||
return static_cast<bool>(reserved_flags &
|
||||
AccountReservedFlags::kLiveEnabled);
|
||||
}
|
||||
|
||||
bool IsXUIDOffline() { return ((xuid_online >> 60) & 0xF) == 0xE; }
|
||||
bool IsXUIDOnline() { return ((xuid_online >> 48) & 0xFFFF) == 0x9; }
|
||||
bool IsXUIDValid() { return IsXUIDOffline() != IsXUIDOnline(); }
|
||||
bool IsTeamXUID() {
|
||||
return (xuid_online & 0xFF00000000000140) == 0xFE00000000000100;
|
||||
}
|
||||
|
||||
uint32_t GetCountry() const {
|
||||
return (cached_user_flags & kCountryMask) >> 8;
|
||||
}
|
||||
|
||||
AccountSubscriptionTier GetSubscriptionTier() const {
|
||||
return static_cast<AccountSubscriptionTier>(
|
||||
(cached_user_flags & kSubscriptionTierMask) >> 20);
|
||||
}
|
||||
|
||||
XLanguage GetLanguage() const {
|
||||
return static_cast<XLanguage>((cached_user_flags & kLanguageMask) >> 25);
|
||||
}
|
||||
|
||||
std::string GetGamertagString() const {
|
||||
return xe::to_utf8(std::u16string(gamertag));
|
||||
}
|
||||
};
|
||||
static_assert_size(X_XAMACCOUNTINFO, 0x17C);
|
||||
#pragma pack(pop)
|
||||
|
||||
} // namespace xe
|
||||
|
||||
// clang-format on
|
||||
|
|
Loading…
Reference in New Issue