[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 <path>.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.
This commit is contained in:
emoose 2019-12-26 07:22:24 +00:00 committed by Gliniak
parent 41b94145e2
commit 12674aa529
8 changed files with 181 additions and 46 deletions

View File

@ -48,12 +48,13 @@ KernelState::KernelState(Emulator* emulator)
file_system_ = emulator->file_system();
app_manager_ = std::make_unique<xam::AppManager>();
user_profile_ = std::make_unique<xam::UserProfile>(this);
auto content_root = emulator_->content_root();
content_root = xe::to_absolute_path(content_root);
content_manager_ = std::make_unique<xam::ContentManager>(this, content_root);
user_profile_ = std::make_unique<xam::UserProfile>(this);
assert_null(shared_kernel_state_);
shared_kernel_state_ = this;

View File

@ -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(

View File

@ -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_;

View File

@ -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<vfs::StfsContainerDevice>(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;

View File

@ -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<uint32_t, xdbf::GpdFile> title_gpds_;

View File

@ -786,7 +786,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);

View File

@ -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<vfs::Entry*> 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

View File

@ -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,