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();