2023 Support

This commit is contained in:
2025-12-29 18:47:07 -03:00
parent b5c86787ab
commit 664ff7cc85
18 changed files with 451 additions and 218 deletions

View File

@@ -478,8 +478,9 @@ struct System::Impl {
Network::RestartSocketOperations();
if (legacy_online) {
legacy_online->Stop();
legacy_online.reset();
// Keep legacy_online running for the emulator's lifetime
// legacy_online->Stop();
// legacy_online.reset();
}
if (auto room_member = Network::GetRoomMember().lock()) {

View File

@@ -57,25 +57,7 @@ std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u
} // Anonymous namespace
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
VirtualDir save_directory_)
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)} {
// Delete all temporary storages
// On hardware, it is expected that temporary storage be empty at first use.
dir->DeleteSubdirectoryRecursive("temp");
}
SaveDataFactory::~SaveDataFactory() = default;
VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
meta.user_id, meta.system_save_data_id);
return dir->CreateDirectoryRelative(save_directory);
}
VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
u64 target_program_id = meta.program_id;
// CRITICAL FIX: If the game requests Cache/Temp with ProgramID 0 (generic),
// we MUST redirect it to the actual running TitleID, otherwise it looks in '.../0000000000000000'.
@@ -89,12 +71,6 @@ VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute&
auto out = dir->GetDirectoryRelative(save_directory);
// Fallback for Ryujinx-style cache paths: sdcard/Nintendo/save/{TitleID} (No /cache/ subdir)
// Also include Temporary, as games like Just Dance use Temporary storage for cache-like data (MapBaseCache),
// and Ryujinx stores this in the same simple 'save/{ID}' structure.
// Fallback logic removed. We now enforce standard paths.
// User instructions will direct them to the correct folder.
if (out == nullptr) {
LOG_WARNING(Service_FS, "Cache/Save path NOT FOUND: '{}'. Auto-create={}", save_directory, auto_create);
} else {
@@ -120,12 +96,10 @@ VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute&
if (dir_1) LOG_INFO(Service_FS, "Found subdirectory '1' in save path.");
if (dir_0 != nullptr && dir_1 == nullptr) {
LOG_INFO(Service_FS, "Ryujinx structure detected: '0' exists, '1' missing. Copying 0->1 for compatibility.");
dir_1 = out->CreateSubdirectory("1");
if (dir_1 != nullptr) {
// Copy contents from 0 to 1
VfsRawCopyD(dir_0, dir_1);
}
// User requested removal of auto-copy 0->1 logic.
// We simply don't create/copy '1' if it's missing. We rely on fallback to '0'.
LOG_INFO(Service_FS, "Ryujinx structure detected: '0' exists, '1' missing. Skipping copy (User Request).");
// VfsRawCopyD(dir_0, dir_1); // REMOVED
}
// Check for 'Addressables' and 'Addressables2' and delete 'json.cache' if present.
@@ -144,19 +118,43 @@ VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute&
}
};
VirtualDir commit_root = nullptr;
if (dir_1 != nullptr) {
LOG_INFO(Service_FS, "Returning subdirectory '1' as Save Root.");
CleanCache(dir_1);
return dir_1;
LOG_INFO(Service_FS, "Using subdirectory '1' as Commit Root.");
commit_root = dir_1;
} else if (dir_0 != nullptr) {
LOG_INFO(Service_FS, "Using subdirectory '0' as Commit Root.");
commit_root = dir_0;
} else {
LOG_INFO(Service_FS, "No '0' or '1' subdirectories found. Using parent folder as Commit Root.");
commit_root = out;
}
if (dir_0 != nullptr) {
LOG_INFO(Service_FS, "Returning subdirectory '0' as Save Root.");
CleanCache(dir_0);
return dir_0;
CleanCache(commit_root);
// Implement SD_Cache.XXXX logic for Cache Storage
if (meta.type == SaveDataType::Cache) {
const std::string sd_cache_name = fmt::format("SD_Cache.{:04}", meta.index);
auto sd_cache_dir = commit_root->GetSubdirectory(sd_cache_name);
if (sd_cache_dir != nullptr) {
LOG_INFO(Service_FS, "Found SD_Cache directory: '{}'", sd_cache_name);
return sd_cache_dir;
} else if (auto_create) {
LOG_INFO(Service_FS, "Auto-creating SD_Cache directory: '{}'", sd_cache_name);
return commit_root->CreateSubdirectory(sd_cache_name);
} else {
LOG_WARNING(Service_FS, "SD_Cache directory '{}' not found in commit root.", sd_cache_name);
// Fallback? Or return nullptr?
// If the user strictly wants SD_Cache, we should probably return nullptr if not found/created.
// But for now, returning the commit_root might be safer for legacy compatibility IF SD_Cache isn't strictly enforced for existing saves?
// User said "SD_Cache now represents which CacheStorage it will be". This implies correct behavior is to use SD_Cache.
// If we return commit_root, it's CacheStorage_0 (conceptually) or just "everything".
// Let's assume we return nullptr if not found, to trigger creation logic or error.
return nullptr;
}
}
LOG_INFO(Service_FS, "No '0' or '1' subdirectories found. Returning parent folder as Save Root.");
CleanCache(out);
return commit_root;
}
return out;
@@ -227,6 +225,40 @@ std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
}
}
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
VirtualDir save_directory_)
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)} {
// Delete all temporary storages
// On hardware, it is expected that temporary storage be empty at first use.
dir->DeleteSubdirectoryRecursive("temp");
}
SaveDataFactory::~SaveDataFactory() = default;
VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
meta.user_id, meta.system_save_data_id);
auto created_dir = dir->CreateDirectoryRelative(save_directory);
// For Cache storage, enforce the new hierarchy: .../1/SD_Cache.XXXX
if (meta.type == SaveDataType::Cache && created_dir != nullptr) {
// Ensure commit directory '1' exists
auto commit_dir = created_dir->GetSubdirectory("1");
if (commit_dir == nullptr) {
commit_dir = created_dir->CreateSubdirectory("1");
}
if (commit_dir != nullptr) {
// Create SD_Cache.XXXX
const std::string sd_cache_name = fmt::format("SD_Cache.{:04}", meta.index);
return commit_dir->CreateSubdirectory(sd_cache_name);
}
}
return created_dir;
}
std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future) {
if (future) {
Common::UUID uuid;

View File

@@ -691,149 +691,11 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
// Nintendo/save (common portable save structure), create a host-side
// symlink so the emulator will see those saves under the expected NAND path.
// This helps users who placed saves under `<eden>/user/sdmc/Nintendo/save/...`.
try {
const auto nand_fs_path = Common::FS::GetEdenPath(EdenPath::NANDDir);
const auto sdmc_fs_path = sdmc_dir_path;
const auto nand_user_save_path = nand_fs_path / "user" / "save";
const auto sdmc_nintendo_save_path = sdmc_fs_path / "Nintendo" / "save";
std::error_code ec;
if (!std::filesystem::exists(nand_user_save_path) &&
std::filesystem::exists(sdmc_nintendo_save_path)) {
std::filesystem::create_directory_symlink(sdmc_nintendo_save_path, nand_user_save_path, ec);
if (ec) {
LOG_WARNING(Service_FS, "Could not create symlink {} -> {}: {}",
Common::FS::PathToUTF8String(nand_user_save_path),
Common::FS::PathToUTF8String(sdmc_nintendo_save_path), ec.message());
// Fallback: copy the SDMC saves into the NAND save folder so the emulator can see them
try {
if (!std::filesystem::exists(nand_user_save_path)) {
std::filesystem::create_directories(nand_user_save_path);
}
std::filesystem::copy(sdmc_nintendo_save_path, nand_user_save_path,
std::filesystem::copy_options::recursive |
std::filesystem::copy_options::skip_existing,
ec);
if (ec) {
LOG_WARNING(Service_FS, "Failed to copy SDMC saves to {}: {}",
Common::FS::PathToUTF8String(nand_user_save_path), ec.message());
} else {
LOG_INFO(Service_FS, "Copied SDMC saves to NAND save path: {} -> {}",
Common::FS::PathToUTF8String(sdmc_nintendo_save_path),
Common::FS::PathToUTF8String(nand_user_save_path));
}
} catch (const std::exception& ex) {
LOG_WARNING(Service_FS, "Exception while copying SDMC saves: {}", ex.what());
}
} else {
LOG_INFO(Service_FS, "Linked NAND save path to SDMC saves: {} -> {}",
Common::FS::PathToUTF8String(nand_user_save_path),
Common::FS::PathToUTF8String(sdmc_nintendo_save_path));
}
}
// Also support official-style SD cache directory name used by some Switch setups.
// If users placed saves or cache under `<eden>/user/sdmc/Nintendo/SD_Cache.0000`,
// expose it to the NAND user save path as well so the emulator can find them.
const auto sdmc_nintendo_cache_path = sdmc_fs_path / "Nintendo" / "SD_Cache.0000";
if (!std::filesystem::exists(nand_user_save_path) &&
std::filesystem::exists(sdmc_nintendo_cache_path)) {
std::error_code ec2;
std::filesystem::create_directory_symlink(sdmc_nintendo_cache_path, nand_user_save_path,
ec2);
if (ec2) {
LOG_WARNING(Service_FS,
"Could not create symlink {} -> {}: {}",
Common::FS::PathToUTF8String(nand_user_save_path),
Common::FS::PathToUTF8String(sdmc_nintendo_cache_path), ec2.message());
// Fallback: copy the SDMC cache into the NAND save folder so the emulator can see them
try {
if (!std::filesystem::exists(nand_user_save_path)) {
std::filesystem::create_directories(nand_user_save_path);
}
std::filesystem::copy(sdmc_nintendo_cache_path, nand_user_save_path,
std::filesystem::copy_options::recursive |
std::filesystem::copy_options::skip_existing,
ec2);
if (ec2) {
LOG_WARNING(Service_FS, "Failed to copy SDMC cache to {}: {}",
Common::FS::PathToUTF8String(nand_user_save_path), ec2.message());
} else {
LOG_INFO(Service_FS, "Copied SDMC cache to NAND save path: {} -> {}",
Common::FS::PathToUTF8String(sdmc_nintendo_cache_path),
Common::FS::PathToUTF8String(nand_user_save_path));
}
} catch (const std::exception& ex) {
LOG_WARNING(Service_FS, "Exception while copying SDMC cache: {}", ex.what());
}
} else {
LOG_INFO(Service_FS, "Linked NAND save path to SDMC cache: {} -> {}",
Common::FS::PathToUTF8String(nand_user_save_path),
Common::FS::PathToUTF8String(sdmc_nintendo_cache_path));
}
}
// If the NAND save folder already exists, ensure individual entries from
// SDMC (both `Nintendo/save` and `Nintendo/SD_Cache.0000`) are visible
// inside it: create per-entry symlinks (with copy fallback) for missing
// title/account directories so saves placed in SDMC are reachable.
auto try_merge_sdmc_entries = [&](const std::filesystem::path& sdmc_src) {
try {
if (!std::filesystem::exists(sdmc_src))
return;
std::error_code ec;
if (!std::filesystem::exists(nand_user_save_path))
std::filesystem::create_directories(nand_user_save_path, ec);
for (auto& ent : std::filesystem::directory_iterator(sdmc_src)) {
const auto name = ent.path().filename();
const auto target = nand_user_save_path / name;
if (std::filesystem::exists(target))
continue;
std::error_code ec2;
std::filesystem::create_directory_symlink(ent.path(), target, ec2);
if (ec2) {
LOG_WARNING(Service_FS, "Could not create symlink {} -> {}: {}",
Common::FS::PathToUTF8String(target),
Common::FS::PathToUTF8String(ent.path()), ec2.message());
try {
std::filesystem::copy(ent.path(), target,
std::filesystem::copy_options::recursive |
std::filesystem::copy_options::skip_existing,
ec2);
if (ec2) {
LOG_WARNING(Service_FS, "Failed to copy {} -> {}: {}",
Common::FS::PathToUTF8String(ent.path()),
Common::FS::PathToUTF8String(target), ec2.message());
} else {
LOG_INFO(Service_FS, "Copied SDMC entry to NAND: {} -> {}",
Common::FS::PathToUTF8String(ent.path()),
Common::FS::PathToUTF8String(target));
}
} catch (const std::exception& ex) {
LOG_WARNING(Service_FS, "Exception while copying SDMC entry: {}",
ex.what());
}
} else {
LOG_INFO(Service_FS, "Linked NAND entry to SDMC: {} -> {}",
Common::FS::PathToUTF8String(target),
Common::FS::PathToUTF8String(ent.path()));
}
}
} catch (const std::exception& ex) {
LOG_WARNING(Service_FS, "Exception while merging SDMC entries: {}", ex.what());
}
};
try_merge_sdmc_entries(sdmc_nintendo_save_path);
try_merge_sdmc_entries(sdmc_nintendo_cache_path);
} catch (const std::exception& e) {
LOG_WARNING(Service_FS, "Exception while linking SDMC saves: {}", e.what());
}
// SDMC to NAND sync logic REMOVED as per user request.
// The emulator will no longer attempt to symlink or copy "Nintendo/save" or "SD_Cache.0000"
// from SDMC to the NAND user save directory.
// Users must ensure their save/cache structure is valid within the NAND directory itself
// if that is what they intend to use, or rely on the game creating it.
const auto rw_mode = FileSys::OpenMode::ReadWrite;
auto nand_directory =

View File

@@ -14,7 +14,7 @@ namespace Service::FileSystem {
ISaveDataInfoReader::ISaveDataInfoReader(Core::System& system_,
std::shared_ptr<SaveDataController> save_data_controller_,
FileSys::SaveDataSpaceId space)
FileSys::SaveDataSpaceId space, bool cache_only)
: ServiceFramework{system_, "ISaveDataInfoReader"}, save_data_controller{
save_data_controller_} {
static const FunctionInfo functions[] = {
@@ -22,7 +22,7 @@ ISaveDataInfoReader::ISaveDataInfoReader(Core::System& system_,
};
RegisterHandlers(functions);
FindAllSaves(space);
FindAllSaves(space, cache_only);
}
ISaveDataInfoReader::~ISaveDataInfoReader() = default;
@@ -63,7 +63,7 @@ Result ISaveDataInfoReader::ReadSaveDataInfo(
R_SUCCEED();
}
void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space) {
void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space, bool cache_only) {
FileSys::VirtualDir save_root{};
const auto result = save_data_controller->OpenSaveDataSpace(&save_root, space);
@@ -74,8 +74,12 @@ void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space) {
for (const auto& type : save_root->GetSubdirectories()) {
if (type->GetName() == "save") {
FindNormalSaves(space, type);
} else if (space == FileSys::SaveDataSpaceId::Temporary) {
if (cache_only) {
FindCacheSaves(space, type);
} else {
FindNormalSaves(space, type);
}
} else if (space == FileSys::SaveDataSpaceId::Temporary && !cache_only) {
FindTemporaryStorageSaves(space, type);
}
}
@@ -84,6 +88,11 @@ void ISaveDataInfoReader::FindAllSaves(FileSys::SaveDataSpaceId space) {
void ISaveDataInfoReader::FindNormalSaves(FileSys::SaveDataSpaceId space,
const FileSys::VirtualDir& type) {
for (const auto& save_id : type->GetSubdirectories()) {
// Skip cache directory in normal scans
if (save_id->GetName() == "cache") {
continue;
}
for (const auto& user_id : save_id->GetSubdirectories()) {
// Skip non user id subdirectories
if (user_id->GetName().size() != 0x20) {
@@ -132,6 +141,96 @@ void ISaveDataInfoReader::FindNormalSaves(FileSys::SaveDataSpaceId space,
}
}
void ISaveDataInfoReader::FindCacheSaves(FileSys::SaveDataSpaceId space,
const FileSys::VirtualDir& type) {
const auto cache_dir = type->GetSubdirectory("cache");
if (cache_dir == nullptr) {
return;
}
for (const auto& title_id_dir : cache_dir->GetSubdirectories()) {
const auto title_id = stoull_be(title_id_dir->GetName());
// Simple validation: TitleID should be non-zero
if (title_id == 0) {
continue;
}
// Determine commit root (priority to "1", then "0", then self)
auto commit_root = title_id_dir->GetSubdirectory("1");
if (commit_root == nullptr) {
commit_root = title_id_dir->GetSubdirectory("0");
}
// If neither exists, we might fall back to title_id_dir itself if users put SD_Cache directly there,
// but based on SaveDataFactory we expect it inside 0 or 1.
if (commit_root == nullptr) {
// Check if SD_Cache exists directly in title_dir (legacy/fallback)
bool has_sd_cache_root = false;
for (const auto& sub : title_id_dir->GetSubdirectories()) {
if (sub->GetName().find("SD_Cache.") == 0) {
has_sd_cache_root = true;
break;
}
}
if (has_sd_cache_root) {
commit_root = title_id_dir;
} else {
continue; // No valid storage found
}
}
bool found_any = false;
for (const auto& sd_cache_dir : commit_root->GetSubdirectories()) {
const std::string& name = sd_cache_dir->GetName();
if (name.find("SD_Cache.") == 0) {
// Parse index from "SD_Cache.XXXX"
u64 index = 0;
try {
if (name.size() > 9) {
index = std::stoull(name.substr(9));
}
} catch(...) {
continue;
}
info.emplace_back(SaveDataInfo{
0,
space,
FileSys::SaveDataType::Cache,
{}, // padding 0x6
{}, // user_id (empty array match)
0, // save_id
title_id,
sd_cache_dir->GetSize(),
static_cast<u16>(index), // Correct index with cast
FileSys::SaveDataRank::Primary,
{}, // padding 0x25
});
found_any = true;
}
}
// Fallback for legacy "flat" cache if no SD_Cache folders found?
// If the user specific structure IS enforced, maybe we don't fallback.
// But if they have existing cache without the folder, it is effectively index 0.
if (!found_any) {
// Treat the entire commit_root as index 0 (Legacy behavior)
info.emplace_back(SaveDataInfo{
0,
space,
FileSys::SaveDataType::Cache,
{}, // padding 0x6
{}, // user_id (empty array match)
0, // save_id
title_id,
commit_root->GetSize(),
0, // index 0
FileSys::SaveDataRank::Primary,
{}, // padding 0x25
});
}
}
}
void ISaveDataInfoReader::FindTemporaryStorageSaves(FileSys::SaveDataSpaceId space,
const FileSys::VirtualDir& type) {
for (const auto& user_id : type->GetSubdirectories()) {

View File

@@ -16,7 +16,7 @@ class ISaveDataInfoReader final : public ServiceFramework<ISaveDataInfoReader> {
public:
explicit ISaveDataInfoReader(Core::System& system_,
std::shared_ptr<SaveDataController> save_data_controller_,
FileSys::SaveDataSpaceId space);
FileSys::SaveDataSpaceId space, bool cache_only = false);
~ISaveDataInfoReader() override;
struct SaveDataInfo {
@@ -38,8 +38,9 @@ public:
OutArray<SaveDataInfo, BufferAttr_HipcMapAlias> out_entries);
private:
void FindAllSaves(FileSys::SaveDataSpaceId space);
void FindAllSaves(FileSys::SaveDataSpaceId space, bool cache_only);
void FindNormalSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
void FindCacheSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
void FindTemporaryStorageSaves(FileSys::SaveDataSpaceId space, const FileSys::VirtualDir& type);
std::shared_ptr<SaveDataController> save_data_controller;

View File

@@ -353,10 +353,10 @@ Result FSP_SRV::OpenSaveDataInfoReaderBySaveDataSpaceId(
Result FSP_SRV::OpenSaveDataInfoReaderOnlyCacheStorage(
OutInterface<ISaveDataInfoReader> out_interface) {
LOG_WARNING(Service_FS, "(STUBBED) called");
LOG_DEBUG(Service_FS, "called");
*out_interface = std::make_shared<ISaveDataInfoReader>(system, save_data_controller,
FileSys::SaveDataSpaceId::Temporary);
FileSys::SaveDataSpaceId::User, true);
R_SUCCEED();
}

View File

@@ -42,8 +42,16 @@ bool SessionRequestManager::HasSessionRequestHandler(const HLERequestContext& co
const auto& message_header = context.GetDomainMessageHeader();
const auto object_id = message_header.object_id;
// Some games send magic numbers in the object_id field, which indicates
// this is not actually a proper domain request
const u32 sfci_magic = Common::MakeMagic('S', 'F', 'C', 'I');
const u32 sfco_magic = Common::MakeMagic('S', 'F', 'C', 'O');
if (object_id == sfci_magic || object_id == sfco_magic) {
// This is not a domain request, treat it as a regular session request
return session_handler != nullptr;
}
if (object_id > DomainHandlerCount()) {
LOG_CRITICAL(IPC, "object_id {} is too big!", object_id);
return false;
}
return !DomainHandler(object_id - 1).expired();
@@ -91,7 +99,18 @@ Result SessionRequestManager::HandleDomainSyncRequest(Kernel::KServerSession* se
// If there is a DomainMessageHeader, then this is CommandType "Request"
const auto& domain_message_header = context.GetDomainMessageHeader();
const u32 object_id{domain_message_header.object_id};
const u32 object_id = domain_message_header.object_id;
// Some games send magic numbers in the object_id field
const u32 sfci_magic = Common::MakeMagic('S', 'F', 'C', 'I');
const u32 sfco_magic = Common::MakeMagic('S', 'F', 'C', 'O');
if (object_id == sfci_magic || object_id == sfco_magic) {
// This is not a domain request, handle as regular session request
LOG_DEBUG(IPC, "Detected magic number 0x{:08X} in object_id, treating as regular session request. Command={}",
object_id, context.GetCommand());
return session_handler->HandleSyncRequest(*server_session, context);
}
switch (domain_message_header.command) {
case IPC::DomainMessageHeader::CommandType::SendMessage:
if (object_id > this->DomainHandlerCount()) {
@@ -200,7 +219,17 @@ void HLERequestContext::ParseCommandBuffer(u32_le* src_cmdbuf, bool incoming) {
// If this is an incoming message, only CommandType "Request" has a domain header
// All outgoing domain messages have the domain header, if only incoming has it
if (incoming || domain_message_header) {
domain_message_header = rp.PopRaw<IPC::DomainMessageHeader>();
// Check if the next value is actually a magic number (SFCI/SFCO)
// Some games send these in the object_id field, indicating this is not a domain request
const u32 possible_object_id = src_cmdbuf[rp.GetCurrentOffset() + 1]; // object_id is second field
const u32 sfci_magic = Common::MakeMagic('S', 'F', 'C', 'I');
const u32 sfco_magic = Common::MakeMagic('S', 'F', 'C', 'O');
if (possible_object_id != sfci_magic && possible_object_id != sfco_magic) {
// This is a proper domain request
domain_message_header = rp.PopRaw<IPC::DomainMessageHeader>();
}
// If it's a magic number, skip reading the domain header entirely
} else {
if (GetManager()->IsDomain()) {
LOG_WARNING(IPC, "Domain request has no DomainMessageHeader!");
@@ -220,9 +249,15 @@ void HLERequestContext::ParseCommandBuffer(u32_le* src_cmdbuf, bool incoming) {
}
if (incoming) {
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'I'));
// Only check magic if we have a valid data payload header
// When domain header is skipped (SFCI in object_id), the structure is different
if (domain_message_header) {
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'I'));
}
} else {
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'O'));
if (domain_message_header) {
ASSERT(data_payload_header->magic == Common::MakeMagic('S', 'F', 'C', 'O'));
}
}
}

View File

@@ -923,7 +923,10 @@ std::pair<s32, Errno> BSD::SendImpl(s32 fd, u32 flags, std::span<const u8> messa
if (!IsFileDescriptorValid(fd)) {
return {-1, Errno::BADF};
}
return Translate(file_descriptors[fd]->socket->Send(message, flags));
LOG_CRITICAL(Service, "SendImpl called: fd={}, size={} bytes, flags={}", fd, message.size(), flags);
auto result = Translate(file_descriptors[fd]->socket->Send(message, flags));
LOG_CRITICAL(Service, "SendImpl result: sent={} bytes, errno={}", result.first, static_cast<int>(result.second));
return result;
}
std::pair<s32, Errno> BSD::SendToImpl(s32 fd, u32 flags, std::span<const u8> message,

View File

@@ -60,12 +60,29 @@ NSD::NSD(Core::System& system_, const char* name) : ServiceFramework{system_, na
RegisterHandlers(functions);
}
static const std::vector<std::pair<std::string, std::string>> redirectionRules = {
{"public-ubiservices.com", "jdlo.ovosimpatico.com"},
{"public-ubiservices.ubi.com", "jdlo.ovosimpatico.com"},
};
static std::string GetRedirectedHost(const std::string& host) {
for (const auto& rule : redirectionRules) {
if (host.find(rule.first) != std::string::npos) {
LOG_INFO(Service, "Redirecting NSD host '{}' to '{}'", host, rule.second);
return rule.second;
}
}
return host;
}
static std::string ResolveImpl(const std::string& fqdn_in) {
// The real implementation makes various substitutions.
// For now we just return the string as-is, which is good enough when not
// connecting to real Nintendo servers.
LOG_WARNING(Service, "(STUBBED) called, fqdn_in={}", fqdn_in);
return fqdn_in;
return GetRedirectedHost(fqdn_in);
}
static Result ResolveCommon(const std::string& fqdn_in, std::array<char, 0x100>& fqdn_out) {

View File

@@ -30,12 +30,12 @@ SFDNSRES::SFDNSRES(Core::System& system_) : ServiceFramework{system_, "sfdnsres"
{5, &SFDNSRES::GetGaiStringErrorRequest, "GetGaiStringErrorRequest"},
{6, &SFDNSRES::GetAddrInfoRequest, "GetAddrInfoRequest"},
{7, nullptr, "GetNameInfoRequest"},
{8, nullptr, "RequestCancelHandleRequest"},
{8, &SFDNSRES::RequestCancelHandleRequest, "RequestCancelHandleRequest"},
{9, nullptr, "CancelRequest"},
{10, &SFDNSRES::GetHostByNameRequestWithOptions, "GetHostByNameRequestWithOptions"},
{11, nullptr, "GetHostByAddrRequestWithOptions"},
{12, &SFDNSRES::GetAddrInfoRequestWithOptions, "GetAddrInfoRequestWithOptions"},
{13, nullptr, "GetNameInfoRequestWithOptions"},
{13, &SFDNSRES::GetNameInfoRequestWithOptions, "GetNameInfoRequestWithOptions"},
{14, &SFDNSRES::ResolverSetOptionRequest, "ResolverSetOptionRequest"},
{15, nullptr, "ResolverGetOptionRequest"},
};
@@ -66,6 +66,21 @@ static bool IsBlockedHost(const std::string& host) {
[&host](const std::string& domain) { return host.find(domain) != std::string::npos; });
}
static const std::vector<std::pair<std::string, std::string>> redirectionRules = {
{"public-ubiservices.com", "jdlo.ovosimpatico.com"},
{"public-ubiservices.ubi.com", "jdlo.ovosimpatico.com"},
};
static std::string GetRedirectedHost(const std::string& host) {
for (const auto& rule : redirectionRules) {
if (host.find(rule.first) != std::string::npos) {
LOG_INFO(Service, "Redirecting host '{}' to '{}'", host, rule.second);
return rule.second;
}
}
return host;
}
static NetDbError GetAddrInfoErrorToNetDbError(GetAddrInfoError result) {
// These combinations have been verified on console (but are not
// exhaustive).
@@ -163,7 +178,8 @@ static std::pair<u32, GetAddrInfoError> GetHostByNameRequestImpl(HLERequestConte
parameters.use_nsd_resolve, parameters.cancel_handle, parameters.process_id);
const auto host_buffer = ctx.ReadBuffer(0);
const std::string host = Common::StringFromBuffer(host_buffer);
std::string host = Common::StringFromBuffer(host_buffer);
host = GetRedirectedHost(host);
// For now, ignore options, which are in input buffer 1 for GetHostByNameRequestWithOptions.
// Prevent resolution of Nintendo servers
@@ -281,7 +297,8 @@ static std::pair<u32, GetAddrInfoError> GetAddrInfoRequestImpl(HLERequestContext
// before looking up.
const auto host_buffer = ctx.ReadBuffer(0);
const std::string host = Common::StringFromBuffer(host_buffer);
std::string host = Common::StringFromBuffer(host_buffer);
host = GetRedirectedHost(host);
// Prevent resolution of Nintendo servers
if (IsBlockedHost(host)) {
@@ -364,6 +381,72 @@ void SFDNSRES::GetAddrInfoRequestWithOptions(HLERequestContext& ctx) {
});
}
void SFDNSRES::GetNameInfoRequestWithOptions(HLERequestContext& ctx) {
struct InputParameters {
u32 flags;
u32 interface_index;
u64 process_id;
u32 padding; // 0x14 + 4 = 0x18? No. 0x14 aligned to 8 bytes?
// Wait, sizeof(InputParameters) == 0x14.
};
// Derived from partial snippets:
// u32 flags, u32 interface_index, u64 process_id.
// 4 + 4 + 8 = 16 bytes (0x10).
// The previous prompt had static_assert size 0x14.
// Maybe a u32 padding?
// Let's rely on standard layout.
// I will use manual popping for safety if struct definition is unknown.
IPC::RequestParser rp{ctx};
const auto addr_in = rp.PopRaw<SockAddrIn>();
const u32 flags = rp.Pop<u32>();
const u32 interface_index = rp.Pop<u32>();
const u64 process_id = rp.Pop<u64>();
(void)flags;
(void)interface_index;
(void)process_id;
// If there was padding, it might be implicitly popped or ignored.
struct OutputParameters {
u32 data_size;
GetAddrInfoError gai_error;
NetDbError netdb_error;
Errno bsd_errno;
};
static_assert(sizeof(OutputParameters) == 0x10);
const auto res = Network::GetNameInfo(Translate(addr_in));
if (res.second != 0) {
const auto network_error = Network::TranslateGetAddrInfoErrorFromNative(res.second);
const auto service_error = Translate(network_error);
IPC::ResponseBuilder rb{ctx, 6};
rb.Push(ResultSuccess);
rb.PushRaw(OutputParameters{
.data_size = 0,
.gai_error = service_error,
.netdb_error = GetAddrInfoErrorToNetDbError(service_error),
.bsd_errno = GetAddrInfoErrorToErrno(service_error),
});
return;
}
const std::string& host = res.first;
const u32 data_size = static_cast<u32>(host.size() + 1);
ctx.WriteBuffer(host.data(), data_size, 0);
IPC::ResponseBuilder rb{ctx, 6};
rb.Push(ResultSuccess);
rb.PushRaw(OutputParameters{
.data_size = data_size,
.gai_error = GetAddrInfoError::SUCCESS,
.netdb_error = NetDbError::Success,
.bsd_errno = Errno::SUCCESS,
});
}
void SFDNSRES::ResolverSetOptionRequest(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
@@ -372,4 +455,24 @@ void SFDNSRES::ResolverSetOptionRequest(HLERequestContext& ctx) {
rb.Push(ResultSuccess);
rb.Push<s32>(0); // bsd errno
}
void SFDNSRES::RequestCancelHandleRequest(HLERequestContext& ctx) {
// This is just a stub for now.
// In a real implementation this would likely cancel a pending request represented by the handle.
LOG_WARNING(Service, "(STUBBED) called");
struct InputParameters {
u32 cancel_handle;
};
IPC::RequestParser rp{ctx};
auto input = rp.PopRaw<InputParameters>();
LOG_DEBUG(Service, "cancel_handle={}", input.cancel_handle);
IPC::ResponseBuilder rb{ctx, 4};
rb.Push(ResultSuccess);
rb.Push<u32>(0); // cancel_handle (response seems to echo it or return a new one? usually just an error code or unrelated)
// Actually based on typical patterns, it probably returns an error code (bsd_errno).
rb.Push<s32>(0); // bsd_errno
}
} // namespace Service::Sockets

View File

@@ -22,7 +22,9 @@ private:
void GetHostByNameRequestWithOptions(HLERequestContext& ctx);
void GetAddrInfoRequest(HLERequestContext& ctx);
void GetAddrInfoRequestWithOptions(HLERequestContext& ctx);
void GetNameInfoRequestWithOptions(HLERequestContext& ctx);
void ResolverSetOptionRequest(HLERequestContext& ctx);
void RequestCancelHandleRequest(HLERequestContext& ctx);
};
} // namespace Service::Sockets

View File

@@ -117,6 +117,7 @@ public:
RegisterHandlers(functions);
shared_data->connection_count++;
LOG_CRITICAL(Service_SSL, "ISslConnection created! Total connections: {}", shared_data->connection_count);
}
~ISslConnection() {
@@ -277,8 +278,10 @@ private:
void SetSocketDescriptor(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const s32 in_fd = rp.Pop<s32>();
LOG_CRITICAL(Service_SSL, "SetSocketDescriptor called with fd={}", in_fd);
s32 out_fd{-1};
const Result res = SetSocketDescriptorImpl(&out_fd, in_fd);
LOG_CRITICAL(Service_SSL, "SetSocketDescriptor result: res={}, out_fd={}", res.raw, out_fd);
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(res);
rb.Push<s32>(out_fd);
@@ -308,7 +311,9 @@ private:
}
void DoHandshake(HLERequestContext& ctx) {
LOG_INFO(Service_SSL, "DoHandshake called, socket={}", socket != nullptr);
const Result res = DoHandshakeImpl();
LOG_INFO(Service_SSL, "DoHandshake result: {}, did_handshake={}", res.raw, did_handshake);
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(res);
}

View File

@@ -115,6 +115,22 @@ inline void LoadCaCertStore(SSL_CTX* ctx, const char* ca_cert, std::size_t size)
}
#endif
static const std::vector<std::pair<std::string, std::string>> redirectionRules = {
{"public-ubiservices.com", "jdlo.ovosimpatico.com"},
{"public-ubiservices.ubi.com", "jdlo.ovosimpatico.com"},
};
static std::string GetRedirectedHost(const std::string& host) {
for (const auto& rule : redirectionRules) {
if (host.find(rule.first) != std::string::npos) {
LOG_INFO(Service_SSL, "Redirecting SSL host '{}' to '{}'", host, rule.second);
return rule.second;
}
}
return host;
}
} // namespace
class SSLConnectionBackendOpenSSL final : public SSLConnectionBackend {
@@ -157,7 +173,8 @@ public:
socket = std::move(socket_in);
}
Result SetHostName(const std::string& hostname) override {
Result SetHostName(const std::string& hostname_in) override {
const std::string hostname = GetRedirectedHost(hostname_in);
if (!SSL_set1_host(ssl, hostname.c_str())) { // hostname for verification
LOG_ERROR(Service_SSL, "SSL_set1_host({}) failed", hostname);
return CheckOpenSSLErrors();

View File

@@ -7,7 +7,7 @@
#include "core/internal_network/network_interface.h"
#ifdef _WIN32
#define NOMINMAX
#include <windows.h>
#include <wlanapi.h>
#ifdef _MSC_VER

View File

@@ -19,10 +19,25 @@
namespace Network {
LegacyOnlineService::LegacyOnlineService() = default;
LegacyOnlineService::LegacyOnlineService() {
#ifdef _WIN32
WSADATA wsa_data;
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) {
LOG_ERROR(Network, "WSAStartup failed with error: {}", WSAGetLastError());
winsock_initialized = false;
} else {
winsock_initialized = true;
}
#endif
}
LegacyOnlineService::~LegacyOnlineService() {
Stop();
#ifdef _WIN32
if (winsock_initialized) {
WSACleanup();
}
#endif
}
void LegacyOnlineService::Start() {
@@ -30,6 +45,13 @@ void LegacyOnlineService::Start() {
return;
}
#ifdef _WIN32
if (!winsock_initialized) {
LOG_ERROR(Network, "Cannot start Legacy Online Service: Winsock not initialized");
return;
}
#endif
is_running = true;
worker_thread = std::thread(&LegacyOnlineService::ServerLoop, this);
}
@@ -59,14 +81,7 @@ void LegacyOnlineService::Stop() {
void LegacyOnlineService::ServerLoop() {
LOG_INFO(Network, "Starting Legacy Online UDP Server on port {}", PORT);
#ifdef _WIN32
WSADATA wsa_data;
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) {
LOG_ERROR(Network, "WSAStartup failed");
is_running = false;
return;
}
#endif
// WSAStartup is now handled in the constructor
@@ -102,10 +117,11 @@ void LegacyOnlineService::ServerLoop() {
#endif
if (res < 0) {
LOG_ERROR(Network, "Failed to bind to port {}", PORT);
#ifdef _WIN32
LOG_ERROR(Network, "Failed to bind to port {}: {}", PORT, WSAGetLastError());
closesocket(static_cast<SOCKET>(socket_fd));
#else
LOG_ERROR(Network, "Failed to bind to port {}: {}", PORT, strerror(errno));
close(static_cast<int>(socket_fd));
#endif
socket_fd = ~0ULL;
@@ -153,7 +169,7 @@ void LegacyOnlineService::ServerLoop() {
#ifdef _WIN32
if (socket_fd != ~0ULL) closesocket(static_cast<SOCKET>(socket_fd));
WSACleanup();
// WSACleanup is now handled in the destructor
#else
if (socket_fd != ~0ULL) close(static_cast<int>(socket_fd));
#endif

View File

@@ -24,6 +24,7 @@ private:
std::atomic_bool is_running{false};
std::thread worker_thread;
uintptr_t socket_fd{~0ULL}; // ~0ULL is approx -1 equivalent for unsigned
bool winsock_initialized{false};
static constexpr int PORT = 6000;
};

View File

@@ -75,6 +75,8 @@ SOCKET GetInterruptSocket() {
return interrupt_socket;
}
} // namespace
sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
sockaddr_in result;
@@ -105,6 +107,8 @@ sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
return addr;
}
namespace {
LINGER MakeLinger(bool enable, u32 linger_value) {
ASSERT(linger_value <= (std::numeric_limits<u_short>::max)());
@@ -212,6 +216,8 @@ SOCKET GetInterruptSocket() {
return interrupt_pipe_fd[0];
}
} // namespace
sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
sockaddr_in result;
@@ -234,6 +240,8 @@ sockaddr TranslateFromSockAddrIn(SockAddrIn input) {
return addr;
}
namespace {
int WSAPoll(WSAPOLLFD* fds, ULONG nfds, int timeout) {
return poll(fds, static_cast<nfds_t>(nfds), timeout);
}
@@ -320,6 +328,8 @@ Errno GetAndLogLastError(CallType call_type = CallType::Other) {
return err;
}
} // namespace
GetAddrInfoError TranslateGetAddrInfoErrorFromNative(int gai_err) {
switch (gai_err) {
case 0:
@@ -373,6 +383,8 @@ GetAddrInfoError TranslateGetAddrInfoErrorFromNative(int gai_err) {
}
}
namespace {
Domain TranslateDomainFromNative(int domain) {
switch (domain) {
case 0:
@@ -607,6 +619,8 @@ Common::Expected<std::vector<AddrInfo>, GetAddrInfoError> GetAddressInfo(
return ret;
}
std::pair<s32, Errno> Poll(std::vector<PollFD>& pollfds, s32 timeout) {
const size_t num = pollfds.size();

View File

@@ -122,8 +122,33 @@ std::optional<IPv4Address> GetHostIPv4Address();
std::string IPv4AddressToString(IPv4Address ip_addr);
u32 IPv4AddressToInteger(IPv4Address ip_addr);
#ifdef _WIN32
#include <ws2tcpip.h>
#else
#include <netdb.h>
#endif
// named to avoid name collision with Windows macro
Common::Expected<std::vector<AddrInfo>, GetAddrInfoError> GetAddressInfo(
const std::string& host, const std::optional<std::string>& service);
sockaddr TranslateFromSockAddrIn(SockAddrIn input);
inline std::pair<std::string, int> GetNameInfo(const SockAddrIn& addr) {
sockaddr sa = TranslateFromSockAddrIn(addr);
sockaddr_in addr_in{};
std::memcpy(&addr_in, &sa, sizeof(sockaddr_in));
char host[1025]; // NI_MAXHOST
int err = getnameinfo(reinterpret_cast<const sockaddr*>(&addr_in), sizeof(addr_in), host, sizeof(host), nullptr, 0, 0);
if (err != 0) {
return {std::string{}, err};
}
return {std::string(host), 0};
}
GetAddrInfoError TranslateGetAddrInfoErrorFromNative(int err);
} // namespace Network