From 37e4861e1d063cfc52930fb66670e155a3bed0a0 Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 29 Oct 2026 08:20:27 +0100 Subject: [PATCH] np: separate PSN emulation from host networking Co-authored-by: Chai T --- src/common/emulatorConfig.cpp | 4 + src/common/emulatorConfig.h | 2 + .../forms/configuration_edit_dialog.ui | 5 + src/launcher/include/configuration.h | 2 + src/launcher/src/configuration.cpp | 2 + src/launcher/src/configurationEditDialog.cpp | 2 + src/launcher/src/mainDialog.cpp | 1 + src/libs/libNet.cpp | 86 +++--- src/libs/network.cpp | 116 +++++--- src/libs/network.h | 2 + src/main.cpp | 2 + tests/KernelFileSystemTests.cpp | 267 ++++++++++++++++++ 12 files changed, 413 insertions(+), 78 deletions(-) diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 09c8711dd..dc783219d 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -133,6 +133,10 @@ bool IsNetworkConnected() { return g_config->network_connected; } +bool PsnEmulationEnabled() { + return g_config->psn_emulation_enabled; +} + bool ShaderValidationEnabled() { return g_config->shader_validation_enabled; } diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index 47a6b9ab6..41ecf87bc 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -69,6 +69,7 @@ struct ConfigOptions { bool vr_enabled = false; bool amd_cpu_enabled = false; bool network_connected = false; + bool psn_emulation_enabled = false; uint32_t vblank_frequency = 60; uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE; bool vulkan_validation_enabled = false; @@ -130,6 +131,7 @@ bool StretchEnabled(); bool VrEnabled(); bool AmdCpuEnabled(); bool IsNetworkConnected(); +bool PsnEmulationEnabled(); uint32_t GetVblankFrequency(); uint32_t GetConsoleLanguage(); bool VulkanValidationEnabled(); diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui index d10bc44dc..c3c588c8f 100644 --- a/src/launcher/forms/configuration_edit_dialog.ui +++ b/src/launcher/forms/configuration_edit_dialog.ui @@ -406,6 +406,11 @@ Enable network connection + + + Emulate PSN (compatibility) + + Qt::Vertical diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index 3ab86bc10..27d76a297 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -128,6 +128,7 @@ public: bool perf_overlay_enabled = false; bool skip_notice_screen = false; bool network_connected = false; + bool psn_emulation_enabled = false; int vblank_frequency = 60; int console_language = DEFAULT_CONSOLE_LANGUAGE; bool vulkan_validation_enabled = false; @@ -171,6 +172,7 @@ public: perf_overlay_enabled = other.perf_overlay_enabled; skip_notice_screen = other.skip_notice_screen; network_connected = other.network_connected; + psn_emulation_enabled = other.psn_emulation_enabled; vblank_frequency = other.vblank_frequency; console_language = other.console_language; vulkan_validation_enabled = other.vulkan_validation_enabled; diff --git a/src/launcher/src/configuration.cpp b/src/launcher/src/configuration.cpp index ad07c12d6..f6e501e8d 100644 --- a/src/launcher/src/configuration.cpp +++ b/src/launcher/src/configuration.cpp @@ -42,6 +42,7 @@ QVariantMap Configuration::GameSettings() const { KYTY_CFG_SET(perf_overlay_enabled); KYTY_CFG_SET(skip_notice_screen); KYTY_CFG_SET(network_connected); + KYTY_CFG_SET(psn_emulation_enabled); KYTY_CFG_SET(vblank_frequency); KYTY_CFG_SET(console_language); KYTY_CFG_SET(vulkan_validation_enabled); @@ -96,6 +97,7 @@ void Configuration::ReadGameSettingsValues(const Settings& s) { KYTY_CFG_GET(perf_overlay_enabled); KYTY_CFG_GET(skip_notice_screen); KYTY_CFG_GET(network_connected); + KYTY_CFG_GET(psn_emulation_enabled); vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt(); console_language = s.value("console_language", console_language).toInt(); if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) { diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp index c9de0ee79..c0aac8c7c 100644 --- a/src/launcher/src/configurationEditDialog.cpp +++ b/src/launcher/src/configurationEditDialog.cpp @@ -298,6 +298,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) { m_ui->checkBox_shader_cache->setChecked(info.shader_cache_enabled); m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen); m_ui->checkBox_network_connected->setChecked(info.network_connected); + m_ui->checkBox_psn_emulation->setChecked(info.psn_emulation_enabled); m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled); m_ui->spinBox_trophy_duration->setValue(info.trophy_duration_seconds); m_ui->checkBox_perf_overlay->setChecked(info.perf_overlay_enabled); @@ -533,6 +534,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo info.shader_cache_enabled = ui.checkBox_shader_cache->isChecked(); info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked(); info.network_connected = ui.checkBox_network_connected->isChecked(); + info.psn_emulation_enabled = ui.checkBox_psn_emulation->isChecked(); info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked(); info.trophy_duration_seconds = ui.spinBox_trophy_duration->value(); info.perf_overlay_enabled = ui.checkBox_perf_overlay->isChecked(); diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index 21d0af992..97be871b8 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -276,6 +276,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { AddFlag(args, info.perf_overlay_enabled, "--perf-overlay"); AddFlag(args, info.skip_notice_screen, "--skip-notice-screen"); AddFlag(args, info.network_connected, "--network-connected"); + AddFlag(args, info.psn_emulation_enabled, "--emulate-psn"); AddFlag(args, info.tessellation_enabled, "--tessellation"); AddFlag(args, info.occlusion_queries_enabled, "--occlusion-queries"); AddFlag(args, info.shader_cache_enabled, "--shader-cache"); diff --git a/src/libs/libNet.cpp b/src/libs/libNet.cpp index 95be4eeb5..4b78e9894 100644 --- a/src/libs/libNet.cpp +++ b/src/libs/libNet.cpp @@ -1685,12 +1685,34 @@ LIB_VERSION("NpAuth", 1, "NpAuth", 1, 1); constexpr int NP_AUTH_MAX_REQUEST_NUM = 16; constexpr int NP_AUTH_REQUEST_ID_OFFSET = 0x10000000; constexpr int NP_AUTH_ERROR_INVALID_ARGUMENT = -2141912319; /* 0x80550301 */ +constexpr int NP_AUTH_ERROR_INVALID_SIZE = -2141912318; /* 0x80550302 */ constexpr int NP_AUTH_ERROR_ABORTED = -2141912316; /* 0x80550304 */ constexpr int NP_AUTH_ERROR_REQUEST_MAX = -2141912315; /* 0x80550305 */ constexpr int NP_AUTH_ERROR_REQUEST_NOT_FOUND = -2141912314; /* 0x80550306 */ constexpr int NP_AUTH_ERROR_INVALID_ID = -2141912313; /* 0x80550307 */ +constexpr int NP_AUTH_ERROR_SERVICE_DOWN = -2141912063; /* 0x80550401 */ constexpr int NP_ERROR_SIGNED_OUT = -2141913082; /* 0x80550006 */ +struct NpAuthGetAuthorizationCodeParameterV3 { + size_t size; + int user_id; + uint8_t padding[4]; + const void* client_id; + const char* scope; +}; + +struct NpAuthGetIdTokenParameterV3 { + size_t size; + int user_id; + uint8_t padding[4]; + const void* client_id; + const void* client_secret; + const char* scope; +}; + +static_assert(sizeof(NpAuthGetAuthorizationCodeParameterV3) == 32); +static_assert(sizeof(NpAuthGetIdTokenParameterV3) == 40); + enum class NpAuthRequestState { None = 0, Ready, @@ -1822,7 +1844,7 @@ static int np_auth_wait_or_poll_async(int req_id, int* result) { return 0; } -static int np_auth_complete_signed_out(NpAuthRequest* request) { +static int np_auth_complete_unavailable(NpAuthRequest* request, int user_id) { if (request->state == NpAuthRequestState::Complete) { request->result = NP_AUTH_ERROR_INVALID_ARGUMENT; return NP_AUTH_ERROR_INVALID_ARGUMENT; @@ -1833,9 +1855,11 @@ static int np_auth_complete_signed_out(NpAuthRequest* request) { } request->state = NpAuthRequestState::Complete; - request->result = NP_ERROR_SIGNED_OUT; + // Emulating sign-in cannot provide a real authorization code or ID token. + request->result = Network::NpManager::IsSignedIn(user_id) ? NP_AUTH_ERROR_SERVICE_DOWN + : NP_ERROR_SIGNED_OUT; - return request->async ? 0 : NP_ERROR_SIGNED_OUT; + return request->async ? 0 : request->result; } static int KYTY_SYSV_ABI NpAuthWaitAsync(int req_id, int* result) { @@ -1856,8 +1880,9 @@ static int KYTY_SYSV_ABI NpAuthPollAsync(int req_id, int* result) { return np_auth_wait_or_poll_async(req_id, result); } -static int KYTY_SYSV_ABI NpAuthGetAuthorizationCodeV3(int req_id, const void* param, - void* auth_code, int* issuer_id) { +static int KYTY_SYSV_ABI NpAuthGetAuthorizationCodeV3( + int req_id, const NpAuthGetAuthorizationCodeParameterV3* param, void* auth_code, + int* issuer_id) { PRINT_NAME(); LOGF("\t req_id = %d\n", req_id); @@ -1865,9 +1890,17 @@ static int KYTY_SYSV_ABI NpAuthGetAuthorizationCodeV3(int req_id, const void* pa LOGF("\t auth_code = 0x%016" PRIx64 "\n", reinterpret_cast(auth_code)); LOGF("\t issuer_id = 0x%016" PRIx64 "\n", reinterpret_cast(issuer_id)); - if (auth_code != nullptr) { - memset(auth_code, 0, 136); + if (req_id <= 0 || param == nullptr || auth_code == nullptr) { + return NP_AUTH_ERROR_INVALID_ARGUMENT; } + if (param->size < sizeof(*param)) { + return NP_AUTH_ERROR_INVALID_SIZE; + } + if (param->client_id == nullptr || param->scope == nullptr) { + return NP_AUTH_ERROR_INVALID_ARGUMENT; + } + + memset(auth_code, 0, 136); if (issuer_id != nullptr) { *issuer_id = 0; } @@ -1877,46 +1910,35 @@ static int KYTY_SYSV_ABI NpAuthGetAuthorizationCodeV3(int req_id, const void* pa if (request == nullptr) { return NP_AUTH_ERROR_REQUEST_NOT_FOUND; } - if (request->state == NpAuthRequestState::Aborted) { - // request->result = 0; - // return 0; - request->result = NP_AUTH_ERROR_ABORTED; - return NP_AUTH_ERROR_ABORTED; - } - // request->state = NpAuthRequestState::Complete; - // request->result = 0; - - // return 0; - return np_auth_complete_signed_out(request); + return np_auth_complete_unavailable(request, param->user_id); } -static int KYTY_SYSV_ABI NpAuthGetIdTokenV3(int req_id, const void* param, void* id_token) { +static int KYTY_SYSV_ABI NpAuthGetIdTokenV3(int req_id, const NpAuthGetIdTokenParameterV3* param, + void* id_token) { PRINT_NAME(); LOGF("\t req_id = %d\n", req_id); LOGF("\t param = 0x%016" PRIx64 "\n", reinterpret_cast(param)); LOGF("\t id_token = 0x%016" PRIx64 "\n", reinterpret_cast(id_token)); - if (id_token != nullptr) { - memset(id_token, 0, 4104); + if (req_id <= 0 || param == nullptr || id_token == nullptr) { + return NP_AUTH_ERROR_INVALID_ARGUMENT; } + if (param->size < sizeof(*param)) { + return NP_AUTH_ERROR_INVALID_SIZE; + } + if (param->client_id == nullptr || param->client_secret == nullptr || param->scope == nullptr) { + return NP_AUTH_ERROR_INVALID_ARGUMENT; + } + + memset(id_token, 0, 4104); std::scoped_lock lock(g_np_auth_request_mutex); auto* request = np_auth_find_request(req_id); if (request == nullptr) { return NP_AUTH_ERROR_REQUEST_NOT_FOUND; } - if (request->state == NpAuthRequestState::Aborted) { - // request->result = 0; - // return 0; - request->result = NP_AUTH_ERROR_ABORTED; - return NP_AUTH_ERROR_ABORTED; - } - // request->state = NpAuthRequestState::Complete; - // request->result = 0; - - // return 0; - return np_auth_complete_signed_out(request); + return np_auth_complete_unavailable(request, param->user_id); } LIB_DEFINE(InitNet_1_NpAuth) { diff --git a/src/libs/network.cpp b/src/libs/network.cpp index 78e8bbb3b..3d10f6b1b 100644 --- a/src/libs/network.cpp +++ b/src/libs/network.cpp @@ -4002,6 +4002,11 @@ struct NpCountryCode { char padding[1]; }; +struct NpLanguageCode { + char code[36]; + char padding[12]; +}; + struct NpAgeRestriction { NpCountryCode country_code; int8_t age; @@ -4056,8 +4061,23 @@ constexpr int np_error_aborted = -2141913070; /* 0x8055 constexpr int np_error_request_max = -2141913069; /* 0x80550013 */ constexpr int np_error_request_not_found = -2141913068; /* 0x80550014 */ constexpr int np_error_invalid_id = -2141913067; /* 0x80550015 */ +constexpr int np_error_network_not_available = -2141872086; /* 0x8055a02a */ constexpr int np_request_max = 128; constexpr uint32_t np_state_signed_out = 1; +constexpr uint32_t np_state_signed_in = 2; + +// A simulated account belongs only to the primary user and survives network loss. +bool IsSignedIn(int user_id) { + return Config::PsnEmulationEnabled() && user_id == Config::GetUserId(); +} + +static int np_reachability_result(int user_id) { + if (!IsSignedIn(user_id)) { + return np_error_signed_out; + } + // This is an explicit compatibility simulation, not a connection to PSN. + return NetCtl::NetCtlConnected() ? OK : np_error_network_not_available; +} struct NpStateCallback { NpStateCallbackA callback = nullptr; @@ -4117,7 +4137,7 @@ static NpRequest* np_get_request_locked(int req_id) { return &g_np_requests[index]; } -static int np_complete_signed_out_locked(NpRequest* request) { +static int np_complete_locked(NpRequest* request, int result) { if (request->state == NpRequestState::Complete) { request->result = np_error_invalid_argument; return np_error_invalid_argument; @@ -4128,9 +4148,9 @@ static int np_complete_signed_out_locked(NpRequest* request) { } request->state = NpRequestState::Complete; - request->result = np_error_signed_out; + request->result = result; - return (request->async ? OK : np_error_signed_out); + return (request->async ? OK : result); } int KYTY_SYSV_ABI NpCheckCallback() { @@ -4144,7 +4164,9 @@ int KYTY_SYSV_ABI NpCheckCallback() { } } for (const auto& entry: pending) { - entry.callback(Config::GetUserId(), np_state_signed_out, entry.userdata); + entry.callback(Config::GetUserId(), + IsSignedIn(Config::GetUserId()) ? np_state_signed_in : np_state_signed_out, + entry.userdata); } return OK; @@ -4239,12 +4261,13 @@ int KYTY_SYSV_ABI NpGetNpId(int user_id, NpId* np_id) { EXIT_NOT_IMPLEMENTED(np_id == nullptr); - // int s = snprintf(np_id->handle.data, 16, "Kyty"); - // EXIT_NOT_IMPLEMENTED(s >= 16); - // np_id->handle.term = 0; std::memset(np_id, 0, sizeof(*np_id)); - // return OK; + if (IsSignedIn(user_id)) { + std::memcpy(np_id->handle.data, "Kyty", 4); + return OK; + } + return np_error_signed_out; } @@ -4255,12 +4278,13 @@ int KYTY_SYSV_ABI NpGetOnlineId(int user_id, NpOnlineId* online_id) { EXIT_NOT_IMPLEMENTED(online_id == nullptr); - // int s = snprintf(online_id->data, 16, "Kyty"); - // EXIT_NOT_IMPLEMENTED(s >= 16); - // online_id->term = 0; std::memset(online_id, 0, sizeof(*online_id)); - // return OK; + if (IsSignedIn(user_id)) { + std::memcpy(online_id->data, "Kyty", 4); + return OK; + } + return np_error_signed_out; } @@ -4271,10 +4295,13 @@ int KYTY_SYSV_ABI NpGetAccountIdA(int user_id, uint64_t* account_id) { EXIT_NOT_IMPLEMENTED(account_id == nullptr); - // *account_id = 0x00000000feedfaceull; *account_id = 0; - // return OK; + if (IsSignedIn(user_id)) { + *account_id = 0x00000000feedfaceull; + return OK; + } + return np_error_signed_out; } @@ -4287,10 +4314,12 @@ int KYTY_SYSV_ABI NpGetAccountCountryA(int user_id, void* country_code) { auto* code = static_cast(country_code); std::memset(code, 0, sizeof(*code)); - // code->data[0] = ''; - // code->data[1] = 'S'; - // return OK; + if (IsSignedIn(user_id)) { + std::memcpy(code->data, "US", 2); + return OK; + } + return np_error_signed_out; } @@ -4305,11 +4334,14 @@ int KYTY_SYSV_ABI NpGetAccountAge(int req_id, int user_id, uint8_t* age) { return np_error_invalid_argument; } - // *age = 21; - *age = 0; + std::lock_guard lock(g_np_request_mutex); + auto* request = np_get_request_locked(req_id); + if (request == nullptr) { + return np_error_request_not_found; + } - // return OK; - return np_error_signed_out; + *age = IsSignedIn(user_id) ? 21 : 0; + return np_complete_locked(request, IsSignedIn(user_id) ? OK : np_error_signed_out); } int KYTY_SYSV_ABI NpGetAccountLanguage2(int req_id, int user_id, void* language_code) { @@ -4325,11 +4357,17 @@ int KYTY_SYSV_ABI NpGetAccountLanguage2(int req_id, int user_id, void* language_ std::lock_guard lock(g_np_request_mutex); - if (np_get_request_locked(req_id) == nullptr) { + auto* request = np_get_request_locked(req_id); + if (request == nullptr) { return np_error_request_not_found; } - return np_error_signed_out; + auto* code = static_cast(language_code); + std::memset(code, 0, sizeof(*code)); + if (IsSignedIn(user_id)) { + std::memcpy(code->code, "en-US", 5); + } + return np_complete_locked(request, IsSignedIn(user_id) ? OK : np_error_signed_out); } int KYTY_SYSV_ABI NpCreateRequest() { @@ -4428,8 +4466,9 @@ int KYTY_SYSV_ABI NpCheckNpAvailability(int req_id, const char* user, void* resu return np_error_request_not_found; } - // return OK; - return np_complete_signed_out_locked(request); + return np_complete_locked(request, std::strcmp(user, "Kyty") == 0 + ? np_reachability_result(Config::GetUserId()) + : np_error_signed_out); } int KYTY_SYSV_ABI NpCheckNpReachability(int req_id, int user_id) { @@ -4451,8 +4490,7 @@ int KYTY_SYSV_ABI NpCheckNpReachability(int req_id, int user_id) { return np_error_request_not_found; } - // return OK; - return np_complete_signed_out_locked(request); + return np_complete_locked(request, np_reachability_result(user_id)); } int KYTY_SYSV_ABI NpPollAsync(int req_id, int* result) { @@ -4509,23 +4547,10 @@ int KYTY_SYSV_ABI NpCheckPremium(int req_id, const NpCheckPremiumParameter* para return np_error_request_not_found; } - if (request->state == NpRequestState::Complete) { - request->result = np_error_invalid_argument; - return np_error_invalid_argument; - } - - if (request->state == NpRequestState::Aborted) { - return np_error_aborted; - } - std::memset(result, 0, sizeof(*result)); - // result->authorized = true; - // request->state = NpRequestState::Complete; - // request->result = OK; - - // return OK; - return np_complete_signed_out_locked(request); + // The simulated account has no premium eligibility. + return np_complete_locked(request, np_reachability_result(param->user_id)); } int KYTY_SYSV_ABI NpGetState(int user_id, uint32_t* state) { @@ -4535,7 +4560,7 @@ int KYTY_SYSV_ABI NpGetState(int user_id, uint32_t* state) { LOGF("\t user_id = %d\n", user_id); - *state = np_state_signed_out; + *state = IsSignedIn(user_id) ? np_state_signed_in : np_state_signed_out; return OK; } @@ -4549,8 +4574,7 @@ int KYTY_SYSV_ABI NpGetNpReachabilityState(int user_id, uint32_t* state) { LOGF("\t user_id = %d\n", user_id); - // *state = 2; // SCE_NP_REACHABILITY_STATE_REACHABLE - *state = 0; // SCE_NP_REACHABILITY_STATE_UNAVAILABLE + *state = np_reachability_result(user_id) == OK ? 2 : 0; // REACHABLE / UNAVAILABLE return OK; } @@ -4564,7 +4588,7 @@ int KYTY_SYSV_ABI NpHasSignedUp(int user_id, bool* has_signed_up) { LOGF("\t user_id = %d\n", user_id); - *has_signed_up = false; + *has_signed_up = IsSignedIn(user_id); return OK; } diff --git a/src/libs/network.h b/src/libs/network.h index 4a942cea5..dc81c1f53 100644 --- a/src/libs/network.h +++ b/src/libs/network.h @@ -182,6 +182,8 @@ struct NpCheckPremiumResult; using NpStateCallbackA = KYTY_SYSV_ABI void (*)(int user_id, uint32_t state, void* userdata); +bool IsSignedIn(int user_id); + int KYTY_SYSV_ABI NpCheckCallback(); int KYTY_SYSV_ABI NpSetNpTitleId(const NpTitleId* title_id, const NpTitleSecret* title_secret); int KYTY_SYSV_ABI NpSetContentRestriction(const NpContentRestriction* restriction); diff --git a/src/main.cpp b/src/main.cpp index 87cab9934..9a0a2b1dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -79,6 +79,7 @@ static void PrintUsage() { ::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n"); ::printf(" --vulkan-validation Enable Vulkan validation.\n"); ::printf(" --network-connected Use the host local network.\n"); + ::printf(" --emulate-psn Simulate PSN sign-in for compatibility.\n"); ::printf(" --gpu-assisted-validation Bounds-check shader accesses on the GPU.\n" " Implies --vulkan-validation; very slow.\n"); ::printf(" --shader-validation Enable shader validation.\n"); @@ -206,6 +207,7 @@ static constexpr Flag FLAGS[] = { {"--vr", &ConfigOptions::vr_enabled}, {"--amd-cpu", &ConfigOptions::amd_cpu_enabled}, {"--network-connected", &ConfigOptions::network_connected}, + {"--emulate-psn", &ConfigOptions::psn_emulation_enabled}, {"--vulkan-validation", &ConfigOptions::vulkan_validation_enabled}, {"--gpu-assisted-validation", &ConfigOptions::gpu_assisted_validation_enabled}, {"--shader-validation", &ConfigOptions::shader_validation_enabled}, diff --git a/tests/KernelFileSystemTests.cpp b/tests/KernelFileSystemTests.cpp index db2a406e5..2bc64ac1b 100644 --- a/tests/KernelFileSystemTests.cpp +++ b/tests/KernelFileSystemTests.cpp @@ -73,6 +73,10 @@ namespace Libs::LibNpCommerce { void InitNet_1_NpCommerce(Loader::SymbolDatabase *symbols); } +namespace Libs::LibNpAuth { +void InitNet_1_NpAuth(Loader::SymbolDatabase *symbols); +} + namespace Libs::LibNpEntitlementAccess { void InitNet_1_NpEntitlementAccess(Loader::SymbolDatabase *symbols); } @@ -2082,6 +2086,256 @@ void TestNetCtlHostIpv4() { } #endif +template +void KYTY_SYSV_ABI RecordNpState(int user_id, uint32_t state, void* arg) { + auto& event = *static_cast*>(arg); + ++event[0]; + event[1] = user_id; + event[2] = static_cast(state); +} + +void TestNpEmulation() { + using namespace Libs::Network::NpManager; + using namespace Libs::Network::NetCtl; + constexpr int SignedOut = static_cast(0x80550006u); + constexpr int NetworkUnavailable = static_cast(0x8055a02au); + constexpr int Aborted = static_cast(0x80550012u); + struct AsyncParameters { + size_t size = sizeof(AsyncParameters); + uint64_t affinity = 0; + int priority = 0; + uint8_t padding[4] {}; + } async_parameters; + struct PremiumParameters { + size_t size = sizeof(PremiumParameters); + int user_id = 0; + uint8_t padding[4] {}; + uint64_t features = 1; + uint8_t reserved[32] {}; + } premium_parameters; + struct PremiumResult { + bool authorized = true; + uint8_t reserved[32] {}; + } premium_result; + + const auto create_request = [&] (bool async) { + const int request = async + ? NpCreateAsyncRequest(reinterpret_cast( + &async_parameters)) + : NpCreateRequest(); + Check(request > 0, "create NP request"); + return request; + }; + const auto check_request = [&] (bool async, int expected, auto operation) { + const int request = create_request(async); + Check(operation(request) == (async ? OK : expected), + "NP call separates asynchronous submission from its execution result"); + if (async) { + int result = 123; + Check(NpPollAsync(request, &result) == OK && result == expected, + "NP polling reports the completed operation result"); + } + Check(NpDeleteRequest(request) == OK, "release completed NP request"); + }; + + Config::ConfigOptions options; + options.printf_direction = Config::LogDirection::Silent; + Check(!options.network_connected && !options.psn_emulation_enabled, + "host networking and PSN emulation both require explicit opt-in"); + static std::array, 4> events {}; + const std::array callbacks { + RecordNpState<0>, RecordNpState<1>, RecordNpState<2>, RecordNpState<3>}; + int mode = 0; + for (bool network : {false, true}) { + int previous_network_state = -1; + for (bool emulate : {false, true}) { + options.network_connected = network; + options.psn_emulation_enabled = emulate; + Config::Load(options); + Check(Config::IsNetworkConnected() == network && Config::PsnEmulationEnabled() == emulate, + "loading PSN emulation does not change the independent network option"); + Check(NetCtlInit() == OK, "initialize network state for NP mode"); + int network_state = -1; + Check(NetCtlGetState(&network_state) == OK && (network || network_state == 0), + "PSN emulation cannot connect disabled host networking"); + Check(previous_network_state == -1 || previous_network_state == network_state, + "PSN emulation does not change host network connectivity"); + previous_network_state = network_state; + const bool connected = network_state == 3; + + Check(NpRegisterStateCallbackA(callbacks[mode], &events[mode]) > 0 && + NpCheckCallback() == OK && events[mode][0] == 1 && + events[mode][1] == Config::GetUserId() && + events[mode][2] == (emulate ? 2 : 1), + "initial NP callback matches the selected emulated sign-in state"); + ++mode; + for (int player = 0; player < Config::MAX_PLAYERS; ++player) { + const int user_id = Config::GetPlayerUserId(player); + const bool signed_in = emulate && player == 0; + const int identity_result = signed_in ? OK : SignedOut; + const int online_result = !signed_in ? SignedOut : connected ? OK : NetworkUnavailable; + uint32_t state = 99; + bool signed_up = false; + Check(NpGetState(user_id, &state) == OK && state == (signed_in ? 2u : 1u) && + NpHasSignedUp(user_id, &signed_up) == OK && signed_up == signed_in, + "only the primary emulated user has a persistent signed-in account"); + Check(NpGetNpReachabilityState(user_id, &state) == OK && + state == (signed_in && connected ? 2u : 0u), + "NP reachability requires both the emulated account and host connectivity"); + + uint64_t account_id = 0; + std::array online_id {}; + std::array np_id {}; + std::array country {}; + Check(NpGetAccountIdA(user_id, &account_id) == identity_result && + NpGetOnlineId(user_id, reinterpret_cast(online_id.data())) == + identity_result && + NpGetNpId(user_id, reinterpret_cast(np_id.data())) == identity_result && + NpGetAccountCountryA(user_id, country.data()) == identity_result, + "NP identity and account-country getters agree on primary and guest sign-in"); + if (signed_in) { + Check(account_id != 0 && online_id[0] != 0 && online_id[16] == 0 && + std::strcmp(online_id.data(), np_id.data()) == 0 && + country[0] != 0 && country[1] != 0 && country[2] == 0, + "emulated account identity and country remain usable while disconnected"); + } + + for (bool async : {false, true}) { + check_request(async, online_result, [&] (int request) { + return NpCheckNpReachability(request, user_id); + }); + uint8_t age = 0; + check_request(async, identity_result, [&] (int request) { + return NpGetAccountAge(request, user_id, &age); + }); + std::array language {}; + check_request(async, identity_result, [&] (int request) { + return NpGetAccountLanguage2(request, user_id, language.data()); + }); + Check(!signed_in || (age > 0 && language[0] != 0 && language[35] == 0), + "signed-in account metadata is available independently of host networking"); + premium_parameters.user_id = user_id; + premium_result.authorized = true; + check_request(async, online_result, [&] (int request) { + return NpCheckPremium(request, + reinterpret_cast(&premium_parameters), + reinterpret_cast(&premium_result)); + }); + Check(!premium_result.authorized, "PSN emulation does not grant premium eligibility"); + if (player == 0) { + check_request(async, online_result, [&] (int request) { + return NpCheckNpAvailability(request, "Kyty", nullptr); + }); + } + } + } + const int aborted = create_request(true); + int result = 123; + Check(NpAbortRequest(aborted) == OK && + NpCheckNpReachability(aborted, Config::GetUserId()) == Aborted && + NpPollAsync(aborted, &result) == OK && result == Aborted && + NpDeleteRequest(aborted) == OK, + "NP emulation does not turn an aborted request into a successful one"); + NetCtlTerm(); + } + } + Config::Load(Config::ConfigOptions {}); +} + +void TestNpAuthEmulation() { + Loader::SymbolDatabase symbols; + Libs::LibNpAuth::InitNet_1_NpAuth(&symbols); + const auto resolve = [&] (const char* nid) { + const auto* symbol = symbols.FindByNid(nid, Loader::SymbolType::Func); + Check(symbol != nullptr, "resolve NP authentication exports"); + return symbol->vaddr; + }; + const auto create = reinterpret_cast(resolve("6bwFkosYRQg")); + const auto create_async = reinterpret_cast( + resolve("N+mr7GjTvr8")); + const auto destroy = reinterpret_cast(resolve("H8wG9Bk-nPc")); + const auto poll = reinterpret_cast(resolve("gjSyfzSsDcE")); + const auto get_code = reinterpret_cast( + resolve("KI4dHLlTNl0")); + const auto get_token = reinterpret_cast( + resolve("RdsFVsgSpZY")); + constexpr int InvalidArgument = static_cast(0x80550301u); + constexpr int InvalidSize = static_cast(0x80550302u); + constexpr int ServiceUnavailable = static_cast(0x80550401u); + constexpr int SignedOut = static_cast(0x80550006u); + struct AuthorizationCodeParameters { + size_t size = sizeof(AuthorizationCodeParameters); + int user_id = 0; + uint8_t padding[4] {}; + const void* client_id = "test-client"; + const char* scope = "test-scope"; + } code_parameters; + struct IdTokenParameters { + size_t size = sizeof(IdTokenParameters); + int user_id = 0; + uint8_t padding[4] {}; + const void* client_id = "test-client"; + const void* client_secret = "test-secret"; + const char* scope = "test-scope"; + } token_parameters; + std::array code {}; + std::array token {}; + const std::array async_parameters {24, 0, 0}; + int issuer = 0; + const int invalid_request = create(); + code_parameters.size = 0; + token_parameters.size = 0; + Check(invalid_request > 0 && + get_code(invalid_request, nullptr, code.data(), &issuer) == InvalidArgument && + get_token(invalid_request, nullptr, token.data()) == InvalidArgument && + get_code(invalid_request, &code_parameters, code.data(), &issuer) == InvalidSize && + get_token(invalid_request, &token_parameters, token.data()) == InvalidSize && + destroy(invalid_request) == OK, + "NP authentication rejects missing and undersized user parameters"); + code_parameters.size = sizeof(code_parameters); + token_parameters.size = sizeof(token_parameters); + + Config::ConfigOptions options; + options.printf_direction = Config::LogDirection::Silent; + for (bool network : {false, true}) { + for (bool emulate : {false, true}) { + options.network_connected = network; + options.psn_emulation_enabled = emulate; + Config::Load(options); + for (int player = 0; player < Config::MAX_PLAYERS; ++player) { + code_parameters.user_id = Config::GetPlayerUserId(player); + token_parameters.user_id = code_parameters.user_id; + const int expected = emulate && player == 0 ? ServiceUnavailable : SignedOut; + for (bool async : {false, true}) { + for (bool id_token : {false, true}) { + const int request = async ? create_async(async_parameters.data()) : create(); + Check(request > 0, "create NP authentication request"); + code.fill(0xff); + token.fill(0xff); + issuer = -1; + const int result = id_token + ? get_token(request, &token_parameters, token.data()) + : get_code(request, &code_parameters, code.data(), &issuer); + Check(result == (async ? OK : expected), + "NP authentication distinguishes an unavailable service from sign-out"); + if (async) { + int completion = 123; + Check(poll(request, &completion) == OK && completion == expected, + "asynchronous NP authentication delivers the actual failure"); + } + const auto zero = [] (uint8_t value) { return value == 0; }; + Check(id_token ? std::all_of(token.begin(), token.end(), zero) + : std::all_of(code.begin(), code.end(), zero) && issuer == 0, + "PSN emulation does not fabricate authorization codes or ID tokens"); + Check(destroy(request) == OK, "release NP authentication request"); + } + } + } + } + } + Config::Load(Config::ConfigOptions {}); +} + void CheckEtherAddressFormatting() { Loader::SymbolDatabase symbols; Libs::LibNet::InitNet_1_Net(&symbols); @@ -2458,6 +2712,17 @@ int main(int argc, char** argv) { Config::Load(options); subsystems.Initialize(); + if (argc == 2 && std::string_view(argv[1]) == "--network-only") { +#if defined(__linux__) + TestNetCtlHostIpv4(); +#endif + TestNpEmulation(); + TestNpAuthEmulation(); + subsystems.Destroy(); + std::printf("KernelFileSystemTests: network cases passed\n"); + return 0; + } + TestMutexHandoff(); if (argc == 2 && std::string_view(argv[1]) == "--mutex-only") { subsystems.Destroy(); @@ -2503,6 +2768,8 @@ int main(int argc, char** argv) { #if defined(__linux__) TestNetCtlHostIpv4(); #endif + TestNpEmulation(); + TestNpAuthEmulation(); TestNpWebApi2Memory(); TestNpCommerceDialog(); graphics.reset();