From 7bf03f6fa99dae6bf9826b9447145c2cf625b497 Mon Sep 17 00:00:00 2001 From: emoose Date: Thu, 26 Dec 2019 07:22:24 +0000 Subject: [PATCH] [XAM] Allow loading profile from STFS (extracts package automatically!) Profiles can now be placed as either an extracted folder with GPDs, or an STFS package, inside the Documents\Xenia\content\FFFE07D1\00010000\ directory eg. Documents\Xenia\content\FFFE07D1\00010000\E0000E07FA53D7F1 (this roughly matches the same location as X360 stores it) If loading an STFS package the package will first get extracted to .dir/, and then the profile is loaded/saved into that directory. (originally was going to mount the package and read everything in-memory, but then realized how hard adding new files/modifying/etc would be.. VFS doesn't allow mixing two devices into the same mount_path afaik) Code for extraction is taken from xenia-vfs-dump (as StfsContainerDevice::ExtractToFolder) A [XAM]profile_xuid config option is added too, which should let you pick which profile to load from the FFFE07D1\00010000\ folder if you have multiple there. (at least I hope it should - something like "profile_xuid = 0xE0000E07FA53D7F1" will work I hope... cpptoml might have issues with hex digits though, not sure, will investigate later...) If profile_xuid isn't set (left at -1), Xenia will just load whatever the first file/folder inside there is. --- src/xenia/kernel/kernel_state.cc | 3 +- src/xenia/kernel/xam/content_manager.cc | 30 +----- src/xenia/kernel/xam/content_manager.h | 3 +- src/xenia/kernel/xam/user_profile.cc | 100 +++++++++++++++--- src/xenia/kernel/xam/user_profile.h | 9 +- src/xenia/kernel/xam/xam_user.cc | 2 +- .../vfs/devices/stfs_container_device.cc | 78 ++++++++++++++ src/xenia/vfs/devices/stfs_container_device.h | 2 + 8 files changed, 181 insertions(+), 46 deletions(-) diff --git a/src/xenia/kernel/kernel_state.cc b/src/xenia/kernel/kernel_state.cc index 2dd0481b4..1293a16c6 100644 --- a/src/xenia/kernel/kernel_state.cc +++ b/src/xenia/kernel/kernel_state.cc @@ -48,12 +48,13 @@ KernelState::KernelState(Emulator* emulator) file_system_ = emulator->file_system(); app_manager_ = std::make_unique(); - user_profile_ = std::make_unique(this); auto content_root = emulator_->content_root(); content_root = xe::to_absolute_path(content_root); content_manager_ = std::make_unique(this, content_root); + user_profile_ = std::make_unique(this); + assert_null(shared_kernel_state_); shared_kernel_state_ = this; diff --git a/src/xenia/kernel/xam/content_manager.cc b/src/xenia/kernel/xam/content_manager.cc index c2ee72858..b418ce9be 100644 --- a/src/xenia/kernel/xam/content_manager.cc +++ b/src/xenia/kernel/xam/content_manager.cc @@ -44,37 +44,15 @@ uint32_t ContentManager::title_id() { return kernel_state_->title_id(); } -std::wstring ContentManager::ResolvePackageRoot(uint32_t content_type) { +std::wstring ContentManager::ResolvePackageRoot(uint32_t content_type, + uint32_t title_id) { wchar_t title_id_str[9] = L"00000000"; - std::swprintf(title_id_str, 9, L"%.8X", title_id()); + std::swprintf(title_id_str, 9, L"%.8X", + title_id == -1 ? this->title_id() : title_id); wchar_t content_type_str[9] = L"00000000"; std::swprintf(content_type_str, 9, L"%.8X", content_type); - std::wstring type_name; - switch (content_type) { - case 1: - // Save games. - type_name = L"00000001"; - break; - case 2: - // DLC from the marketplace. - type_name = L"00000002"; - break; - case 3: - // Publisher content? - type_name = L"00000003"; - break; - case 0x000D0000: - // ??? - type_name = L"000D0000"; - break; - default: - type_name = L"00000000"; - //assert_unhandled_case(data.content_type); - //return nullptr; - } - // Package root path: // content_root/title_id/type_name/ auto package_root = xe::join_paths( diff --git a/src/xenia/kernel/xam/content_manager.h b/src/xenia/kernel/xam/content_manager.h index 1099314e2..f8a33736b 100644 --- a/src/xenia/kernel/xam/content_manager.h +++ b/src/xenia/kernel/xam/content_manager.h @@ -65,6 +65,8 @@ class ContentManager { uint32_t content_type); ContentPackage* ResolvePackage(const XCONTENT_DATA& data); + std::wstring ResolvePackageRoot(uint32_t content_type, + uint32_t title_id = -1); bool ContentExists(const XCONTENT_DATA& data); X_RESULT CreateContent(std::string root_name, const XCONTENT_DATA& data); @@ -82,7 +84,6 @@ class ContentManager { private: uint32_t title_id(); - std::wstring ResolvePackageRoot(uint32_t content_type); std::wstring ResolvePackagePath(const XCONTENT_DATA& data); KernelState* kernel_state_; diff --git a/src/xenia/kernel/xam/user_profile.cc b/src/xenia/kernel/xam/user_profile.cc index d82d8b722..132b66097 100644 --- a/src/xenia/kernel/xam/user_profile.cc +++ b/src/xenia/kernel/xam/user_profile.cc @@ -20,9 +20,12 @@ #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/crypto_utils.h" #include "xenia/kernel/util/shim_utils.h" +#include "xenia/vfs/devices/host_path_device.h" DECLARE_int32(license_mask); +DEFINE_int64(profile_xuid, -1, "XUID of the profile to load in (E0...)", "XAM"); + namespace xe { namespace kernel { namespace xam { @@ -31,9 +34,18 @@ std::string X_XAMACCOUNTINFO::GetGamertagString() const { return xe::to_string(std::wstring(gamertag)); } -std::wstring UserProfile::directory() const { - return xe::to_absolute_path(kernel_state_->emulator()->content_root() + - L"\\profile\\"); +std::wstring UserProfile::path() const { + if (!profile_path_.empty()) { + return profile_path_; + } + + return base_path_; +} + +std::wstring UserProfile::path(uint64_t xuid) const { + wchar_t path[4096]; + swprintf_s(path, L"%s%llX", base_path_.c_str(), xuid); + return path; } bool UserProfile::DecryptAccountFile(const uint8_t* data, @@ -108,18 +120,75 @@ void UserProfile::EncryptAccountFile(const X_XAMACCOUNTINFO* input, UserProfile::UserProfile(KernelState* kernel_state) : kernel_state_(kernel_state), dash_gpd_(kDashboardID) { - account_.xuid_online = 0xE000BABEBABEBABE; + base_path_ = + xe::to_absolute_path(kernel_state_->content_manager()->ResolvePackageRoot( + 0x10000, 0xFFFE07D1)); + + account_.xuid_online = cvars::profile_xuid; wcscpy(account_.gamertag, L"XeniaUser"); // Try loading profile GPD files... LoadProfile(); } -void UserProfile::LoadProfile() { +std::wstring UserProfile::MountProfile(const std::wstring& path) { + auto package = path; + auto package_dir = package + L".dir\\"; + if (filesystem::PathExists(package_dir)) { + return package_dir; + } + + filesystem::FileInfo info; + // Get info for the original path too, in case we changed to the .dir above + if (!filesystem::GetInfo(package, &info)) { + return L""; + } + auto file_system = kernel_state_->file_system(); + + auto file_name = xe::to_string(info.name); + auto mount_path = "\\Device\\Profile_" + file_name; + + // If package still points to a file, open it as STFS and extract + if (info.type == filesystem::FileInfo::Type::kFile) { + XELOGI("MountProfile: extracting STFS profile %S", package.c_str()); + + // Register the container in the virtual filesystem. + auto device = + std::make_unique(mount_path, package); + if (!device->Initialize()) { + XELOGE( + "MountProfile: Unable to mount %S as STFS; file not found or " + "corrupt.", + package.c_str()); + return L""; + } + device->ExtractToFolder(package_dir); + return package_dir; + } + + return package + L"\\"; +} + +bool UserProfile::LoadProfile() { + auto profile_path = path(cvars::profile_xuid); + if (cvars::profile_xuid == -1) { + auto files = filesystem::ListFiles(path()); + // TODO: allow choosing by index maybe? + for (auto f : files) { + profile_path = f.path + f.name; + break; // use first found dir/package as profile + } + } + + profile_path_ = MountProfile(profile_path); + if (profile_path_.empty()) { + return false; + } + auto mmap_ = - MappedMemory::Open(directory() + L"Account", MappedMemory::Mode::kRead); + MappedMemory::Open(path() + L"Account", MappedMemory::Mode::kRead); if (mmap_) { - XELOGI("Loading Account file from path %SAccount", directory().c_str()); + XELOGI("Loading Account file from path %SAccount", path().c_str()); X_XAMACCOUNTINFO tmp_acct; bool success = DecryptAccountFile(mmap_->data(), &tmp_acct); @@ -137,10 +206,10 @@ void UserProfile::LoadProfile() { mmap_->Close(); } - XELOGI("Loading profile GPDs from path %S", directory().c_str()); + XELOGI("Loading profile GPDs from path %S", path().c_str()); - mmap_ = MappedMemory::Open(directory() + L"FFFE07D1.gpd", - MappedMemory::Mode::kRead); + mmap_ = + MappedMemory::Open(path() + L"FFFE07D1.gpd", MappedMemory::Mode::kRead); if (mmap_) { dash_gpd_.Read(mmap_->data(), mmap_->size()); mmap_->Close(); @@ -229,7 +298,7 @@ void UserProfile::LoadProfile() { for (auto title : titles) { wchar_t fname[256]; swprintf(fname, 256, L"%X.gpd", title.title_id); - mmap_ = MappedMemory::Open(directory() + fname, MappedMemory::Mode::kRead); + mmap_ = MappedMemory::Open(path() + fname, MappedMemory::Mode::kRead); if (!mmap_) { XELOGE("Failed to open GPD for title %X (%s)!", title.title_id, xe::to_string(title.title_name).c_str()); @@ -250,6 +319,7 @@ void UserProfile::LoadProfile() { } XELOGI("Loaded %d profile GPDs", title_gpds_.size()); + return true; } xdbf::GpdFile* UserProfile::SetTitleSpaData(const xdbf::SpaFile& spa_data) { @@ -535,16 +605,16 @@ bool UserProfile::UpdateGpd(uint32_t title_id, xdbf::GpdFile& gpd_data) { return false; } - if (!filesystem::PathExists(directory())) { - filesystem::CreateFolder(directory()); + if (!filesystem::PathExists(path())) { + filesystem::CreateFolder(path()); } wchar_t fname[256]; swprintf(fname, 256, L"%X.gpd", title_id); - filesystem::CreateFile(directory() + fname); + filesystem::CreateFile(path() + fname); auto mmap_ = MappedMemory::Open( - directory() + fname, MappedMemory::Mode::kReadWrite, 0, gpd_length); + path() + fname, MappedMemory::Mode::kReadWrite, 0, gpd_length); if (!mmap_) { XELOGE("Failed to open %X.gpd for writing!", title_id); return false; diff --git a/src/xenia/kernel/xam/user_profile.h b/src/xenia/kernel/xam/user_profile.h index 04f0726a3..1568b544b 100644 --- a/src/xenia/kernel/xam/user_profile.h +++ b/src/xenia/kernel/xam/user_profile.h @@ -175,7 +175,8 @@ class UserProfile { uint64_t xuid() const { return account_.xuid_online; } std::string name() const { return account_.GetGamertagString(); } - std::wstring directory() const; + std::wstring path() const; + std::wstring path(uint64_t xuid) const; // uint32_t signin_state() const { return 1; } xdbf::GpdFile* SetTitleSpaData(const xdbf::SpaFile& spa_data); @@ -188,13 +189,17 @@ class UserProfile { bool UpdateAllGpds(); private: - void LoadProfile(); + bool LoadProfile(); + std::wstring MountProfile(const std::wstring& path); + bool UpdateGpd(uint32_t title_id, xdbf::GpdFile& gpd_data); bool AddSettingIfNotExist(xdbf::Setting& setting); KernelState* kernel_state_; + std::wstring profile_path_; + std::wstring base_path_; X_XAMACCOUNTINFO account_; std::unordered_map title_gpds_; diff --git a/src/xenia/kernel/xam/xam_user.cc b/src/xenia/kernel/xam/xam_user.cc index 99edf63c5..1bb838211 100644 --- a/src/xenia/kernel/xam/xam_user.cc +++ b/src/xenia/kernel/xam/xam_user.cc @@ -781,7 +781,7 @@ dword_result_t XamReadTile(dword_t tile_type, dword_t game_id, qword_t item_id, if (kTileFileNames.count(type)) { // image_id = XUID of profile to retrieve from - auto file_path = kernel_state()->user_profile()->directory(); + auto file_path = kernel_state()->user_profile()->path(); file_path += kTileFileNames.at(type); mmap = MappedMemory::Open(file_path, MappedMemory::Mode::kRead); diff --git a/src/xenia/vfs/devices/stfs_container_device.cc b/src/xenia/vfs/devices/stfs_container_device.cc index 25f588e36..8b180e91d 100644 --- a/src/xenia/vfs/devices/stfs_container_device.cc +++ b/src/xenia/vfs/devices/stfs_container_device.cc @@ -777,5 +777,83 @@ bool StfsContainerDevice::ResolveFromFolder(const std::wstring& path) { return true; } +uint32_t StfsContainerDevice::ExtractToFolder(const std::wstring& base_path) { + XELOGD("Unpacking to %S", base_path.c_str()); + + // Create path if it doesn't exist + if (!filesystem::PathExists(base_path)) { + filesystem::CreateFolder(base_path); + } + + // Run through all the files, breadth-first style. + std::queue queue; + auto root = ResolvePath("/"); + queue.push(root); + + // Allocate a buffer when needed. + size_t buffer_size = 0; + uint8_t* buffer = nullptr; + uint32_t extracted = 0; + + while (!queue.empty()) { + auto entry = queue.front(); + queue.pop(); + for (auto& entry : entry->children()) { + queue.push(entry.get()); + } + + XELOGD(" %s", entry->path().c_str()); + auto dest_name = xe::join_paths(base_path, xe::to_wstring(entry->path())); + if (entry->attributes() & kFileAttributeDirectory) { + xe::filesystem::CreateFolder(dest_name + L"\\"); + continue; + } + + vfs::File* in_file = nullptr; + if (entry->Open(FileAccess::kFileReadData, &in_file) != X_STATUS_SUCCESS) { + continue; + } + + auto file = xe::filesystem::OpenFile(dest_name, "wb"); + if (!file) { + in_file->Destroy(); + continue; + } + + if (entry->can_map()) { + auto map = entry->OpenMapped(xe::MappedMemory::Mode::kRead); + fwrite(map->data(), map->size(), 1, file); + map->Close(); + } else { + // Can't map the file into memory. Read it into a temporary buffer. + if (!buffer || entry->size() > buffer_size) { + // Resize the buffer. + if (buffer) { + delete[] buffer; + } + + // Allocate a buffer rounded up to the nearest 512MB. + buffer_size = xe::round_up(entry->size(), 512 * 1024 * 1024); + buffer = new uint8_t[buffer_size]; + } + + size_t bytes_read = 0; + in_file->ReadSync(buffer, entry->size(), 0, &bytes_read); + fwrite(buffer, bytes_read, 1, file); + } + + extracted++; + + fclose(file); + in_file->Destroy(); + } + + if (buffer) { + delete[] buffer; + } + + return extracted; +} + } // namespace vfs } // namespace xe \ No newline at end of file diff --git a/src/xenia/vfs/devices/stfs_container_device.h b/src/xenia/vfs/devices/stfs_container_device.h index 89e76c231..e77674a3a 100644 --- a/src/xenia/vfs/devices/stfs_container_device.h +++ b/src/xenia/vfs/devices/stfs_container_device.h @@ -185,6 +185,8 @@ class StfsContainerDevice : public Device { StfsHeader& header() { return header_; } + uint32_t ExtractToFolder(const std::wstring& dest_path); + private: enum class Error { kSuccess = 0,