diff --git a/CMakeLists.txt b/CMakeLists.txt index b043e944a..46bec8ff5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -628,6 +628,14 @@ set(NP_LIBS src/core/libraries/np/np_error.h src/core/libraries/np/np_manager.h src/core/libraries/np/np_matching2.cpp src/core/libraries/np/np_matching2.h + src/core/libraries/np/np_signaling/np_signaling.cpp + src/core/libraries/np/np_signaling/np_signaling.h + src/core/libraries/np/np_signaling/np_signaling_state.cpp + src/core/libraries/np/np_signaling/np_signaling_state.h + src/core/libraries/np/np_signaling/np_signaling_helpers.cpp + src/core/libraries/np/np_signaling/np_signaling_helpers.h + src/core/libraries/np/np_signaling/np_signaling_stubs.cpp + src/core/libraries/np/np_signaling/np_signaling_stubs.h src/core/libraries/np/np_trophy.cpp src/core/libraries/np/np_trophy.h src/core/libraries/np/np_tus.cpp diff --git a/src/common/logging/classes.h b/src/common/logging/classes.h index a25197289..3a5240f1d 100644 --- a/src/common/logging/classes.h +++ b/src/common/logging/classes.h @@ -66,6 +66,7 @@ constexpr auto Lib_NpCommerce = "Lib.NpCommerce"; ///< The Lib constexpr auto Lib_NpCommon = "Lib.NpCommon"; ///< The LibSceNpCommon implementation constexpr auto Lib_NpManager = "Lib.NpManager"; ///< The LibSceNpManager implementation constexpr auto Lib_NpMatching2 = "Lib.NpMatching2"; ///< The LibSceNpMatching2 implementation +constexpr auto Lib_NpSignaling = "Lib.NpSignaling"; ///< The LibSceNpSignaling implementation constexpr auto Lib_NpPartner = "Lib.NpPartner"; ///< The LibSceNpPartner implementation constexpr auto Lib_NpParty = "Lib.NpParty"; ///< The LibSceNpParty implementation constexpr auto Lib_NpProfileDialog = "Lib.NpProfileDialog"; ///< The LibSceNpProfileDialog implementation diff --git a/src/common/logging/log.cpp b/src/common/logging/log.cpp index 6b0456bfd..f144939d2 100644 --- a/src/common/logging/log.cpp +++ b/src/common/logging/log.cpp @@ -88,6 +88,7 @@ std::unordered_map> ALL_LOGGER {Class::Lib_NpCommon, nullptr}, {Class::Lib_NpManager, nullptr}, {Class::Lib_NpMatching2, nullptr}, + {Class::Lib_NpSignaling, nullptr}, {Class::Lib_NpPartner, nullptr}, {Class::Lib_NpParty, nullptr}, {Class::Lib_NpProfileDialog, nullptr}, diff --git a/src/core/libraries/kernel/threads.h b/src/core/libraries/kernel/threads.h index 04e9743d7..dec851143 100644 --- a/src/core/libraries/kernel/threads.h +++ b/src/core/libraries/kernel/threads.h @@ -39,6 +39,26 @@ int PS4_SYSV_ABI scePthreadMutexInit(PthreadMutexT* mutex, const PthreadMutexAtt int PS4_SYSV_ABI posix_pthread_mutex_lock(PthreadMutexT* mutex); int PS4_SYSV_ABI posix_pthread_mutex_unlock(PthreadMutexT* mutex); int PS4_SYSV_ABI posix_pthread_mutex_destroy(PthreadMutexT* mutex); +s32 PS4_SYSV_ABI posix_pthread_mutex_trylock(PthreadMutexT* mutex); + +int PS4_SYSV_ABI posix_pthread_condattr_init(PthreadCondAttrT* attr); +int PS4_SYSV_ABI posix_pthread_condattr_destroy(PthreadCondAttrT* attr); +int PS4_SYSV_ABI scePthreadCondInit(PthreadCondT* cond, const PthreadCondAttrT* cond_attr, + const char* name); +int PS4_SYSV_ABI posix_pthread_cond_destroy(PthreadCondT* cond); +int PS4_SYSV_ABI posix_pthread_cond_signal(PthreadCondT* cond); +int PS4_SYSV_ABI posix_pthread_cond_wait(PthreadCondT* cond, PthreadMutexT* mutex); +int PS4_SYSV_ABI posix_pthread_cond_reltimedwait_np(PthreadCondT* cond, PthreadMutexT* mutex, + u64 usec); + +int PS4_SYSV_ABI posix_pthread_attr_setstacksize(PthreadAttrT* attr, size_t stacksize); +int PS4_SYSV_ABI posix_pthread_attr_setinheritsched(PthreadAttrT* attr, int sched_inherit); +int PS4_SYSV_ABI posix_pthread_attr_setschedpolicy(PthreadAttrT* attr, SchedPolicy policy); +int PS4_SYSV_ABI posix_pthread_attr_setschedparam(PthreadAttrT* attr, SchedParam* param); +int PS4_SYSV_ABI scePthreadAttrSetaffinity(PthreadAttrT* attr, const u64 mask); +int PS4_SYSV_ABI posix_pthread_create_name_np(PthreadT* thread, const PthreadAttrT* attr, + PthreadEntryFunc start_routine, void* arg, + const char* name); void RegisterThreads(Core::Loader::SymbolsResolver* sym); diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 61d5d34e1..1aadc6ffa 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -41,6 +41,7 @@ #include "core/libraries/np/np_party.h" #include "core/libraries/np/np_profile_dialog/np_profile_dialog.h" #include "core/libraries/np/np_score/np_score.h" +#include "core/libraries/np/np_signaling/np_signaling.h" #include "core/libraries/np/np_sns_facebook_dialog.h" #include "core/libraries/np/np_trophy.h" #include "core/libraries/np/np_tus.h" @@ -103,6 +104,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::Np::NpCommon::RegisterLib(sym); Libraries::Np::NpManager::RegisterLib(sym); Libraries::Np::NpMatching2::RegisterLib(sym); + Libraries::Np::NpSignaling::RegisterLib(sym); Libraries::Np::NpScore::RegisterLib(sym); Libraries::Np::NpTrophy::RegisterLib(sym); Libraries::Np::NpWebApi::RegisterLib(sym); diff --git a/src/core/libraries/network/net_ctl_codes.h b/src/core/libraries/network/net_ctl_codes.h index 0dac4201e..f2c588984 100644 --- a/src/core/libraries/network/net_ctl_codes.h +++ b/src/core/libraries/network/net_ctl_codes.h @@ -8,6 +8,7 @@ constexpr int ORBIS_NET_CTL_ERROR_CALLBACK_MAX = 0x80412103; constexpr int ORBIS_NET_CTL_ERROR_ID_NOT_FOUND = 0x80412104; constexpr int ORBIS_NET_CTL_ERROR_INVALID_ID = 0x80412105; constexpr int ORBIS_NET_CTL_ERROR_INVALID_ADDR = 0x80412107; +constexpr int ORBIS_NET_CTL_ERROR_INVALID_SIZE = 0x80412111; constexpr int ORBIS_NET_CTL_ERROR_NOT_CONNECTED = 0x80412108; constexpr int ORBIS_NET_CTL_ERROR_NOT_AVAIL = 0x80412109; constexpr int ORBIS_NET_CTL_ERROR_NETWORK_DISABLED = 0x8041210D; diff --git a/src/core/libraries/network/net_util.cpp b/src/core/libraries/network/net_util.cpp index fd0ed877b..0dda3c21b 100644 --- a/src/core/libraries/network/net_util.cpp +++ b/src/core/libraries/network/net_util.cpp @@ -334,6 +334,18 @@ const std::string& NetUtilInternal::GetIp() const { return ip; } +u32 NetUtilInternal::GetExternalIp() const { + return external_ip; +} + +void NetUtilInternal::SetExternalIp(u32 addr) { + external_ip = addr; +} + +u32 NetUtilInternal::GetNatType() const { + return nat_type; +} + bool NetUtilInternal::RetrieveIp() { std::scoped_lock lock{m_mutex}; diff --git a/src/core/libraries/network/net_util.h b/src/core/libraries/network/net_util.h index d8c6870f9..661722b4b 100644 --- a/src/core/libraries/network/net_util.h +++ b/src/core/libraries/network/net_util.h @@ -22,6 +22,8 @@ private: std::string default_gateway{}; std::string netmask{}; std::string ip{}; + u32 external_ip{0}; + u32 nat_type{0}; std::mutex m_mutex; public: @@ -29,6 +31,9 @@ public: const std::string& GetDefaultGateway() const; const std::string& GetNetmask() const; const std::string& GetIp() const; + u32 GetExternalIp() const; + void SetExternalIp(u32 addr); + u32 GetNatType() const; bool RetrieveEthernetAddr(); bool RetrieveDefaultGateway(); bool RetrieveNetmask(); diff --git a/src/core/libraries/network/netctl.cpp b/src/core/libraries/network/netctl.cpp index 136d63810..f0bd459fd 100644 --- a/src/core/libraries/network/netctl.cpp +++ b/src/core/libraries/network/netctl.cpp @@ -258,8 +258,30 @@ int PS4_SYSV_ABI sceNetCtlGetInfoV6IpcInt() { return ORBIS_OK; } -int PS4_SYSV_ABI sceNetCtlGetNatInfo() { - LOG_ERROR(Lib_NetCtl, "(STUBBED) called"); +int PS4_SYSV_ABI sceNetCtlGetNatInfo(OrbisNetCtlNatInfo* nat_info) { + if (!nat_info) { + return ORBIS_NET_CTL_ERROR_INVALID_ADDR; + } + if (nat_info->size != sizeof(OrbisNetCtlNatInfo)) { + return ORBIS_NET_CTL_ERROR_INVALID_SIZE; + } + + auto* netinfo = Common::Singleton::Instance(); + nat_info->nat_type = netinfo->GetNatType(); + const u32 ext_ip = netinfo->GetExternalIp(); + if (ext_ip != 0) { + nat_info->stun_status = 1; + nat_info->mapped_addr = ext_ip; + } else { + nat_info->stun_status = 0; + nat_info->mapped_addr = inet_addr("127.0.0.1"); + if (netinfo->RetrieveIp()) { + nat_info->mapped_addr = inet_addr(netinfo->GetIp().c_str()); + } + } + + LOG_DEBUG(Lib_NetCtl, "stun_status={} nat_type={} mapped_addr={:#x}", nat_info->stun_status, + nat_info->nat_type, nat_info->mapped_addr); return ORBIS_OK; } diff --git a/src/core/libraries/network/netctl.h b/src/core/libraries/network/netctl.h index 7f139e8c3..8bd3d3af6 100644 --- a/src/core/libraries/network/netctl.h +++ b/src/core/libraries/network/netctl.h @@ -47,6 +47,14 @@ union OrbisNetCtlInfo { u16 http_proxy_port; }; +struct OrbisNetCtlNatInfo { + u32 size; + s32 stun_status; + s32 nat_type; + u32 mapped_addr; +}; +static_assert(sizeof(OrbisNetCtlNatInfo) == 0x10); + // GetInfo codes constexpr int ORBIS_NET_CTL_INFO_DEVICE = 1; constexpr int ORBIS_NET_CTL_INFO_ETHER_ADDR = 2; @@ -100,7 +108,7 @@ int PS4_SYSV_ABI sceNetCtlGetIfStat(); int PS4_SYSV_ABI sceNetCtlGetInfo(int code, OrbisNetCtlInfo* info); int PS4_SYSV_ABI sceNetCtlGetInfoIpcInt(); int PS4_SYSV_ABI sceNetCtlGetInfoV6IpcInt(); -int PS4_SYSV_ABI sceNetCtlGetNatInfo(); +int PS4_SYSV_ABI sceNetCtlGetNatInfo(OrbisNetCtlNatInfo* nat_info); int PS4_SYSV_ABI sceNetCtlGetNatInfoIpcInt(); int PS4_SYSV_ABI sceNetCtlGetNetEvConfigInfoIpcInt(); int PS4_SYSV_ABI sceNetCtlGetResult(int eventType, int* errorCode); diff --git a/src/core/libraries/np/np_common.cpp b/src/core/libraries/np/np_common.cpp index 6fcf7ef25..e95cf3322 100644 --- a/src/core/libraries/np/np_common.cpp +++ b/src/core/libraries/np/np_common.cpp @@ -1,8 +1,16 @@ // SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include #include "common/logging/log.h" #include "core/libraries/error_codes.h" +#include "core/libraries/kernel/kernel.h" +#include "core/libraries/kernel/process.h" +#include "core/libraries/kernel/threads.h" +#include "core/libraries/kernel/threads/pthread.h" +#include "core/libraries/kernel/time.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_common.h" #include "core/libraries/np/np_error.h" @@ -10,24 +18,94 @@ namespace Libraries::Np::NpCommon { +namespace { +static_assert(sizeof(OrbisNpCalloutEntry) == 0x20); +static_assert(offsetof(OrbisNpCalloutContext, thread) == 0x00); +static_assert(offsetof(OrbisNpCalloutContext, mutex) == 0x08); +static_assert(offsetof(OrbisNpCalloutContext, cond) == 0x10); +static_assert(offsetof(OrbisNpCalloutContext, active) == 0x18); +static_assert(offsetof(OrbisNpCalloutContext, stop_requested) == 0x1c); +static_assert(offsetof(OrbisNpCalloutContext, head) == 0x20); +static_assert(sizeof(OrbisNpCalloutContext) == 0x28); + +s32 NormalizeNpCommonResult(int rc) { + if (rc <= 0) { + return rc; + } + return Libraries::Kernel::ErrnoToSceKernelError(rc); +} + +u32 NormalizeNpTryLockResult(int rc) { + const s32 sce_rc = NormalizeNpCommonResult(rc); + if (sce_rc == ORBIS_KERNEL_ERROR_EBUSY) { + return ORBIS_NP_LW_MUTEX_ERROR_BUSY; + } + return static_cast(sce_rc); +} + +void* PS4_SYSV_ABI NpCalloutThreadMain(void* arg) { + auto* ctx = static_cast(arg); + const auto mutex = &ctx->mutex; + sceNpMutexLock(mutex); + + for (;;) { + s64 now_usec = 0; + sceNpGetSystemClockUsec(&now_usec); + + auto* entry = ctx->head; + u32 wait_usec = 0; + + if (entry == nullptr) { + if (ctx->stop_requested > 0) { + break; + } + } else { + const u64 deadline = static_cast(entry->deadline_usec); + const u64 now = static_cast(now_usec); + const u64 remaining = deadline - now; + if (deadline < now || remaining == 0) { + ctx->head = entry->next; + sceNpMutexUnlock(mutex); + if (entry->handler != nullptr) { + entry->handler(entry->arg); + } + sceNpMutexLock(mutex); + continue; + } + + if (ctx->stop_requested > 0) { + break; + } + + wait_usec = static_cast(remaining > 0xffffffffULL ? 0xffffffffULL : remaining); + } + + sceNpCondTimedwait(&ctx->cond, mutex, wait_usec); + if (ctx->stop_requested > 0) { + break; + } + } + + sceNpMutexUnlock(mutex); + return nullptr; +} +} // namespace + s32 PS4_SYSV_ABI sceNpCmpNpId(OrbisNpId* np_id1, OrbisNpId* np_id2) { if (np_id1 == nullptr || np_id2 == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - // Compare data if (std::strncmp(np_id1->handle.data, np_id2->handle.data, ORBIS_NP_ONLINEID_MAX_LENGTH) != 0) { return ORBIS_NP_UTIL_ERROR_NOT_MATCH; } - // Compare opt for (u32 i = 0; i < 8; i++) { if (np_id1->opt[i] != np_id2->opt[i]) { return ORBIS_NP_UTIL_ERROR_NOT_MATCH; } } - // Compare reserved for (u32 i = 0; i < 8; i++) { if (np_id1->reserved[i] != np_id2->reserved[i]) { return ORBIS_NP_UTIL_ERROR_NOT_MATCH; @@ -42,7 +120,6 @@ s32 PS4_SYSV_ABI sceNpCmpNpIdInOrder(OrbisNpId* np_id1, OrbisNpId* np_id2, u32* return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - // Compare data u32 compare = std::strncmp(np_id1->handle.data, np_id2->handle.data, ORBIS_NP_ONLINEID_MAX_LENGTH); if (compare < 0) { @@ -53,7 +130,6 @@ s32 PS4_SYSV_ABI sceNpCmpNpIdInOrder(OrbisNpId* np_id1, OrbisNpId* np_id2, u32* return ORBIS_OK; } - // Compare opt for (u32 i = 0; i < 8; i++) { if (np_id1->opt[i] < np_id2->opt[i]) { *out_result = -1; @@ -64,7 +140,6 @@ s32 PS4_SYSV_ABI sceNpCmpNpIdInOrder(OrbisNpId* np_id1, OrbisNpId* np_id2, u32* } } - // Compare reserved for (u32 i = 0; i < 8; i++) { if (np_id1->reserved[i] < np_id2->reserved[i]) { *out_result = -1; @@ -90,13 +165,441 @@ s32 PS4_SYSV_ABI sceNpCmpOnlineId(OrbisNpOnlineId* online_id1, OrbisNpOnlineId* return ORBIS_OK; } +u32 PS4_SYSV_ABI sceNpMutexLock(Libraries::Kernel::PthreadMutexT* mutex) { + LOG_TRACE(Lib_NpCommon, "mutex={:p}", fmt::ptr(mutex)); + return static_cast( + NormalizeNpCommonResult(Libraries::Kernel::posix_pthread_mutex_lock(mutex))); +} + +u32 PS4_SYSV_ABI sceNpMutexUnlock(Libraries::Kernel::PthreadMutexT* mutex) { + LOG_TRACE(Lib_NpCommon, "mutex={:p}", fmt::ptr(mutex)); + return static_cast( + NormalizeNpCommonResult(Libraries::Kernel::posix_pthread_mutex_unlock(mutex))); +} + +u32 PS4_SYSV_ABI sceNpMutexInit(Libraries::Kernel::PthreadMutexT* mutex, const char* name, + u64 flags) { + LOG_DEBUG(Lib_NpCommon, "mutex={:p} name={:p} flags={:#x}", fmt::ptr(mutex), fmt::ptr(name), + flags); + + Libraries::Kernel::PthreadMutexAttrT attr = nullptr; + int rc = Libraries::Kernel::posix_pthread_mutexattr_init(&attr); + if (rc == 0) { + if ((flags & 1) != 0) { + rc = Libraries::Kernel::posix_pthread_mutexattr_settype( + &attr, Libraries::Kernel::PthreadMutexType::Recursive); + } + if (rc == 0) { + rc = Libraries::Kernel::scePthreadMutexInit(mutex, &attr, name); + } + Libraries::Kernel::posix_pthread_mutexattr_destroy(&attr); + } + return static_cast(NormalizeNpCommonResult(rc)); +} + +s32 PS4_SYSV_ABI sceNpMutexDestroy(Libraries::Kernel::PthreadMutexT* mutex) { + LOG_DEBUG(Lib_NpCommon, "mutex={:p}", fmt::ptr(mutex)); + return NormalizeNpCommonResult(Libraries::Kernel::posix_pthread_mutex_destroy(mutex)); +} + +u32 PS4_SYSV_ABI sceNpMutexTryLock(Libraries::Kernel::PthreadMutexT* mutex) { + LOG_TRACE(Lib_NpCommon, "mutex={:p}", fmt::ptr(mutex)); + return NormalizeNpTryLockResult(Libraries::Kernel::posix_pthread_mutex_trylock(mutex)); +} + +u32 PS4_SYSV_ABI sceNpLwMutexLock(Libraries::Kernel::PthreadMutexT* mutex) { + LOG_TRACE(Lib_NpCommon, "mutex={:p}", fmt::ptr(mutex)); + return sceNpMutexLock(mutex); +} + +u32 PS4_SYSV_ABI sceNpLwMutexUnlock(Libraries::Kernel::PthreadMutexT* mutex) { + LOG_TRACE(Lib_NpCommon, "mutex={:p}", fmt::ptr(mutex)); + return sceNpMutexUnlock(mutex); +} + +u32 PS4_SYSV_ABI sceNpLwMutexInit(Libraries::Kernel::PthreadMutexT* mutex, const char* name, + u64 flags) { + LOG_DEBUG(Lib_NpCommon, "mutex={:p} name={:p} flags={:#x}", fmt::ptr(mutex), fmt::ptr(name), + flags); + return sceNpMutexInit(mutex, name, flags); +} + +s32 PS4_SYSV_ABI sceNpLwMutexDestroy(Libraries::Kernel::PthreadMutexT* mutex) { + LOG_DEBUG(Lib_NpCommon, "mutex={:p}", fmt::ptr(mutex)); + return sceNpMutexDestroy(mutex); +} + +u32 PS4_SYSV_ABI sceNpLwMutexTryLock(Libraries::Kernel::PthreadMutexT* mutex) { + LOG_TRACE(Lib_NpCommon, "mutex={:p}", fmt::ptr(mutex)); + return sceNpMutexTryLock(mutex); +} + +s32 PS4_SYSV_ABI sceNpCondInit(Libraries::Kernel::PthreadCondT* cond, const char* name, u64 flags) { + LOG_DEBUG(Lib_NpCommon, "cond={:p} name={:p} flags={:#x}", fmt::ptr(cond), fmt::ptr(name), + flags); + + Libraries::Kernel::PthreadCondAttrT attr = nullptr; + int rc = Libraries::Kernel::posix_pthread_condattr_init(&attr); + if (rc == 0) { + rc = Libraries::Kernel::scePthreadCondInit(cond, &attr, name); + Libraries::Kernel::posix_pthread_condattr_destroy(&attr); + } + return NormalizeNpCommonResult(rc); +} + +s32 PS4_SYSV_ABI sceNpCondDestroy(Libraries::Kernel::PthreadCondT* cond) { + LOG_DEBUG(Lib_NpCommon, "cond={:p}", fmt::ptr(cond)); + return NormalizeNpCommonResult(Libraries::Kernel::posix_pthread_cond_destroy(cond)); +} + +s32 PS4_SYSV_ABI sceNpCondSignal(Libraries::Kernel::PthreadCondT* cond) { + LOG_DEBUG(Lib_NpCommon, "cond={:p}", fmt::ptr(cond)); + return NormalizeNpCommonResult(Libraries::Kernel::posix_pthread_cond_signal(cond)); +} + +s32 PS4_SYSV_ABI sceNpCondTimedwait(Libraries::Kernel::PthreadCondT* cond, + Libraries::Kernel::PthreadMutexT* mutex, u32 usec) { + LOG_DEBUG(Lib_NpCommon, "cond={:p} mutex={:p} usec={:#x}", fmt::ptr(cond), fmt::ptr(mutex), + usec); + if (usec == 0) { + return NormalizeNpCommonResult(Libraries::Kernel::posix_pthread_cond_wait(cond, mutex)); + } + const s32 rc = NormalizeNpCommonResult( + Libraries::Kernel::posix_pthread_cond_reltimedwait_np(cond, mutex, usec)); + if (rc == ORBIS_KERNEL_ERROR_ETIMEDOUT) { + return ORBIS_NP_LW_COND_ERROR_TIMEDOUT; + } + return rc; +} + +s32 PS4_SYSV_ABI sceNpCreateThread(Libraries::Kernel::PthreadT* thread, NpThreadEntry start_routine, + void* arg, s32 priority, u64 stack_size, u64 affinity_mask, + const char* name) { + LOG_DEBUG(Lib_NpCommon, + "thread={:p} start={:#x} arg={:p} priority={} stack={:#x} " + "affinity={:#x} name={:p}", + fmt::ptr(thread), reinterpret_cast(start_routine), fmt::ptr(arg), priority, + stack_size, affinity_mask, fmt::ptr(name)); + + Libraries::Kernel::PthreadAttrT attr = nullptr; + int rc = Libraries::Kernel::posix_pthread_attr_init(&attr); + if (rc != 0) { + return NormalizeNpCommonResult(rc); + } + + rc = Libraries::Kernel::posix_pthread_attr_setstacksize(&attr, static_cast(stack_size)); + if (rc == 0) { + if (priority != 0) { + s32 sdk_version = 0; + rc = Libraries::Kernel::sceKernelGetCompiledSdkVersion(&sdk_version); + if (rc >= 0 && sdk_version >= 0x2500000) { + rc = Libraries::Kernel::posix_pthread_attr_setinheritsched(&attr, 0); + if (rc == 0) { + rc = Libraries::Kernel::posix_pthread_attr_setschedpolicy( + &attr, Libraries::Kernel::SchedPolicy::Fifo); + } + } + if (rc >= 0) { + Libraries::Kernel::SchedParam param{.sched_priority = priority}; + rc = Libraries::Kernel::posix_pthread_attr_setschedparam(&attr, ¶m); + } + } + if (rc == 0 && affinity_mask != 0) { + rc = Libraries::Kernel::scePthreadAttrSetaffinity(&attr, affinity_mask); + } + if (rc == 0) { + rc = Libraries::Kernel::posix_pthread_create_name_np(thread, &attr, start_routine, arg, + name); + } + } + + Libraries::Kernel::posix_pthread_attr_destroy(&attr); + return NormalizeNpCommonResult(rc); +} + +u32 PS4_SYSV_ABI sceNpJoinThread(Libraries::Kernel::PthreadT thread, void** ret) { + LOG_DEBUG(Lib_NpCommon, "thread={:p} ret={:p}", fmt::ptr(thread), fmt::ptr(ret)); + return static_cast( + NormalizeNpCommonResult(Libraries::Kernel::posix_pthread_join(thread, ret))); +} + +s32 PS4_SYSV_ABI sceNpGetSystemClockUsec(s64* usec) { + LOG_DEBUG(Lib_NpCommon, "usec={:p}", fmt::ptr(usec)); + Libraries::Kernel::OrbisKernelTimespec ts{}; + const s32 rc = + Libraries::Kernel::sceKernelClockGettime(Libraries::Kernel::ORBIS_CLOCK_MONOTONIC, &ts); + if (rc == ORBIS_OK) { + *usec = ts.tv_sec * 1000000 + ts.tv_nsec / 1000; + } + return rc; +} + +s32 PS4_SYSV_ABI sceNpGetPlatformType(const OrbisNpId* npid) { + LOG_DEBUG(Lib_NpCommon, "np_id={:p}", fmt::ptr(npid)); + + if (npid == nullptr) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + + static constexpr u32 kTagPs3 = 0x00337370; // "ps3" + static constexpr u32 kTagVita = 0x32707370; // "psp2(VITA)" + static constexpr u32 kTagPs4 = 0x00347370; // "ps4" + + if (npid->opt[4] == 0) { + return OrbisNpPlatformType::None; + } + u32 tag = 0; + std::memcpy(&tag, npid->opt + 4, sizeof(tag)); + switch (tag) { + case kTagPs3: + return OrbisNpPlatformType::PS3; + case kTagVita: + return OrbisNpPlatformType::Vita; + case kTagPs4: + return OrbisNpPlatformType::PS4; + default: + return ORBIS_NP_ERROR_INVALID_PLATFORM_TYPE; + } +} + +s32 PS4_SYSV_ABI sceNpIntIsValidOnlineId(const OrbisNpOnlineId* id) { + LOG_DEBUG(Lib_NpCommon, "online_id={:p}", fmt::ptr(id)); + + if (id == nullptr || id->term != 0) { + return 0; + } + + const size_t len = strnlen(id->data, ORBIS_NP_ONLINEID_MAX_LENGTH + 1); + if (len < 3 || len > ORBIS_NP_ONLINEID_MAX_LENGTH) { + return 0; + } + + const unsigned char first = static_cast(id->data[0]); + if (!std::isalnum(first)) { + return 0; + } + + for (size_t i = 1; i < len; ++i) { + const unsigned char c = static_cast(id->data[i]); + if (std::isalnum(c) || c == '_' || c == '-') { + continue; + } + return 0; + } + + return 1; +} + +s32 PS4_SYSV_ABI sceNpCalloutInitCtx(OrbisNpCalloutContext* callout_ctx, const char* name, + u64 stack_size, s32 priority, u64 affinity_mask) { + LOG_DEBUG(Lib_NpCommon, "ctx={:p} name={:p} stack={:#x} priority={} affinity={:#x}", + fmt::ptr(callout_ctx), fmt::ptr(name), stack_size, priority, affinity_mask); + + s32 rc = ORBIS_NP_CALLOUT_ERROR_ALREADY_INITIALIZED; + if (callout_ctx->active < 1) { + callout_ctx->stop_requested = 0; + callout_ctx->head = nullptr; + rc = static_cast(sceNpMutexInit(&callout_ctx->mutex, name, 1)); + if (rc >= 0) { + rc = sceNpCondInit(&callout_ctx->cond, name, 0); + if (rc >= 0) { + rc = sceNpCreateThread(&callout_ctx->thread, NpCalloutThreadMain, callout_ctx, + priority, stack_size, affinity_mask, name); + if (rc >= 0) { + callout_ctx->active = 1; + return ORBIS_OK; + } + sceNpCondDestroy(&callout_ctx->cond); + } + sceNpMutexDestroy(&callout_ctx->mutex); + } + } + return rc; +} + +s32 PS4_SYSV_ABI sceNpCalloutStartOnCtx(OrbisNpCalloutContext* callout_ctx, + OrbisNpCalloutEntry* entry, u64 delay_usec, u64 handler, + u64 arg) { + LOG_DEBUG(Lib_NpCommon, "ctx={:p} callout={:p} delay={:#x} handler={:p} arg={:#x}", + fmt::ptr(callout_ctx), fmt::ptr(entry), delay_usec, + fmt::ptr(reinterpret_cast(static_cast(handler))), arg); + + if (callout_ctx->active == 0) { + return ORBIS_NP_CALLOUT_ERROR_NOT_INITIALIZED; + } + + entry->next = nullptr; + entry->handler = reinterpret_cast(handler); + entry->arg = arg; + sceNpGetSystemClockUsec(&entry->deadline_usec); + entry->deadline_usec += static_cast(static_cast(delay_usec)); + + sceNpMutexLock(&callout_ctx->mutex); + + for (auto* cur = callout_ctx->head; cur != nullptr; cur = cur->next) { + if (cur == entry) { + sceNpMutexUnlock(&callout_ctx->mutex); + return ORBIS_NP_CALLOUT_ERROR_DUPLICATE_ENTRY; + } + } + + OrbisNpCalloutEntry** prev_next = &callout_ctx->head; + for (auto* cur = callout_ctx->head; cur != nullptr; cur = cur->next) { + if (cur->deadline_usec >= entry->deadline_usec) { + break; + } + prev_next = &cur->next; + } + + entry->next = *prev_next; + *prev_next = entry; + sceNpCondSignal(&callout_ctx->cond); + sceNpMutexUnlock(&callout_ctx->mutex); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpCalloutStartOnCtx64(OrbisNpCalloutContext* callout_ctx, + OrbisNpCalloutEntry* entry, s64 delay_usec, u64 handler, + u64 arg) { + LOG_DEBUG(Lib_NpCommon, "ctx={:p} callout={:p} delay={} handler={:p} arg={:#x}", + fmt::ptr(callout_ctx), fmt::ptr(entry), delay_usec, + fmt::ptr(reinterpret_cast(static_cast(handler))), arg); + + if (callout_ctx->active == 0) { + return ORBIS_NP_CALLOUT_ERROR_NOT_INITIALIZED; + } + + entry->next = nullptr; + entry->handler = reinterpret_cast(handler); + entry->arg = arg; + sceNpGetSystemClockUsec(&entry->deadline_usec); + entry->deadline_usec += delay_usec; + + sceNpMutexLock(&callout_ctx->mutex); + + for (auto* cur = callout_ctx->head; cur != nullptr; cur = cur->next) { + if (cur == entry) { + sceNpMutexUnlock(&callout_ctx->mutex); + return ORBIS_NP_CALLOUT_ERROR_DUPLICATE_ENTRY; + } + } + + OrbisNpCalloutEntry** prev_next = &callout_ctx->head; + for (auto* cur = callout_ctx->head; cur != nullptr; cur = cur->next) { + if (cur->deadline_usec >= entry->deadline_usec) { + break; + } + prev_next = &cur->next; + } + + entry->next = *prev_next; + *prev_next = entry; + sceNpCondSignal(&callout_ctx->cond); + sceNpMutexUnlock(&callout_ctx->mutex); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpCalloutStopOnCtx(OrbisNpCalloutContext* callout_ctx, + OrbisNpCalloutEntry* entry, u32* removed) { + LOG_DEBUG(Lib_NpCommon, "ctx={:p} callout={:p} removed={:p}", fmt::ptr(callout_ctx), + fmt::ptr(entry), fmt::ptr(removed)); + + if (callout_ctx->active == 0) { + return ORBIS_NP_CALLOUT_ERROR_NOT_INITIALIZED; + } + + sceNpMutexLock(&callout_ctx->mutex); + u32 found = 0; + OrbisNpCalloutEntry** prev_next = &callout_ctx->head; + for (auto* cur = callout_ctx->head; cur != nullptr; cur = cur->next) { + if (cur == entry) { + *prev_next = cur->next; + found = 1; + break; + } + prev_next = &cur->next; + } + if (removed != nullptr) { + *removed = found; + } + sceNpMutexUnlock(&callout_ctx->mutex); + return ORBIS_OK; +} + +void PS4_SYSV_ABI sceNpCalloutTermCtx(OrbisNpCalloutContext* callout_ctx) { + LOG_DEBUG(Lib_NpCommon, "ctx={:p}", fmt::ptr(callout_ctx)); + + if (callout_ctx->active != 0) { + sceNpMutexLock(&callout_ctx->mutex); + callout_ctx->stop_requested = 1; + sceNpCondSignal(&callout_ctx->cond); + sceNpMutexUnlock(&callout_ctx->mutex); + sceNpJoinThread(callout_ctx->thread, nullptr); + sceNpCondDestroy(&callout_ctx->cond); + sceNpMutexDestroy(&callout_ctx->mutex); + callout_ctx->active = 0; + } +} + void RegisterLib(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("i8UmXTSq7N4", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCmpNpId); LIB_FUNCTION("TcwEFnakiSc", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCmpNpIdInOrder); LIB_FUNCTION("dj+O5aD2a0Q", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCmpOnlineId); + LIB_FUNCTION("1a+iY5YUJcI", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCondDestroy); + LIB_FUNCTION("9+m5nRdJ-wQ", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCalloutInitCtx); + LIB_FUNCTION("AqJ4xkWsV+I", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCalloutTermCtx); + LIB_FUNCTION("18j+qk6dRwk", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpLwMutexLock); + LIB_FUNCTION("1CiXI-MyEKs", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpLwMutexInit); + LIB_FUNCTION("4zxevggtYrQ", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpLwMutexDestroy); + LIB_FUNCTION("CQG2oyx1-nM", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpLwMutexUnlock); + LIB_FUNCTION("DuslmoqQ+nk", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpMutexTryLock); + LIB_FUNCTION("EjMsfO3GCIA", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpJoinThread); + LIB_FUNCTION("fClnlkZmA6k", "libSceNpCommonCompat", 1, "libSceNpCommon", + sceNpCalloutStartOnCtx); + LIB_FUNCTION("fhJ5uKzcn0w", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCreateThread); + LIB_FUNCTION("hkeX9iuCwlI", "libSceNpCommonCompat", 1, "libSceNpCommon", + sceNpIntIsValidOnlineId); + LIB_FUNCTION("hp0kVgu5Fxw", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpLwMutexTryLock); + LIB_FUNCTION("in19gH7G040", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCalloutStopOnCtx); + LIB_FUNCTION("lQ11BpMM4LU", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpMutexDestroy); + LIB_FUNCTION("lpr66Gby8dQ", "libSceNpCommonCompat", 1, "libSceNpCommon", + sceNpCalloutStartOnCtx64); + LIB_FUNCTION("oZyb9ktuCpA", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpMutexUnlock); + LIB_FUNCTION("PVVsRmMkO1g", "libSceNpCommonCompat", 1, "libSceNpCommon", + sceNpGetSystemClockUsec); + LIB_FUNCTION("q2tsVO3lM4A", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCondInit); + LIB_FUNCTION("r9Bet+s6fKc", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpMutexLock); + LIB_FUNCTION("sXVQUIGmk2U", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpGetPlatformType); + LIB_FUNCTION("ss2xO9IJxKQ", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCondTimedwait); + LIB_FUNCTION("uEwag-0YZPc", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpMutexInit); + LIB_FUNCTION("uMJFOA62mVU", "libSceNpCommonCompat", 1, "libSceNpCommon", sceNpCondSignal); LIB_FUNCTION("i8UmXTSq7N4", "libSceNpCommon", 1, "libSceNpCommon", sceNpCmpNpId); LIB_FUNCTION("TcwEFnakiSc", "libSceNpCommon", 1, "libSceNpCommon", sceNpCmpNpIdInOrder); LIB_FUNCTION("dj+O5aD2a0Q", "libSceNpCommon", 1, "libSceNpCommon", sceNpCmpOnlineId); + LIB_FUNCTION("1a+iY5YUJcI", "libSceNpCommon", 1, "libSceNpCommon", sceNpCondDestroy); + LIB_FUNCTION("9+m5nRdJ-wQ", "libSceNpCommon", 1, "libSceNpCommon", sceNpCalloutInitCtx); + LIB_FUNCTION("AqJ4xkWsV+I", "libSceNpCommon", 1, "libSceNpCommon", sceNpCalloutTermCtx); + LIB_FUNCTION("18j+qk6dRwk", "libSceNpCommon", 1, "libSceNpCommon", sceNpLwMutexLock); + LIB_FUNCTION("1CiXI-MyEKs", "libSceNpCommon", 1, "libSceNpCommon", sceNpLwMutexInit); + LIB_FUNCTION("4zxevggtYrQ", "libSceNpCommon", 1, "libSceNpCommon", sceNpLwMutexDestroy); + LIB_FUNCTION("CQG2oyx1-nM", "libSceNpCommon", 1, "libSceNpCommon", sceNpLwMutexUnlock); + LIB_FUNCTION("DuslmoqQ+nk", "libSceNpCommon", 1, "libSceNpCommon", sceNpMutexTryLock); + LIB_FUNCTION("EjMsfO3GCIA", "libSceNpCommon", 1, "libSceNpCommon", sceNpJoinThread); + LIB_FUNCTION("fClnlkZmA6k", "libSceNpCommon", 1, "libSceNpCommon", sceNpCalloutStartOnCtx); + LIB_FUNCTION("fhJ5uKzcn0w", "libSceNpCommon", 1, "libSceNpCommon", sceNpCreateThread); + LIB_FUNCTION("hkeX9iuCwlI", "libSceNpCommon", 1, "libSceNpCommon", sceNpIntIsValidOnlineId); + LIB_FUNCTION("hp0kVgu5Fxw", "libSceNpCommon", 1, "libSceNpCommon", sceNpLwMutexTryLock); + LIB_FUNCTION("in19gH7G040", "libSceNpCommon", 1, "libSceNpCommon", sceNpCalloutStopOnCtx); + LIB_FUNCTION("lQ11BpMM4LU", "libSceNpCommon", 1, "libSceNpCommon", sceNpMutexDestroy); + LIB_FUNCTION("lpr66Gby8dQ", "libSceNpCommon", 1, "libSceNpCommon", sceNpCalloutStartOnCtx64); + LIB_FUNCTION("oZyb9ktuCpA", "libSceNpCommon", 1, "libSceNpCommon", sceNpMutexUnlock); + LIB_FUNCTION("PVVsRmMkO1g", "libSceNpCommon", 1, "libSceNpCommon", sceNpGetSystemClockUsec); + LIB_FUNCTION("q2tsVO3lM4A", "libSceNpCommon", 1, "libSceNpCommon", sceNpCondInit); + LIB_FUNCTION("r9Bet+s6fKc", "libSceNpCommon", 1, "libSceNpCommon", sceNpMutexLock); + LIB_FUNCTION("sXVQUIGmk2U", "libSceNpCommon", 1, "libSceNpCommon", sceNpGetPlatformType); + LIB_FUNCTION("ss2xO9IJxKQ", "libSceNpCommon", 1, "libSceNpCommon", sceNpCondTimedwait); + LIB_FUNCTION("uEwag-0YZPc", "libSceNpCommon", 1, "libSceNpCommon", sceNpMutexInit); + LIB_FUNCTION("uMJFOA62mVU", "libSceNpCommon", 1, "libSceNpCommon", sceNpCondSignal); }; -} // namespace Libraries::Np::NpCommon \ No newline at end of file +} // namespace Libraries::Np::NpCommon diff --git a/src/core/libraries/np/np_common.h b/src/core/libraries/np/np_common.h index a130f9c1d..7ff5a81c0 100644 --- a/src/core/libraries/np/np_common.h +++ b/src/core/libraries/np/np_common.h @@ -4,6 +4,8 @@ #pragma once #include "common/types.h" +#include "core/libraries/kernel/threads/pthread.h" +#include "core/libraries/np/np_types.h" namespace Core::Loader { class SymbolsResolver; @@ -17,5 +19,58 @@ struct OrbisNpFreeKernelMemoryArgs { void* addr; }; +using NpThreadEntry = void* PS4_SYSV_ABI (*)(void*); +using OrbisNpCalloutHandler = void PS4_SYSV_ABI (*)(u64); + +struct OrbisNpCalloutEntry { + OrbisNpCalloutEntry* next; + OrbisNpCalloutHandler handler; + s64 deadline_usec; + u64 arg; +}; + +struct OrbisNpCalloutContext { + Libraries::Kernel::PthreadT thread; + Libraries::Kernel::PthreadMutexT mutex; + Libraries::Kernel::PthreadCondT cond; + s32 active; + s32 stop_requested; + OrbisNpCalloutEntry* head; +}; + +u32 PS4_SYSV_ABI sceNpMutexLock(Libraries::Kernel::PthreadMutexT* mutex); +u32 PS4_SYSV_ABI sceNpMutexUnlock(Libraries::Kernel::PthreadMutexT* mutex); +u32 PS4_SYSV_ABI sceNpMutexInit(Libraries::Kernel::PthreadMutexT* mutex, const char* name, + u64 flags); +s32 PS4_SYSV_ABI sceNpMutexDestroy(Libraries::Kernel::PthreadMutexT* mutex); +u32 PS4_SYSV_ABI sceNpMutexTryLock(Libraries::Kernel::PthreadMutexT* mutex); +u32 PS4_SYSV_ABI sceNpLwMutexLock(Libraries::Kernel::PthreadMutexT* mutex); +u32 PS4_SYSV_ABI sceNpLwMutexUnlock(Libraries::Kernel::PthreadMutexT* mutex); +u32 PS4_SYSV_ABI sceNpLwMutexInit(Libraries::Kernel::PthreadMutexT* mutex, const char* name, + u64 flags); +s32 PS4_SYSV_ABI sceNpLwMutexDestroy(Libraries::Kernel::PthreadMutexT* mutex); +u32 PS4_SYSV_ABI sceNpLwMutexTryLock(Libraries::Kernel::PthreadMutexT* mutex); +s32 PS4_SYSV_ABI sceNpCondInit(Libraries::Kernel::PthreadCondT* cond, const char* name, u64 flags); +s32 PS4_SYSV_ABI sceNpCondDestroy(Libraries::Kernel::PthreadCondT* cond); +s32 PS4_SYSV_ABI sceNpCondSignal(Libraries::Kernel::PthreadCondT* cond); +s32 PS4_SYSV_ABI sceNpCondTimedwait(Libraries::Kernel::PthreadCondT* cond, + Libraries::Kernel::PthreadMutexT* mutex, u32 usec); +s32 PS4_SYSV_ABI sceNpCreateThread(Libraries::Kernel::PthreadT* thread, NpThreadEntry start_routine, + void* arg, s32 priority, u64 stack_size, u64 affinity_mask, + const char* name); +u32 PS4_SYSV_ABI sceNpJoinThread(Libraries::Kernel::PthreadT thread, void** ret); +s32 PS4_SYSV_ABI sceNpGetSystemClockUsec(s64* usec); +s32 PS4_SYSV_ABI sceNpGetPlatformType(const OrbisNpId* np_id); +s32 PS4_SYSV_ABI sceNpIntIsValidOnlineId(const OrbisNpOnlineId* id); +s32 PS4_SYSV_ABI sceNpCalloutInitCtx(OrbisNpCalloutContext* ctx, const char* name, u64 stack_size, + s32 priority, u64 affinity_mask); +s32 PS4_SYSV_ABI sceNpCalloutStartOnCtx(OrbisNpCalloutContext* ctx, OrbisNpCalloutEntry* callout, + u64 delay_usec, u64 handler, u64 arg); +s32 PS4_SYSV_ABI sceNpCalloutStartOnCtx64(OrbisNpCalloutContext* ctx, OrbisNpCalloutEntry* callout, + s64 delay_usec, u64 handler, u64 arg); +s32 PS4_SYSV_ABI sceNpCalloutStopOnCtx(OrbisNpCalloutContext* ctx, OrbisNpCalloutEntry* callout, + u32* removed); +void PS4_SYSV_ABI sceNpCalloutTermCtx(OrbisNpCalloutContext* ctx); + void RegisterLib(Core::Loader::SymbolsResolver* sym); -} // namespace Libraries::Np::NpCommon \ No newline at end of file +} // namespace Libraries::Np::NpCommon diff --git a/src/core/libraries/np/np_error.h b/src/core/libraries/np/np_error.h index 1753aac8f..c2fe511d1 100644 --- a/src/core/libraries/np/np_error.h +++ b/src/core/libraries/np/np_error.h @@ -10,6 +10,7 @@ constexpr int ORBIS_NP_ERROR_ALREADY_INITIALIZED = 0x80550001; constexpr int ORBIS_NP_ERROR_NOT_INITIALIZED = 0x80550002; constexpr int ORBIS_NP_ERROR_INVALID_ARGUMENT = 0x80550003; constexpr int ORBIS_NP_ERROR_UNKNOWN_PLATFORM_TYPE = 0x80550004; +constexpr int ORBIS_NP_ERROR_INVALID_PLATFORM_TYPE = 0x80550004; constexpr int ORBIS_NP_ERROR_OUT_OF_MEMORY = 0x80550005; constexpr int ORBIS_NP_ERROR_SIGNED_OUT = 0x80550006; constexpr int ORBIS_NP_ERROR_USER_NOT_FOUND = 0x80550007; @@ -37,6 +38,9 @@ constexpr int ORBIS_NP_ERROR_TITLE_ID_IN_PARAM_SFO_NOT_EXIST = 0x8055001C; constexpr int ORBIS_NP_ERROR_CALLBACK_MAX = 0x8055001D; constexpr int ORBIS_NP_ERROR_INVALID_NP_TITLE_ID = 0x8055001E; constexpr int ORBIS_NP_ERROR_ONLINE_ID_CHANGED = 0x8055001F; +constexpr int ORBIS_NP_INT_ERROR_INVALID_ARGUMENT = 0x8055A283; +constexpr int ORBIS_NP_SCORE_INVALID_ARGUMENT = 0x80550704; +constexpr int ORBIS_NP_WEB_API_INVALID_ARGUMENT = 0x80552902; // NP Util (0x80550601 - 0x8055060e) constexpr int ORBIS_NP_UTIL_ERROR_INVALID_ARGUMENT = 0x80550601; @@ -674,3 +678,107 @@ constexpr int ORBIS_NP_GAME_INTENT_ERROR_VALUE_NOT_FOUND = 0x80553807; // NP ASM Client (0x8055a287 - 0x8055a289) constexpr int ORBIS_NP_ASM_CLIENT_ERROR_ABORTED = 0x8055A287; constexpr int ORBIS_NP_ASM_CLIENT_ERROR_NP_SERVICE_LAVEL_NOT_MATCH = 0x8055A289; + +// NP Callout / LW Mutex (0x80558001 - 0x8055800F) +constexpr int ORBIS_NP_CALLOUT_ERROR_NOT_INITIALIZED = 0x80558001; +constexpr int ORBIS_NP_CALLOUT_ERROR_ALREADY_INITIALIZED = 0x80558002; +constexpr int ORBIS_NP_CALLOUT_ERROR_DUPLICATE_ENTRY = 0x80558006; +constexpr int ORBIS_NP_LW_COND_ERROR_TIMEDOUT = 0x8055800B; +constexpr int ORBIS_NP_LW_MUTEX_ERROR_BUSY = 0x8055800F; + +// NP Signaling internal errors (0x80559601 - 0x8055960F) +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_INVALID_ARGUMENT = 0x80559601; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_DATA_TOO_BIG = 0x80559602; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_INVALID_ACTION = 0x80559603; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_CONN_NOT_ACTIVATED = 0x80559604; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_OUT_OF_MEMORY = 0x80559605; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_PARSER_FAILED = 0x80559606; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_INVALID_NAMESPACE = 0x80559607; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_ALLOCATOR = 0x80559608; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_IPC_CONTEXT_NOT_FOUND = 0x80559609; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_ALREADY_INITIALIZED = 0x8055960A; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_NOT_INITIALIZED = 0x8055960B; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_XMPP_IPC_INVALID_MSG_TYPE = 0x8055960C; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_XMPP_IPC_INVALID_ID = 0x8055960D; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_INVALID_DATA_TYPE = 0x8055960E; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_ERROR_CALLBACK_QUEUE_OVERFLOW = 0x8055960F; + +// NP Signaling internal socket / core errors +constexpr int ORBIS_NP_SIGNALING_INTERNAL_CORE_ERROR_80550E81 = 0x80550E81; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_CORE_ERROR_80550E82 = 0x80550E82; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_CORE_ERROR_80550E83 = 0x80550E83; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_SERVICE_ERROR_TRANSIENT_SOCKET_ALLOC = 0x80559611; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_SERVICE_ERROR_POLL_PENDING = 0x80559D1B; + +// NP Signaling internal socket errors +constexpr int ORBIS_NP_SIGNALING_INTERNAL_SOCKET_ERROR_BASE = 0x80410100; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_SOCKET_ERROR_SHORT_IO = 0x80410104; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_SOCKET_ERROR_ECHO_RECV_REBUILD = 0x80410123; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_SOCKET_ERROR_80410124 = 0x80410124; + +// NP Signaling internal STUN errors +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_ALREADY_INITIALIZED = 0x80412401; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_NOT_INITIALIZED = 0x80412402; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_TIMEOUT = 0x80412403; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_BAD_RESPONSE_HEADER = 0x80412404; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_BAD_ATTRIBUTE_LENGTH = 0x80412405; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_PARSE_FAILED = 0x80412406; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_UNSUPPORTED_ADDRESS_FAMILY = 0x80412407; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_NO_SLOT = 0x80412409; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_BAD_QUEUE_INDEX = 0x8041240A; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_MESSAGE_INTEGRITY = 0x8041240B; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_BAD_XOR_MAPPED_ADDRESS = 0x8041240C; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_ALLOC_FAILED = 0x8041240D; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_CANCELLED = 0x8041240E; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_NO_RESULT = 0x8041240F; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_QUEUE_SEND_FAILED = 0x80412410; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_BAD_SERVICE_FLAGS = 0x80412411; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_STUN_ERROR_SERVICE_UNAVAILABLE = 0x80412412; + +// NP Signaling internal UPNP errors +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_INITIALIZATION_FAILED = 0x80414101; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_NET_UNAVAILABLE = 0x80414102; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_INVALID_ARGUMENT = 0x80414105; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_DISCOVERY_PENDING = 0x80414108; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_HTTP_SOCKET_FAILED = 0x8041410A; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SERVICE_NOT_FOUND = 0x8041410B; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_INVALID_ACTION = 0x80414171; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_INVALID_ARGS = 0x80414172; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_INVALID_VAR = 0x80414173; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_ACTION_FAILED = 0x80414174; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_WILDCARD_NOT_PERMITTED = 0x80414175; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_EXTERNAL_PORT_WILDCARD_ONLY = + 0x80414176; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_CONFLICT_MAPPING_ENTRY = 0x80414177; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_SAME_PORT_VALUES_REQUIRED = + 0x80414178; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_ONLY_PERMANENT_LEASE_SUPPORTED = + 0x80414179; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_REMOTE_HOST_WILDCARD_ONLY = + 0x8041417A; +constexpr int + ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_EXTERNAL_PORT_WILDCARD_NOT_PERMITTED = + 0x8041417B; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_NO_PORT_MAPS_AVAILABLE = 0x8041417C; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SOAP_FAULT_GENERIC = 0x8041417D; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_8041417E = 0x8041417E; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_80414181 = 0x80414181; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_80414184 = 0x80414184; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_FORMAT_OR_BUFFER = 0x80414185; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SSDP_ADDR = 0x80414186; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SSDP_TIMEOUT = 0x80414187; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_80414188 = 0x80414188; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_HTTP_URL_INVALID = 0x80414189; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_SSDP_IGNORED = 0x8041418A; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_8041418B = 0x8041418B; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_CONTENT_LENGTH_MISMATCH = 0x8041418D; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_UNSUPPORTED_ACTION = 0x8041418F; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_80414190 = 0x80414190; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_XML_ARGUMENT = 0x804141A0; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_XML_INPUT = 0x804141A1; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_UPNP_ERROR_XML_SPACE = 0x804141A2; + +// NP Signaling internal misc errors +constexpr int ORBIS_NP_SIGNALING_INTERNAL_SHA1_ERROR_INVALID_ARGUMENT = 0x80480300; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_SHA1_ERROR_INACTIVE_CONTEXT = 0x80480302; +constexpr int ORBIS_NP_SIGNALING_INTERNAL_HMAC_ERROR_INVALID_ARGUMENT = 0x80481000; diff --git a/src/core/libraries/np/np_signaling/np_signaling.cpp b/src/core/libraries/np/np_signaling/np_signaling.cpp new file mode 100644 index 000000000..8ca259b9d --- /dev/null +++ b/src/core/libraries/np/np_signaling/np_signaling.cpp @@ -0,0 +1,886 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "common/logging/log.h" +#include "common/singleton.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" +#include "core/libraries/network/net.h" +#include "core/libraries/network/net_util.h" +#include "core/libraries/network/netctl.h" +#include "core/libraries/np/np_common.h" +#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_manager.h" +#include "core/libraries/np/np_signaling/np_signaling.h" +#include "core/libraries/np/np_signaling/np_signaling_helpers.h" +#include "core/libraries/np/np_signaling/np_signaling_state.h" +#include "core/libraries/np/np_signaling/np_signaling_stubs.h" + +namespace Libraries::Np::NpSignaling { + +using Libraries::Net::sceNetNtohs; + +s32 PS4_SYSV_ABI sceNpSignalingInitialize(s64 memorySize, s32 threadPriority, s32 cpuAffinityMask, + s64 threadStackSize) { + if (g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_ALREADY_INITIALIZED; + } + InitSignalingMutex(); + SignalingMutexGuard lock; + + u32 app_type_4 = 0; + const s32 app_type_rc = Helpers::CheckInitializeAppType(&app_type_4); + if (app_type_rc < 0) { + return app_type_rc; + } + if (app_type_4 != 0) { + return ORBIS_NP_SIGNALING_ERROR_PROHIBITED_TO_USE; + } + + if (memorySize == 0) { + memorySize = 0x20000; + } + if (threadPriority == 0) { + threadPriority = 700; + } + if (threadStackSize == 0) { + threadStackSize = 0x4000; + } + + LOG_INFO(Lib_NpSignaling, + "memorySize={} threadPriority={} cpuAffinityMask={} threadStackSize={}", memorySize, + threadPriority, cpuAffinityMask, threadStackSize); + + const s32 heap_rc = Helpers::InitSignalingHeap(memorySize); + if (heap_rc < 0) { + return heap_rc; + } + + RegisterRuntimeHooks(); + + const s32 check_app_type_rc = Helpers::CheckAppType(); + if (check_app_type_rc < 0) { + Helpers::ShutdownSignalingHeap(); + return check_app_type_rc; + } + + g_initialized = true; + + const s32 rc = Helpers::StartMainRuntime(threadPriority, cpuAffinityMask, threadStackSize); + if (rc < 0) { + g_initialized = false; + Helpers::ShutdownSignalingHeap(); + return rc; + } + + const s32 echo_rc = Helpers::StartEchoRuntime(threadPriority, cpuAffinityMask); + if (echo_rc < 0) { + g_initialized = false; + Helpers::ShutdownRuntime(); + Helpers::ShutdownSignalingHeap(); + return echo_rc; + } + + const s32 callout_rc = NpCommon::sceNpCalloutInitCtx( + &g_callout_ctx, "SceNpSignalingCallout", static_cast(threadStackSize), threadPriority, + static_cast(cpuAffinityMask)); + if (callout_rc < 0) { + g_initialized = false; + Helpers::ShutdownRuntime(); + Helpers::ShutdownSignalingHeap(); + return callout_rc; + } + g_callout_ctx_active = true; + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingTerminate() { + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + { + SignalingMutexGuard lock; + g_initialized = false; + } + + Helpers::ShutdownRuntime(); + + if (g_callout_ctx_active) { + NpCommon::sceNpCalloutTermCtx(&g_callout_ctx); + g_callout_ctx_active = false; + } + + Helpers::ShutdownSignalingHeap(); + + { + SignalingMutexGuard lock; + g_contexts.clear(); + g_connections.clear(); + g_npid_to_conn.clear(); + g_peer_netinfo_results.clear(); + } + + DestroySignalingMutex(); + + LOG_INFO(Lib_NpSignaling, "cleared all state"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingCreateContext(const void* npId, void* callback, void* callbackArg, + OrbisNpSignalingContextId* outContextId) { + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + } + if (!npId || !outContextId) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + OrbisNpId owner_npid{}; + OrbisNpOnlineId owner_online_id{}; + if (NormalizeNpId(npId, &owner_npid, &owner_online_id) != ORBIS_OK) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + s32 ctx_id = 0; + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + + ctx_id = AllocateContextIdLocked(); + if (ctx_id < 0) { + return ORBIS_NP_SIGNALING_ERROR_CTXID_NOT_AVAILABLE; + } + + NpSignalingContext& ctx = g_contexts[ctx_id]; + ctx.callback = reinterpret_cast(callback); + ctx.callback_arg = callbackArg; + ctx.active = true; + ctx.owner_npid = owner_npid; + ctx.owner_online_id = owner_online_id; + ctx.flags = 0x2; + ctx.compiled_sdk_version = CaptureCompiledSdkVersion(); + ctx.activate_budget_us = kActivateBudgetMaxUs; + ctx.activate_last_update_us = NowUs(); + ctx.bound_port = Stubs::ConfiguredPort(); + *outContextId = ctx_id; + + LOG_INFO(Lib_NpSignaling, "ctxId={} owner='{}' callback={} arg={} sdk={:#x} p2p_port={}", + ctx_id, OnlineIdToString(owner_online_id), fmt::ptr(callback), + fmt::ptr(callbackArg), ctx.compiled_sdk_version, ctx.bound_port); + } + + if (Stubs::TransportIsReady()) { + SendStunPing(ctx_id); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingCreateContextA(s32 userId, void* callback, void* callbackArg, + OrbisNpSignalingContextId* outContextId) { + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + } + if (userId == -1 || !outContextId) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + OrbisNpId owner_npid{}; + const s32 npid_rc = NpManager::sceNpGetNpId(userId, &owner_npid); + if (npid_rc < 0) { + return npid_rc; + } + // Account id isn't needed without matchmaking wired up; leave it 0 for now. + const OrbisNpAccountId account_id = 0; + + s32 ctx_id = 0; + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + + ctx_id = AllocateContextIdLocked(); + if (ctx_id < 0) { + return ORBIS_NP_SIGNALING_ERROR_CTXID_NOT_AVAILABLE; + } + + NpSignalingContext& ctx = g_contexts[ctx_id]; + ctx.callback = reinterpret_cast(callback); + ctx.callback_arg = callbackArg; + ctx.active = true; + ctx.owner_npid = owner_npid; + ctx.owner_online_id = owner_npid.handle; + ctx.flags = 0x4 | 0x2; + ctx.account_id = account_id; + ctx.platform_type = 0; + ctx.compiled_sdk_version = CaptureCompiledSdkVersion(); + ctx.activate_budget_us = kActivateBudgetMaxUs; + ctx.activate_last_update_us = NowUs(); + ctx.bound_port = Stubs::ConfiguredPort(); + *outContextId = ctx_id; + + LOG_INFO(Lib_NpSignaling, "ctxId={} userId={} owner='{}' accountId={:#x}", ctx_id, userId, + OnlineIdToString(ctx.owner_online_id), account_id); + } + + if (Stubs::TransportIsReady()) { + SendStunPing(ctx_id); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingDeleteContext(OrbisNpSignalingContextId ctxId) { + std::vector active_conns; + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + + LOG_INFO(Lib_NpSignaling, "ctxId={}", ctxId); + + for (const auto& [cid, ci] : g_connections) { + if (ci.ctx_id == ctxId && ci.status != ORBIS_NP_SIGNALING_CONN_STATUS_INACTIVE) { + active_conns.push_back(cid); + } + } + } + + for (const s32 cid : active_conns) { + CloseConnectionAndDispatchDead(cid, ORBIS_NP_SIGNALING_ERROR_TERMINATED_BY_PEER); + } + + { + SignalingMutexGuard lock; + RemoveContextConnectionsLocked(ctxId); + for (auto it = g_peer_netinfo_results.begin(); it != g_peer_netinfo_results.end();) { + it = (it->second.ctx_id == ctxId) ? g_peer_netinfo_results.erase(it) : std::next(it); + } + g_contexts.erase(ctxId); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingActivateConnection(OrbisNpSignalingContextId ctxId, + const void* peerNpId, + OrbisNpSignalingConnectionId* outConnId) { + LOG_INFO(Lib_NpSignaling, "t={} ctxId={} peerNpId={:p} connId={:p}", NowMs(), ctxId, peerNpId, + fmt::ptr(outConnId)); + + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + } + if (!peerNpId || !outConnId) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + OrbisNpId peer_npid{}; + OrbisNpOnlineId peer_online_id{}; + if (NormalizeNpId(peerNpId, &peer_npid, &peer_online_id) != ORBIS_OK) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + const std::string peer_online_id_str = OnlineIdToString(peer_online_id); + + s32 cid = 0; + bool reused_established = false; + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + const auto ctx_it = g_contexts.find(ctxId); + if (ctx_it == g_contexts.end() || !ctx_it->second.active) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + if (std::memcmp(&ctx_it->second.owner_npid, &peer_npid, sizeof(peer_npid)) == 0) { + return ORBIS_NP_SIGNALING_ERROR_OWN_NP_ID; + } + if (!ConsumeActivationBudgetGatedLocked(ctx_it->second)) { + return ORBIS_NP_SIGNALING_ERROR_EXCEED_RATE_LIMIT; + } + + const CtxNpIdKey lookup_key = MakeCtxNpIdKey(ctxId, peer_npid); + const auto existing_it = g_npid_to_conn.find(lookup_key); + if (existing_it != g_npid_to_conn.end()) { + const auto conn_it = g_connections.find(existing_it->second); + if (conn_it != g_connections.end() && conn_it->second.state == ConnState::Inactive) { + RemoveConnectionLocked(existing_it->second); + } else if (conn_it != g_connections.end()) { + cid = existing_it->second; + ConnectionInfo& ci = conn_it->second; + ci.locally_activated = true; + if (ci.state == ConnState::Established) { + ClearLingerAndTimeoutLocked(cid); + reused_established = true; + } else { + ClearLingerAndTimeoutLocked(cid); + } + } + } + bool created_new = false; + if (cid == 0) { + cid = AllocateConnectionIdLocked(); + if (cid < 0) { + return ORBIS_NP_SIGNALING_ERROR_OUT_OF_MEMORY; + } + ConnectionInfo ci{}; + ci.conn_id = cid; + ci.ctx_id = ctxId; + ci.state = ConnState::SendingOffer; + ci.status = ORBIS_NP_SIGNALING_CONN_STATUS_PENDING; + ci.is_initiator = true; + ci.locally_activated = true; + ci.npid = peer_npid; + ci.online_id = peer_online_id; + g_connections[cid] = std::move(ci); + g_npid_to_conn[lookup_key] = cid; + created_new = true; + } + if (created_new) { + ArmConnectTimeoutLocked(cid); + QueueActivationLocked(cid, peer_online_id_str); + } + LOG_INFO(Lib_NpSignaling, "ctxId={} peer='{}' connId={} Status: {}", ctxId, + peer_online_id_str, cid, + created_new ? "queued, async" + : reused_established ? "reused established" + : "reused transient"); + } + + *outConnId = cid; + + if (reused_established) { + EstablishConnection(cid, false); + return ORBIS_OK; + } + + g_dispatch_cv.notify_all(); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingActivateConnectionA( + OrbisNpSignalingContextId ctxId, const OrbisNpSignalingAccountPlatformPair* peerAddr, + OrbisNpSignalingConnectionId* outConnId) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!peerAddr || peerAddr->accountId == 0 || !outConnId || peerAddr->platformType == 0) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + const auto ctx_it = g_contexts.find(ctxId); + if (ctx_it == g_contexts.end() || !ctx_it->second.active) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + if ((ctx_it->second.flags & 0x4) != 0 && ctx_it->second.account_id == peerAddr->accountId && + ctx_it->second.platform_type == peerAddr->platformType) { + return ORBIS_NP_SIGNALING_ERROR_OWN_PEER_ADDRESS; + } + + if (!ConsumeActivationBudgetGatedLocked(ctx_it->second)) { + return ORBIS_NP_SIGNALING_ERROR_EXCEED_RATE_LIMIT; + } + const s32 cid = AllocateConnectionIdLocked(); + if (cid < 0) { + return ORBIS_NP_SIGNALING_ERROR_OUT_OF_MEMORY; + } + ConnectionInfo ci{}; + ci.conn_id = cid; + ci.ctx_id = ctxId; + ci.status = ORBIS_NP_SIGNALING_CONN_STATUS_PENDING; + g_connections[cid] = std::move(ci); + *outConnId = cid; + + LOG_INFO(Lib_NpSignaling, "ctxId={} accountId={:#x} platform={} connId={}", ctxId, + peerAddr->accountId, peerAddr->platformType, cid); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingDeactivateConnection(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId) { + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!IsContextValidLocked(ctxId)) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + + const auto it = g_connections.find(connId); + if (it == g_connections.end() || it->second.ctx_id != ctxId) { + return ORBIS_NP_SIGNALING_ERROR_CONN_NOT_FOUND; + } + + LOG_INFO(Lib_NpSignaling, "t={} ctxId={} connId={} peer='{}' status={}", NowMs(), ctxId, + connId, OnlineIdToString(it->second.online_id), it->second.status); + } + DeactivateConnectionFaithful(connId); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingTerminateConnection(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId) { + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!IsContextValidLocked(ctxId)) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + const auto it = g_connections.find(connId); + if (it == g_connections.end() || it->second.ctx_id != ctxId) { + return ORBIS_NP_SIGNALING_ERROR_CONN_NOT_FOUND; + } + LOG_INFO(Lib_NpSignaling, "ctxId={} connId={}", ctxId, connId); + } + TerminateConnectionFaithful(connId); + { + SignalingMutexGuard lock; + RemoveConnectionLocked(connId); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionStatus(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId, + u32* outStatus, u32* outPeerAddr, + u16* outPeerPort) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!outStatus) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + if (!IsContextValidLocked(ctxId)) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + + const auto it = g_connections.find(connId); + if (it == g_connections.end() || it->second.ctx_id != ctxId) { + *outStatus = ORBIS_NP_SIGNALING_CONN_STATUS_INACTIVE; + return ORBIS_OK; + } + + const s32 state = ConnectionStateFromStatus(it->second.status); + if (state == 10) { + *outStatus = ORBIS_NP_SIGNALING_CONN_STATUS_ACTIVE; + if (outPeerAddr) { + *outPeerAddr = it->second.addr; + } + if (outPeerPort) { + *outPeerPort = it->second.port; + } + } else if (state != 0) { + *outStatus = ORBIS_NP_SIGNALING_CONN_STATUS_PENDING; + } else { + *outStatus = ORBIS_NP_SIGNALING_CONN_STATUS_INACTIVE; + } + + LOG_INFO(Lib_NpSignaling, "t={} ctxId={} connId={} peer='{}' status={}", NowMs(), ctxId, connId, + OnlineIdToString(it->second.online_id), *outStatus); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionInfo(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId, s32 infoCode, + void* outInfo) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!outInfo) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + const s32 rc = GetConnectionInfoInternal(ctxId, connId, infoCode, outInfo, nullptr); + LOG_INFO(Lib_NpSignaling, "ctxId={} connId={} infoCode={} rc={:#x}", ctxId, connId, infoCode, + static_cast(rc)); + return rc; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionInfoA(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId, s32 infoCode, + void* outInfo) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!outInfo) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + const s32 rc = GetConnectionInfoInternal(ctxId, connId, infoCode, nullptr, outInfo); + LOG_INFO(Lib_NpSignaling, "ctxId={} connId={} infoCode={} rc={:#x}", ctxId, connId, infoCode, + static_cast(rc)); + return rc; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionFromNpId(OrbisNpSignalingContextId ctxId, + const void* peerNpId, + OrbisNpSignalingConnectionId* outConnId) { + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + } + if (!peerNpId || !outConnId) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + OrbisNpId peer_npid{}; + OrbisNpOnlineId peer_online_id{}; + if (NormalizeNpId(peerNpId, &peer_npid, &peer_online_id) != ORBIS_OK) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!IsContextValidLocked(ctxId)) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + + const auto it = g_npid_to_conn.find(MakeCtxNpIdKey(ctxId, peer_npid)); + if (it == g_npid_to_conn.end()) { + return ORBIS_NP_SIGNALING_ERROR_CONN_NOT_FOUND; + } + const auto conn_it = g_connections.find(it->second); + if (conn_it == g_connections.end()) { + return ORBIS_NP_SIGNALING_ERROR_CONN_NOT_FOUND; + } + + *outConnId = it->second; + LOG_INFO(Lib_NpSignaling, "ctxId={} npid='{}' connId={}", ctxId, + OnlineIdToString(peer_online_id), it->second); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI +sceNpSignalingGetConnectionFromPeerAddress(OrbisNpSignalingContextId ctxId, u32 peerAddr, + u16 peerPort, OrbisNpSignalingConnectionId* outConnId) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!outConnId) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + if (!IsContextValidLocked(ctxId)) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + + for (const auto& [id, ci] : g_connections) { + if (ci.ctx_id == ctxId && ci.addr == peerAddr && ci.port == peerPort && + ConnectionStateFromStatus(ci.status) == 10) { + *outConnId = id; + LOG_INFO(Lib_NpSignaling, "ctxId={} addr={:#x} port={} connId={}", ctxId, peerAddr, + sceNetNtohs(peerPort), id); + return ORBIS_OK; + } + } + return ORBIS_NP_SIGNALING_ERROR_CONN_NOT_FOUND; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionFromPeerAddressA( + OrbisNpSignalingContextId ctxId, const OrbisNpSignalingAccountPlatformPair* peerAddr, + OrbisNpSignalingConnectionId* outConnId) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!peerAddr || !outConnId) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + const auto ctx_it = g_contexts.find(ctxId); + if (ctx_it == g_contexts.end() || !ctx_it->second.active) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + if ((ctx_it->second.flags & 0x4) == 0) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + LOG_INFO(Lib_NpSignaling, "ctxId={} accountId={:#x}", ctxId, peerAddr->accountId); + return ORBIS_NP_SIGNALING_ERROR_CONN_NOT_FOUND; +} + +s32 PS4_SYSV_ABI sceNpSignalingSetContextOption(OrbisNpSignalingContextId ctxId, s32 optionId, + s32 optionValue) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + const auto it = g_contexts.find(ctxId); + if (it == g_contexts.end() || !it->second.active) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + if (optionId != ORBIS_NP_SIGNALING_CONTEXT_OPTION_FLAG) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + if (optionValue == 0) { + it->second.flags &= ~0x2u; + } else if (optionValue == 1) { + it->second.flags |= 0x2u; + } else { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetContextOption(OrbisNpSignalingContextId ctxId, s32 optionId, + s32* outOptionValue) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!outOptionValue) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + const auto it = g_contexts.find(ctxId); + if (it == g_contexts.end() || !it->second.active) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + if (optionId != ORBIS_NP_SIGNALING_CONTEXT_OPTION_FLAG) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + *outOptionValue = (it->second.flags & 0x2u) != 0 ? 1 : 0; + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetLocalNetInfo(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingNetInfo* info) { + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + } + if (!info) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + if (info->size != sizeof(OrbisNpSignalingNetInfo)) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + auto* netinfo = Common::Singleton::Instance(); + info->localAddr = ParseIpv4Nbo(netinfo->GetIp()); + + NetCtl::OrbisNetCtlNatInfo nat_info{}; + nat_info.size = sizeof(nat_info); + info->mappedAddr = 0; + info->natStatus = 0; + if (NetCtl::sceNetCtlGetNatInfo(&nat_info) >= 0) { + info->mappedAddr = nat_info.mapped_addr; + info->natStatus = nat_info.nat_type; + } + if (info->mappedAddr == 0) { + const u32 external = netinfo->GetExternalIp(); + info->mappedAddr = external != 0 ? external : info->localAddr; + } + info->_pad_14 = 0; + + LOG_INFO(Lib_NpSignaling, "localAddr={:#x} mappedAddr={:#x} natStatus={}", info->localAddr, + info->mappedAddr, info->natStatus); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetPeerNetInfo(OrbisNpSignalingContextId ctxId, const void* peerNpId, + OrbisNpSignalingRequestId* outReqId) { + { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + } + if (!peerNpId || !outReqId) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + OrbisNpId peer_npid{}; + if (NormalizeNpId(peerNpId, &peer_npid, nullptr) != ORBIS_OK) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + s32 req_id = 0; + { + SignalingMutexGuard lock; + if (!IsContextValidLocked(ctxId)) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + req_id = StagePeerNetInfoResultLocked(ctxId); + if (req_id == 0) { + return ORBIS_NP_MATCHING2_SIGNALING_ERROR_OUT_OF_MEMORY; + } + } + *outReqId = static_cast(req_id); + LOG_INFO(Lib_NpSignaling, "ctxId={} reqId={:#x}", ctxId, *outReqId); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetPeerNetInfoA(OrbisNpSignalingContextId ctxId, + const void* peerAccountPayload, + OrbisNpSignalingRequestId* outReqId) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!peerAccountPayload || !outReqId) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + const auto ctx_it = g_contexts.find(ctxId); + if (ctx_it == g_contexts.end() || !ctx_it->second.active) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + if ((ctx_it->second.flags & 0x4) == 0) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + const s32 req_id = StagePeerNetInfoResultLocked(ctxId); + if (req_id == 0) { + return ORBIS_NP_MATCHING2_SIGNALING_ERROR_OUT_OF_MEMORY; + } + *outReqId = static_cast(req_id); + LOG_INFO(Lib_NpSignaling, "ctxId={} reqId={:#x}", ctxId, *outReqId); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingCancelPeerNetInfo(OrbisNpSignalingContextId ctxId, s32 reqOrConnId) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!IsContextValidLocked(ctxId)) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + if (!DropPeerNetInfoResultLocked(ctxId, reqOrConnId)) { + return ORBIS_NP_SIGNALING_ERROR_REQ_NOT_FOUND; + } + LOG_INFO(Lib_NpSignaling, "ctxId={} reqOrConnId={:#x}", ctxId, reqOrConnId); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetPeerNetInfoResult(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId, + OrbisNpSignalingNetInfo* peerNetInfo) { + SignalingMutexGuard lock; + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!peerNetInfo) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + if (!IsContextValidLocked(ctxId)) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + if (peerNetInfo->size != sizeof(OrbisNpSignalingNetInfo)) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + + PeerNetInfoResult result{}; + if (!TakePeerNetInfoResultLocked(ctxId, connId, &result)) { + return ORBIS_NP_SIGNALING_ERROR_RESULT_NOT_FOUND; + } + peerNetInfo->localAddr = result.local_ipv4; + peerNetInfo->mappedAddr = result.external_ipv4; + peerNetInfo->natStatus = static_cast(result.nat_route_kind); + peerNetInfo->_pad_14 = 0; + LOG_INFO(Lib_NpSignaling, "ctxId={} connId={:#x} ext={:#x}", ctxId, connId, + result.external_ipv4); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSignalingGetMemoryInfo(OrbisNpSignalingMemoryInfo* info) { + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!info) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + info->currentInUse = 0; + info->peakInUse = 0; + info->maxSystemSize = 0x20000; + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI +sceNpSignalingGetConnectionStatistics(OrbisNpSignalingConnectionStatistics* stats) { + if (!g_initialized) { + return ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED; + } + if (!stats) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + SignalingMutexGuard lock; + SnapshotConnectionStatisticsLocked(&stats->peakConnectionCount, &stats->activeConnectionCount, + &stats->transientConnectionCount, + &stats->establishedConnectionCount); + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("0UvTFeomAUM", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingActivateConnection); + LIB_FUNCTION("ZPLavCKqAB0", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingActivateConnectionA); + LIB_FUNCTION("X1G4kkN2R-8", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingCancelPeerNetInfo); + LIB_FUNCTION("5yYjEdd4t8Y", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingCreateContext); + LIB_FUNCTION("dDLNFdY8dws", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingCreateContextA); + LIB_FUNCTION("6UEembipgrM", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingDeactivateConnection); + LIB_FUNCTION("hx+LIg-1koI", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingDeleteContext); + LIB_FUNCTION("GQ0hqmzj0F4", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetConnectionFromNpId); + LIB_FUNCTION("CkPxQjSm018", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetConnectionFromPeerAddress); + LIB_FUNCTION("B7cT9aVby7A", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetConnectionFromPeerAddressA); + LIB_FUNCTION("AN3h0EBSX7A", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetConnectionInfo); + LIB_FUNCTION("rcylknsUDwg", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetConnectionInfoA); + LIB_FUNCTION("C6ZNCDTj00Y", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetConnectionStatistics); + LIB_FUNCTION("bD-JizUb3JM", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetConnectionStatus); + LIB_FUNCTION("npU5V56id34", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetContextOption); + LIB_FUNCTION("U8AQMlOFBc8", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetLocalNetInfo); + LIB_FUNCTION("tOpqyDyMje4", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetMemoryInfo); + LIB_FUNCTION("zFgFHId7vAE", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetPeerNetInfo); + LIB_FUNCTION("Shr7bZq8QHY", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetPeerNetInfoA); + LIB_FUNCTION("2HajCEGgG4s", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingGetPeerNetInfoResult); + LIB_FUNCTION("3KOuC4RmZZU", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingInitialize); + LIB_FUNCTION("IHRDvZodPYY", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingSetContextOption); + LIB_FUNCTION("NPhw0UXaNrk", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingTerminate); + LIB_FUNCTION("b4qaXPzMJxo", "libSceNpSignaling", 1, "libSceNpSignaling", + sceNpSignalingTerminateConnection); +} + +} // namespace Libraries::Np::NpSignaling diff --git a/src/core/libraries/np/np_signaling/np_signaling.h b/src/core/libraries/np/np_signaling/np_signaling.h new file mode 100644 index 000000000..31a660d1d --- /dev/null +++ b/src/core/libraries/np/np_signaling/np_signaling.h @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/types.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::Np::NpSignaling { + +using OrbisNpSignalingContextId = s32; +using OrbisNpSignalingConnectionId = s32; +using OrbisNpSignalingRequestId = u32; + +using OrbisNpSignalingHandler = PS4_SYSV_ABI void (*)(u32 ctxId, u32 connId, s32 event, + s32 errorCode, void* userArg); + +constexpr s32 ORBIS_NP_SIGNALING_EVENT_DEAD = 0; +constexpr s32 ORBIS_NP_SIGNALING_EVENT_ESTABLISHED = 1; +constexpr s32 ORBIS_NP_SIGNALING_EVENT_NETINFO_ERROR = 2; +constexpr s32 ORBIS_NP_SIGNALING_EVENT_NETINFO_RESULT = 3; +constexpr s32 ORBIS_NP_SIGNALING_EVENT_PEER_ACTIVATED = 10; +constexpr s32 ORBIS_NP_SIGNALING_EVENT_PEER_DEACTIVATED = 11; +constexpr s32 ORBIS_NP_SIGNALING_EVENT_MUTUAL_ACTIVATED = 12; + +constexpr s32 ORBIS_NP_SIGNALING_CONN_STATUS_INACTIVE = 0; +constexpr s32 ORBIS_NP_SIGNALING_CONN_STATUS_PENDING = 1; +constexpr s32 ORBIS_NP_SIGNALING_CONN_STATUS_ACTIVE = 2; + +constexpr s32 ORBIS_NP_SIGNALING_CONN_INFO_RTT = 1; +constexpr s32 ORBIS_NP_SIGNALING_CONN_INFO_BANDWIDTH = 2; +constexpr s32 ORBIS_NP_SIGNALING_CONN_INFO_PEER_NP_ID = 3; +constexpr s32 ORBIS_NP_SIGNALING_CONN_INFO_PEER_ADDR = 4; +constexpr s32 ORBIS_NP_SIGNALING_CONN_INFO_MAPPED_ADDR = 5; +constexpr s32 ORBIS_NP_SIGNALING_CONN_INFO_PACKET_LOSS = 6; +constexpr s32 ORBIS_NP_SIGNALING_CONN_INFO_PEER_ADDRESS_A = 7; + +constexpr s32 ORBIS_NP_SIGNALING_CONTEXT_OPTION_FLAG = 1; + +struct OrbisNpSignalingNetInfo { + u64 size; + u32 localAddr; + u32 mappedAddr; + s32 natStatus; + u32 _pad_14; +}; +static_assert(sizeof(OrbisNpSignalingNetInfo) == 0x18); + +struct OrbisNpSignalingAccountPlatformPair { + u64 accountId; + u32 platformType; + u32 _pad_0c; +}; +static_assert(sizeof(OrbisNpSignalingAccountPlatformPair) == 0x10); + +struct OrbisNpSignalingMemoryInfo { + u64 currentInUse; + u64 peakInUse; + u64 maxSystemSize; +}; +static_assert(sizeof(OrbisNpSignalingMemoryInfo) == 0x18); + +struct OrbisNpSignalingConnectionStatistics { + u32 peakConnectionCount; + u32 activeConnectionCount; + u32 transientConnectionCount; + u32 establishedConnectionCount; +}; +static_assert(sizeof(OrbisNpSignalingConnectionStatistics) == 0x10); + +s32 PS4_SYSV_ABI sceNpSignalingInitialize(s64 memorySize, s32 threadPriority, s32 cpuAffinityMask, + s64 threadStackSize); +s32 PS4_SYSV_ABI sceNpSignalingTerminate(); + +s32 PS4_SYSV_ABI sceNpSignalingCreateContext(const void* npId, void* callback, void* callbackArg, + OrbisNpSignalingContextId* outContextId); +s32 PS4_SYSV_ABI sceNpSignalingCreateContextA(s32 userId, void* callback, void* callbackArg, + OrbisNpSignalingContextId* outContextId); +s32 PS4_SYSV_ABI sceNpSignalingDeleteContext(OrbisNpSignalingContextId ctxId); + +s32 PS4_SYSV_ABI sceNpSignalingActivateConnection(OrbisNpSignalingContextId ctxId, + const void* peerNpId, + OrbisNpSignalingConnectionId* outConnId); +s32 PS4_SYSV_ABI sceNpSignalingActivateConnectionA( + OrbisNpSignalingContextId ctxId, const OrbisNpSignalingAccountPlatformPair* peerAddr, + OrbisNpSignalingConnectionId* outConnId); +s32 PS4_SYSV_ABI sceNpSignalingDeactivateConnection(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId); +s32 PS4_SYSV_ABI sceNpSignalingTerminateConnection(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId); + +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionStatus(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId, + u32* outStatus, u32* outPeerAddr, + u16* outPeerPort); +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionInfo(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId, s32 infoCode, + void* outInfo); +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionInfoA(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId, s32 infoCode, + void* outInfo); + +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionFromNpId(OrbisNpSignalingContextId ctxId, + const void* peerNpId, + OrbisNpSignalingConnectionId* outConnId); +s32 PS4_SYSV_ABI +sceNpSignalingGetConnectionFromPeerAddress(OrbisNpSignalingContextId ctxId, u32 peerAddr, + u16 peerPort, OrbisNpSignalingConnectionId* outConnId); +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionFromPeerAddressA( + OrbisNpSignalingContextId ctxId, const OrbisNpSignalingAccountPlatformPair* peerAddr, + OrbisNpSignalingConnectionId* outConnId); + +s32 PS4_SYSV_ABI sceNpSignalingSetContextOption(OrbisNpSignalingContextId ctxId, s32 optionId, + s32 optionValue); +s32 PS4_SYSV_ABI sceNpSignalingGetContextOption(OrbisNpSignalingContextId ctxId, s32 optionId, + s32* outOptionValue); + +s32 PS4_SYSV_ABI sceNpSignalingGetLocalNetInfo(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingNetInfo* info); + +s32 PS4_SYSV_ABI sceNpSignalingGetPeerNetInfo(OrbisNpSignalingContextId ctxId, const void* peerNpId, + OrbisNpSignalingRequestId* outReqId); +s32 PS4_SYSV_ABI sceNpSignalingGetPeerNetInfoA(OrbisNpSignalingContextId ctxId, + const void* peerAccountPayload, + OrbisNpSignalingRequestId* outReqId); +s32 PS4_SYSV_ABI sceNpSignalingCancelPeerNetInfo(OrbisNpSignalingContextId ctxId, s32 reqOrConnId); +s32 PS4_SYSV_ABI sceNpSignalingGetPeerNetInfoResult(OrbisNpSignalingContextId ctxId, + OrbisNpSignalingConnectionId connId, + OrbisNpSignalingNetInfo* peerNetInfo); + +s32 PS4_SYSV_ABI sceNpSignalingGetMemoryInfo(OrbisNpSignalingMemoryInfo* info); +s32 PS4_SYSV_ABI sceNpSignalingGetConnectionStatistics(OrbisNpSignalingConnectionStatistics* stats); + +s32 GetActiveConnectionIdForPeer(std::string_view online_id); + +s32 GetConnectionStatusForPeer(std::string_view online_id, s32* out_conn_id); + +bool GetPeerAddress(std::string_view online_id, u32* out_addr, u16* out_port); + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Np::NpSignaling diff --git a/src/core/libraries/np/np_signaling/np_signaling_helpers.cpp b/src/core/libraries/np/np_signaling/np_signaling_helpers.cpp new file mode 100644 index 000000000..65db54b09 --- /dev/null +++ b/src/core/libraries/np/np_signaling/np_signaling_helpers.cpp @@ -0,0 +1,520 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "common/alignment.h" +#include "common/logging/log.h" +#include "common/singleton.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/kernel/kernel.h" +#include "core/libraries/kernel/memory.h" +#include "core/libraries/network/net.h" +#include "core/libraries/network/net_util.h" +#include "core/libraries/np/np_common.h" +#include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_signaling/np_signaling_helpers.h" +#include "core/libraries/np/np_signaling/np_signaling_state.h" +#include "core/libraries/np/np_signaling/np_signaling_stubs.h" + +namespace Libraries::Np::NpSignaling::Helpers { + +namespace { + +constexpr s32 InferredNpAppType = 0; + +bool g_signaling_heap_initialized = false; +void* g_signaling_heap_base = nullptr; +s64 g_signaling_heap_size = 0; +SignalingRuntimeHooks g_runtime_hooks{}; +bool g_runtime_hooks_registered = false; +u64 g_echo_probe_word2 = 0; +u64 g_echo_probe_word3 = 0; +u64 g_echo_thread_handle = static_cast(-1); +s32 g_echo_thread_status = -1; + +s32 GetInferredAppType(s32* app_type) { + if (app_type == nullptr) { + return ORBIS_NP_INT_ERROR_INVALID_ARGUMENT; + } + + *app_type = InferredNpAppType; + return ORBIS_OK; +} + +s16 GetAppTypeStateGate() { + return 0; +} + +void SetAppTypeMarker(u16 marker) { + LOG_DEBUG(Lib_NpSignaling, "marker={:#x}", marker); +} +} // namespace + +void SetRuntimeHooks(const SignalingRuntimeHooks& hooks) { + g_runtime_hooks = hooks; + g_runtime_hooks_registered = true; +} + +s32 CheckInitializeAppType(u32* is_app_type_4) { + s32 app_type = -1; + const s32 rc = GetInferredAppType(&app_type); + if (rc < 0) { + return rc; + } + + if (is_app_type_4 != nullptr) { + *is_app_type_4 = app_type == 4 ? 1u : 0u; + } + + return ORBIS_OK; +} + +s32 InitSignalingHeap(s64 pool_size) { + LOG_DEBUG(Lib_NpSignaling, "pool_size={}", pool_size); + + if (pool_size == 0 || g_signaling_heap_initialized) { + return ORBIS_NP_SIGNALING_INTERNAL_ERROR_ALLOCATOR; + } + + const u64 aligned_size = Common::AlignUp(static_cast(pool_size), 0x4000); + + void* heap_base = nullptr; + const s32 rc = Libraries::Kernel::sceKernelMapNamedFlexibleMemory(&heap_base, aligned_size, 3, + 0, "SceNpSignaling"); + if (rc < 0) { + return rc; + } + + g_signaling_heap_initialized = true; + g_signaling_heap_base = heap_base; + g_signaling_heap_size = static_cast(aligned_size); + return ORBIS_OK; +} + +void ShutdownSignalingHeap() { + if (!g_signaling_heap_initialized) { + return; + } + + g_signaling_heap_initialized = false; + + if (g_signaling_heap_base != nullptr) { + Libraries::Kernel::sceKernelMunmap(g_signaling_heap_base, + static_cast(g_signaling_heap_size)); + g_signaling_heap_base = nullptr; + g_signaling_heap_size = 0; + } +} + +s32 CheckAppType() { + s32 app_type = -1; + const s32 rc = GetInferredAppType(&app_type); + if (rc < 0) { + return rc; + } + + if (app_type == 5) { + const s16 gate = GetAppTypeStateGate(); + if (gate != 0) { + return ORBIS_OK; + } + } + + SetAppTypeMarker(0x245a); + return ORBIS_OK; +} + +s32 StartMainRuntime(s32 thread_priority, s32 cpu_affinity_mask, s64 thread_stack_size) { + LOG_DEBUG(Lib_NpSignaling, + "thread_priority={} cpu_affinity_mask={} " + "thread_stack_size={}", + thread_priority, cpu_affinity_mask, thread_stack_size); + + if (!g_runtime_hooks_registered) { + return ORBIS_NP_SIGNALING_INTERNAL_ERROR_NOT_INITIALIZED; + } + + const s32 priority = thread_priority; + const u64 affinity = static_cast(static_cast(cpu_affinity_mask)); + const u64 stack_size = static_cast(thread_stack_size); + if (g_runtime_hooks.start_dispatch != nullptr) { + g_runtime_hooks.start_dispatch(priority, affinity, stack_size); + } + if (g_runtime_hooks.start_receive != nullptr) { + g_runtime_hooks.start_receive(priority, affinity, stack_size); + } + if (g_runtime_hooks.start_ping != nullptr) { + g_runtime_hooks.start_ping(priority, affinity, stack_size); + } + + return ORBIS_OK; +} + +s32 StartEchoRuntime(s32 thread_priority, s32 cpu_affinity_mask) { + LOG_DEBUG(Lib_NpSignaling, "thread_priority={} cpu_affinity_mask={}", thread_priority, + cpu_affinity_mask); + + auto sock = Libraries::Net::sceNetSocket("SceNpSignalingIoctl", 2, 6, 0); + if (sock < 0) { + return sock; + } + + const s32 ioctl_rc = Libraries::Net::sceNetIoctl(); + Libraries::Net::sceNetSocketClose(sock); + + g_echo_probe_word2 = 0; + g_echo_probe_word3 = 0; + g_echo_thread_handle = static_cast(-1); + g_echo_thread_status = -1; + return ioctl_rc; +} + +void ShutdownRuntime() { + if (!g_runtime_hooks_registered) { + g_echo_probe_word2 = 0; + g_echo_probe_word3 = 0; + g_echo_thread_handle = static_cast(-1); + g_echo_thread_status = -1; + return; + } + + if (g_runtime_hooks.stop_ping != nullptr) { + g_runtime_hooks.stop_ping(); + } + if (g_runtime_hooks.stop_receive != nullptr) { + g_runtime_hooks.stop_receive(); + } + if (g_runtime_hooks.stop_dispatch != nullptr) { + g_runtime_hooks.stop_dispatch(); + } + + g_echo_probe_word2 = 0; + g_echo_probe_word3 = 0; + g_echo_thread_handle = static_cast(-1); + g_echo_thread_status = -1; +} + +} // namespace Libraries::Np::NpSignaling::Helpers + +namespace Libraries::Np::NpSignaling { + +using Libraries::Net::sceNetNtohs; + +static Kernel::PthreadT g_dispatch_thread{}; +static Kernel::PthreadT g_receive_thread{}; +static bool g_receive_stop = false; +static Kernel::PthreadT g_ping_thread{}; +static bool g_ping_stop = false; + +static void HandleStunEcho(s32 ctx_id, const StunEcho& echo) { + SignalingMutexGuard lock; + const auto it = g_contexts.find(ctx_id); + if (it == g_contexts.end() || !it->second.active) { + return; + } + NpSignalingContext& ctx = it->second; + ctx.ext_addr.store(echo.ext_ip); + ctx.ext_port.store(echo.ext_port); + + LOG_DEBUG(Lib_NpSignaling, "STUN echo: ctxId={} ext_addr={:#x} ext_port={}", ctx_id, + echo.ext_ip, sceNetNtohs(echo.ext_port)); + + auto* netinfo = Common::Singleton::Instance(); + netinfo->SetExternalIp(echo.ext_ip); + + ctx.stun_cv.notify_all(); +} + +static bool HasSignalingMagic(const u8* buf, size_t nbytes) { + return nbytes >= 5 && std::memcmp(buf, kSignalingMagic, sizeof(kSignalingMagic)) == 0; +} + +static void DrainControlPackets() { + static constexpr u32 kBufSize = 256; + u8 buf[kBufSize]; + for (;;) { + u32 from_addr = 0; + u16 from_port = 0; + const int rc = Stubs::ControlRecvFrom(buf, kBufSize, &from_addr, &from_port); + if (rc <= 0) { + break; + } + const auto nbytes = static_cast(rc); + if (nbytes == sizeof(SignalingControl) && HasSignalingMagic(buf, nbytes) && + buf[4] == static_cast(SignalingPacketType::Control)) { + SignalingControl ctrl{}; + std::memcpy(&ctrl, buf, sizeof(ctrl)); + HandleControlPacket(from_addr, from_port, ctrl); + } else { + LOG_WARNING(Lib_NpSignaling, "ReceiveThread: bad control packet (size={})", nbytes); + } + } +} + +static void ReceiveThreadMain() { + static constexpr u32 kBufSize = 256; + u8 buf[kBufSize]; + + while (!g_receive_stop) { + DrainControlPackets(); + + u32 from_addr = 0; + u16 from_port = 0; + + const int rc = Stubs::SignalingRecvFrom(buf, kBufSize, &from_addr, &from_port); + if (rc <= 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + continue; + } + + const auto nbytes = static_cast(rc); + + if (nbytes != sizeof(StunEcho)) { + LOG_DEBUG( + Lib_NpSignaling, + "ReceiveThread DATA from {:#x}:{} size={} b0-4={:02x},{:02x},{:02x},{:02x},{:02x}", + from_addr, sceNetNtohs(from_port), nbytes, buf[0], buf[1], buf[2], buf[3], + nbytes >= 5 ? buf[4] : 0); + } + + if (nbytes == sizeof(StunEcho)) { + s32 ctx_id = 0; + { + SignalingMutexGuard lock; + for (const auto& [cid, ctx] : g_contexts) { + if (ctx.active) { + ctx_id = cid; + break; + } + } + } + if (ctx_id == 0) { + continue; + } + StunEcho echo{}; + std::memcpy(&echo, buf, sizeof(echo)); + HandleStunEcho(ctx_id, echo); + continue; + } + + if (!HasSignalingMagic(buf, nbytes)) { + LOG_WARNING(Lib_NpSignaling, "ReceiveThread: dropping non-SHAD packet (size={})", + nbytes); + continue; + } + const u8 type = buf[4]; + + if (nbytes == sizeof(SignalingEchoPing) && + type == static_cast(SignalingPacketType::EchoPing)) { + SignalingEchoPing ping{}; + std::memcpy(&ping, buf, sizeof(ping)); + SignalingEchoPong pong{}; + pong.conn_id = ping.conn_id; + pong.orig_ts_us = ping.send_ts_us; + Stubs::SignalingSendTo(&pong, sizeof(pong), from_addr, from_port); + continue; + } + if (nbytes == sizeof(SignalingEchoPong) && + type == static_cast(SignalingPacketType::EchoPong)) { + SignalingEchoPong pong{}; + std::memcpy(&pong, buf, sizeof(pong)); + const s64 now = NowUs(); + s32 rtt_us = static_cast(now - static_cast(pong.orig_ts_us)); + if (rtt_us < 0) { + rtt_us = 0; + } + { + SignalingMutexGuard lock; + RecordRttSampleLocked(static_cast(pong.conn_id), rtt_us); + } + continue; + } + if (nbytes == sizeof(SignalingHandshake) && + type == static_cast(SignalingPacketType::Handshake)) { + SignalingHandshake hs{}; + std::memcpy(&hs, buf, sizeof(hs)); + HandleHandshakePacket(from_addr, from_port, hs); + continue; + } + + LOG_WARNING(Lib_NpSignaling, "ReceiveThread: unexpected SHAD packet type={} size={}", type, + nbytes); + } +} + +static PS4_SYSV_ABI void* ReceiveThreadFunc(void*) { + ReceiveThreadMain(); + return nullptr; +} + +static void StartReceiveThread(s32 priority, u64 affinity_mask, u64 stack_size) { + g_receive_stop = false; + if (!g_receive_thread) { + NpCommon::sceNpCreateThread(&g_receive_thread, ReceiveThreadFunc, nullptr, priority, + stack_size, affinity_mask, "SceNpSignalingRecv"); + } +} + +static void StopReceiveThread() { + { + SignalingMutexGuard lock; + g_receive_stop = true; + } + if (g_receive_thread) { + NpCommon::sceNpJoinThread(g_receive_thread, nullptr); + g_receive_thread = {}; + } +} + +static void PingThreadMain() { + while (!g_ping_stop) { + if (!Stubs::TransportIsReady()) { + std::this_thread::sleep_for(std::chrono::milliseconds(kSigRetryMs)); + continue; + } + + struct CtxSnapshot { + s32 ctx_id; + OrbisNpOnlineId online_id{}; + bool resolved; + }; + std::vector contexts; + { + SignalingMutexGuard lock; + for (const auto& [ctx_id, ctx] : g_contexts) { + if (ctx.active) { + contexts.push_back({ctx_id, ctx.owner_online_id, ctx.ext_addr.load() != 0}); + } + } + } + + const u32 server_addr = Stubs::MmServerAddr(); + const u16 server_port = Stubs::MmServerUdpPort(); + + for (const auto& cs : contexts) { + if (server_addr == 0 || server_port == 0) { + break; + } + StunPing ping{}; + ping.cmd = 0x01; + std::memcpy(ping.online_id, cs.online_id.data, ORBIS_NP_ONLINEID_MAX_LENGTH); + + Stubs::SignalingSendTo(&ping, sizeof(ping), server_addr, server_port); + } + + SendEchoPings(); + + const bool any_unresolved = std::any_of(contexts.begin(), contexts.end(), + [](const auto& cs) { return !cs.resolved; }); + const u32 sleep_ms = any_unresolved ? kSigRetryMs : kSigPingMs; + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms)); + } +} + +static PS4_SYSV_ABI void* PingThreadFunc(void*) { + PingThreadMain(); + return nullptr; +} + +static void StartPingThread(s32 priority, u64 affinity_mask, u64 stack_size) { + g_ping_stop = false; + if (!g_ping_thread) { + NpCommon::sceNpCreateThread(&g_ping_thread, PingThreadFunc, nullptr, priority, stack_size, + affinity_mask, "SceNpSignalingPing"); + } +} + +static void StopPingThread() { + g_ping_stop = true; + if (g_ping_thread) { + NpCommon::sceNpJoinThread(g_ping_thread, nullptr); + g_ping_thread = {}; + } +} + +static void DispatchThreadMain() { + for (;;) { + ProcessPendingActivations(); + + QueuedDispatch dispatch; + { + std::unique_lock lock(g_dispatch_mutex); + for (;;) { + if (g_dispatch_stop) { + return; + } + if (g_dispatch_queue.empty()) { + g_dispatch_cv.wait_for(lock, std::chrono::seconds(1), [] { + return g_dispatch_stop || !g_dispatch_queue.empty(); + }); + break; + } + const auto it = g_dispatch_queue.begin(); + const auto now = std::chrono::steady_clock::now(); + if (it->first > now) { + g_dispatch_cv.wait_until(lock, it->first); + break; + } + dispatch = std::move(it->second); + g_dispatch_queue.erase(it); + break; + } + } + + if (dispatch.callback) { + LOG_INFO(Lib_NpSignaling, "t={} ctxId={} connId={} event={}({}) delay={}ms", NowMs(), + dispatch.ctx_id, dispatch.conn_id, dispatch.event_type, + SignalingEventName(dispatch.event_type), dispatch.delay_ms); + LOG_DEBUG(Lib_NpSignaling, + "INVOKE signaling_cb={} ctxId={} connId={} event={}({}) errorCode={} arg={}", + fmt::ptr(reinterpret_cast(dispatch.callback)), dispatch.ctx_id, + dispatch.conn_id, dispatch.event_type, + SignalingEventName(dispatch.event_type), dispatch.error_code, + fmt::ptr(dispatch.callback_arg)); + dispatch.callback(static_cast(dispatch.ctx_id), static_cast(dispatch.conn_id), + dispatch.event_type, dispatch.error_code, dispatch.callback_arg); + } + } +} + +static PS4_SYSV_ABI void* DispatchThreadFunc(void*) { + DispatchThreadMain(); + return nullptr; +} + +static void StartDispatchThread(s32 priority, u64 affinity_mask, u64 stack_size) { + std::lock_guard lock(g_dispatch_mutex); + g_dispatch_stop = false; + if (!g_dispatch_thread) { + NpCommon::sceNpCreateThread(&g_dispatch_thread, DispatchThreadFunc, nullptr, priority, + stack_size, affinity_mask, "SceNpSignalingMain"); + } +} + +static void StopDispatchThread() { + { + std::lock_guard lock(g_dispatch_mutex); + g_dispatch_stop = true; + g_dispatch_queue.clear(); + } + g_dispatch_cv.notify_all(); + if (g_dispatch_thread) { + NpCommon::sceNpJoinThread(g_dispatch_thread, nullptr); + g_dispatch_thread = {}; + } +} + +void RegisterRuntimeHooks() { + Helpers::SetRuntimeHooks({ + .start_dispatch = StartDispatchThread, + .start_receive = StartReceiveThread, + .start_ping = StartPingThread, + .stop_ping = StopPingThread, + .stop_receive = StopReceiveThread, + .stop_dispatch = StopDispatchThread, + }); +} + +} // namespace Libraries::Np::NpSignaling diff --git a/src/core/libraries/np/np_signaling/np_signaling_helpers.h b/src/core/libraries/np/np_signaling/np_signaling_helpers.h new file mode 100644 index 000000000..2e54afbe4 --- /dev/null +++ b/src/core/libraries/np/np_signaling/np_signaling_helpers.h @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Libraries::Np::NpSignaling::Helpers { + +struct SignalingRuntimeHooks { + void (*start_dispatch)(s32 priority, u64 affinity_mask, u64 stack_size); + void (*start_receive)(s32 priority, u64 affinity_mask, u64 stack_size); + void (*start_ping)(s32 priority, u64 affinity_mask, u64 stack_size); + void (*stop_ping)(); + void (*stop_receive)(); + void (*stop_dispatch)(); +}; + +void SetRuntimeHooks(const SignalingRuntimeHooks& hooks); +s32 CheckInitializeAppType(u32* is_app_type_4); +s32 InitSignalingHeap(s64 pool_size); +void ShutdownSignalingHeap(); +s32 CheckAppType(); +s32 StartMainRuntime(s32 thread_priority, s32 cpu_affinity_mask, s64 thread_stack_size); +s32 StartEchoRuntime(s32 thread_priority, s32 cpu_affinity_mask); +void ShutdownRuntime(); + +} // namespace Libraries::Np::NpSignaling::Helpers + +namespace Libraries::Np::NpSignaling { + +void RegisterRuntimeHooks(); + +} diff --git a/src/core/libraries/np/np_signaling/np_signaling_state.cpp b/src/core/libraries/np/np_signaling/np_signaling_state.cpp new file mode 100644 index 000000000..4e50dc8f4 --- /dev/null +++ b/src/core/libraries/np/np_signaling/np_signaling_state.cpp @@ -0,0 +1,1361 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include + +#include "common/logging/log.h" +#include "common/singleton.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/kernel/kernel.h" +#include "core/libraries/kernel/process.h" +#include "core/libraries/network/net.h" +#include "core/libraries/network/net_util.h" +#include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_signaling/np_signaling_state.h" +#include "core/libraries/np/np_signaling/np_signaling_stubs.h" + +namespace Libraries::Np::NpSignaling { + +using Libraries::Net::sceNetNtohs; + +std::unordered_map g_contexts; +std::unordered_map g_connections; +std::unordered_map g_npid_to_conn; +std::unordered_map g_peer_netinfo_results; +std::vector g_pending_activations; +u32 g_peer_netinfo_next_id = 1; +u32 g_peak_connection_count = 0; +u32 g_last_assigned_context_id = 0; +u32 g_connection_id_seed = 0; +bool g_initialized = false; +Libraries::Kernel::PthreadMutexT g_mutex_storage{}; + +void InitSignalingMutex() { + NpCommon::sceNpLwMutexInit(&g_mutex_storage, "SceNpSignalingLock", 1); +} +void DestroySignalingMutex() { + NpCommon::sceNpLwMutexDestroy(&g_mutex_storage); +} + +SignalingMutexGuard::SignalingMutexGuard() { + NpCommon::sceNpLwMutexLock(&g_mutex_storage); +} +SignalingMutexGuard::~SignalingMutexGuard() { + NpCommon::sceNpLwMutexUnlock(&g_mutex_storage); +} + +std::multimap g_dispatch_queue; +std::mutex g_dispatch_mutex; +std::condition_variable g_dispatch_cv; +bool g_dispatch_stop = false; + +NpCommon::OrbisNpCalloutContext g_callout_ctx{}; +bool g_callout_ctx_active = false; + +constexpr s64 kHandshakeConnectTimeoutMs = 30'000; +constexpr s64 kHandshakeRetransmitMs = 500; +constexpr s64 kKeepaliveIntervalMs = 45'000; +constexpr s64 kEstablishedLivenessTimeoutMs = 60'000; + +static void PS4_SYSV_ABI TimeoutCalloutHandler(u64 arg); +static void PS4_SYSV_ABI StepCalloutHandler(u64 arg); + +static void ArmTimeoutCalloutLocked(ConnectionInfo& ci, s64 delay_us) { + if (!g_callout_ctx_active) { + return; + } + if (ci.timeout_callout_armed) { + u32 removed = 0; + NpCommon::sceNpCalloutStopOnCtx(&g_callout_ctx, &ci.timeout_callout, &removed); + ci.timeout_callout_armed = false; + } + NpCommon::sceNpCalloutStartOnCtx64(&g_callout_ctx, &ci.timeout_callout, delay_us, + reinterpret_cast(&TimeoutCalloutHandler), + static_cast(ci.conn_id)); + ci.timeout_callout_armed = true; +} + +static void CancelTimeoutCalloutLocked(ConnectionInfo& ci) { + if (ci.timeout_callout_armed) { + u32 removed = 0; + NpCommon::sceNpCalloutStopOnCtx(&g_callout_ctx, &ci.timeout_callout, &removed); + ci.timeout_callout_armed = false; + } +} + +static void ArmStepCalloutLocked(ConnectionInfo& ci, s64 delay_us) { + if (!g_callout_ctx_active) { + return; + } + if (ci.step_callout_armed) { + u32 removed = 0; + NpCommon::sceNpCalloutStopOnCtx(&g_callout_ctx, &ci.step_callout, &removed); + ci.step_callout_armed = false; + } + NpCommon::sceNpCalloutStartOnCtx64(&g_callout_ctx, &ci.step_callout, delay_us, + reinterpret_cast(&StepCalloutHandler), + static_cast(ci.conn_id)); + ci.step_callout_armed = true; +} + +static void CancelStepCalloutLocked(ConnectionInfo& ci) { + if (ci.step_callout_armed) { + u32 removed = 0; + NpCommon::sceNpCalloutStopOnCtx(&g_callout_ctx, &ci.step_callout, &removed); + ci.step_callout_armed = false; + } +} + +static void CancelAllCalloutsLocked(ConnectionInfo& ci) { + CancelTimeoutCalloutLocked(ci); + CancelStepCalloutLocked(ci); +} + +void ArmConnectTimeoutLocked(OrbisNpSignalingConnectionId conn_id) { + const auto it = g_connections.find(conn_id); + if (it == g_connections.end()) { + return; + } + ArmTimeoutCalloutLocked(it->second, kHandshakeConnectTimeoutMs * 1000); +} + +void ClearLingerAndTimeoutLocked(OrbisNpSignalingConnectionId conn_id) { + const auto it = g_connections.find(conn_id); + if (it == g_connections.end()) { + return; + } + it->second.deactivate_lingering = false; + CancelTimeoutCalloutLocked(it->second); +} + +long long NowMs() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} + +s64 NowUs() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} + +std::string OnlineIdToString(const OrbisNpOnlineId& online_id_in) { + std::string online_id(online_id_in.data, ORBIS_NP_ONLINEID_MAX_LENGTH); + const auto nul = online_id.find('\0'); + if (nul != std::string::npos) { + online_id.resize(nul); + } + return online_id; +} + +std::string OnlineIdFromNpId(const OrbisNpId& np_id) { + return OnlineIdToString(np_id.handle); +} + +OrbisNpId NpIdFromOnlineId(const OrbisNpOnlineId& online_id) { + OrbisNpId npid{}; + npid.handle = online_id; + return npid; +} + +bool OnlineIdEqualsString(const OrbisNpOnlineId& online_id, std::string_view value) { + return OnlineIdToString(online_id) == value; +} + +bool IsValidNpId(const OrbisNpId& np_id) { + return NpCommon::sceNpIntIsValidOnlineId(&np_id.handle) != 0; +} + +s32 NormalizeNpId(const void* np_id, OrbisNpId* out_npid, OrbisNpOnlineId* out_online_id) { + if (!np_id || !out_npid) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + std::memcpy(out_npid, np_id, sizeof(*out_npid)); + if (!IsValidNpId(*out_npid)) { + return ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + if (out_online_id) { + *out_online_id = out_npid->handle; + } + return ORBIS_OK; +} + +CtxNpIdKey MakeCtxNpIdKey(s32 ctx_id, const OrbisNpId& npid) { + CtxNpIdKey key{}; + key.ctx_id = ctx_id; + key.npid = npid; + return key; +} + +bool IsContextValidLocked(s32 ctx_id) { + const auto it = g_contexts.find(ctx_id); + return it != g_contexts.end() && it->second.active; +} + +s32 AllocateContextIdLocked() { + u32 candidate = g_last_assigned_context_id + 1; + if (g_last_assigned_context_id == 8) { + candidate = 1; + } + for (int tried = 0; tried < 8; ++tried) { + if (g_contexts.find(static_cast(candidate)) == g_contexts.end()) { + g_last_assigned_context_id = candidate; + return static_cast(candidate); + } + candidate = (candidate == 8) ? 1u : candidate + 1u; + } + return -1; +} + +s32 AllocateConnectionIdLocked() { + if (g_connection_id_seed == 0) { + std::random_device rd; + const u32 rnd = static_cast(rd()); + g_connection_id_seed = (rnd / 0x4001u) & 0x7fffu; + if (g_connection_id_seed == 0) { + g_connection_id_seed = 1; + } + } + for (int tried = 0; tried <= 0xffff; ++tried) { + const u32 candidate = g_connection_id_seed; + g_connection_id_seed = g_connection_id_seed + 1; + if (g_connection_id_seed == 0 || g_connection_id_seed > 0x7fff) { + g_connection_id_seed = 1; + } + if (candidate != 0 && + g_connections.find(static_cast(candidate)) == g_connections.end()) { + return static_cast(candidate); + } + } + return -1; +} + +void RemoveConnectionLocked(s32 conn_id) { + const auto it = g_connections.find(conn_id); + if (it == g_connections.end()) { + return; + } + CancelAllCalloutsLocked(it->second); + const auto map_key = MakeCtxNpIdKey(it->second.ctx_id, it->second.npid); + auto key_it = g_npid_to_conn.find(map_key); + if (key_it != g_npid_to_conn.end() && key_it->second == conn_id) { + g_npid_to_conn.erase(key_it); + } + g_connections.erase(it); +} + +void RemoveContextConnectionsLocked(s32 ctx_id) { + std::vector to_remove; + for (const auto& [cid, ci] : g_connections) { + if (ci.ctx_id == ctx_id) { + to_remove.push_back(cid); + } + } + for (s32 cid : to_remove) { + RemoveConnectionLocked(cid); + } +} + +const char* SignalingEventName(u32 event_type) { + switch (event_type) { + case ORBIS_NP_SIGNALING_EVENT_DEAD: + return "DEAD"; + case ORBIS_NP_SIGNALING_EVENT_ESTABLISHED: + return "ESTABLISHED"; + case ORBIS_NP_SIGNALING_EVENT_NETINFO_ERROR: + return "NETINFO_ERROR"; + case ORBIS_NP_SIGNALING_EVENT_NETINFO_RESULT: + return "NETINFO_RESULT"; + case ORBIS_NP_SIGNALING_EVENT_PEER_ACTIVATED: + return "PEER_ACTIVATED"; + case ORBIS_NP_SIGNALING_EVENT_PEER_DEACTIVATED: + return "PEER_DEACTIVATED"; + case ORBIS_NP_SIGNALING_EVENT_MUTUAL_ACTIVATED: + return "MUTUAL_ACTIVATED"; + default: + return "UNKNOWN"; + } +} + +bool ConsumeActivationBudgetLocked(NpSignalingContext& ctx) { + const s64 now = NowUs(); + + s64 budget = ctx.activate_budget_us; + if (ctx.activate_last_update_us == 0) { + budget = kActivateBudgetMaxUs; + } else { + const s64 elapsed = std::max(0, now - ctx.activate_last_update_us); + budget = std::min(kActivateBudgetMaxUs, budget + elapsed); + } + + ctx.activate_last_update_us = now; + ctx.activate_budget_us = budget; + + if (budget < kActivateCooldownUs) { + return false; + } + + ctx.activate_budget_us = budget - kActivateCooldownUs; + return true; +} + +bool ConsumeActivationBudgetGatedLocked(NpSignalingContext& ctx) { + if (ctx.compiled_sdk_version <= 0x16fffff) { + return true; + } + return ConsumeActivationBudgetLocked(ctx); +} + +s32 ConnectionStateFromStatus(s32 status) { + switch (status) { + case ORBIS_NP_SIGNALING_CONN_STATUS_ACTIVE: + return 10; + case ORBIS_NP_SIGNALING_CONN_STATUS_PENDING: + return 1; + default: + return 0; + } +} + +u32 CaptureCompiledSdkVersion() { + s32 ver = 0; + if (Libraries::Kernel::sceKernelGetCompiledSdkVersion(&ver) < 0) { + return 0; + } + return static_cast(ver); +} + +u32 ParseIpv4Nbo(const std::string& dotted) { + u32 octets[4] = {0, 0, 0, 0}; + int idx = 0; + u32 cur = 0; + bool any_digit = false; + for (const char c : dotted) { + if (c >= '0' && c <= '9') { + cur = cur * 10 + static_cast(c - '0'); + if (cur > 255) { + return 0; + } + any_digit = true; + } else if (c == '.') { + if (!any_digit || idx >= 3) { + return 0; + } + octets[idx++] = cur; + cur = 0; + any_digit = false; + } else { + return 0; + } + } + if (!any_digit || idx != 3) { + return 0; + } + octets[3] = cur; + return octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24); +} + +namespace { + +s32 AverageProbeRtt(const ConnectionInfo& ci) { + s64 sum = 0; + s32 count = 0; + for (const s32 sample : ci.probe_rtt_samples) { + if (sample > 0) { + sum += sample; + ++count; + } + } + if (count == 0) { + return 0; + } + return static_cast(sum / count); +} + +s32 ProbeLossPercent(const ConnectionInfo& ci) { + s32 zero_samples = 0; + s32 present_samples = 0; + for (const s32 sample : ci.probe_rtt_samples) { + if (sample != -1) { + ++present_samples; + if (sample == 0) { + ++zero_samples; + } + } + } + if (present_samples == 0) { + return 0; + } + return static_cast((zero_samples * 100) / present_samples); +} + +} // namespace + +s32 GetConnectionInfoInternal(s32 ctx_id, s32 conn_id, s32 info_code, void* out_a, void* out_b) { + if (!IsContextValidLocked(ctx_id)) { + return ORBIS_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + } + + const auto it = g_connections.find(conn_id); + if (it == g_connections.end() || it->second.ctx_id != ctx_id) { + return ORBIS_NP_SIGNALING_ERROR_CONN_NOT_FOUND; + } + + const ConnectionInfo& ci = it->second; + const s32 state = ConnectionStateFromStatus(ci.status); + const bool established = state == 10; + + s32 result = ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + + switch (info_code) { + case ORBIS_NP_SIGNALING_CONN_INFO_RTT: { + if (!established) { + return ORBIS_NP_SIGNALING_ERROR_CONN_IN_PROGRESS; + } + const s32 avg = AverageProbeRtt(ci); + if (out_a) { + *reinterpret_cast(out_a) = avg; + } + if (out_b) { + *reinterpret_cast(out_b) = static_cast(avg); + } + result = ORBIS_OK; + break; + } + case ORBIS_NP_SIGNALING_CONN_INFO_BANDWIDTH: { + if (!established) { + return ORBIS_NP_SIGNALING_ERROR_CONN_IN_PROGRESS; + } + if (out_a) { + *reinterpret_cast(out_a) = ci.echo_derived_value; + } + if (out_b) { + *reinterpret_cast(out_b) = ci.echo_derived_value; + } + result = ORBIS_OK; + break; + } + case ORBIS_NP_SIGNALING_CONN_INFO_PEER_NP_ID: { + if (out_a) { + std::memcpy(out_a, &ci.npid, sizeof(ci.npid)); + result = ORBIS_OK; + } else { + result = ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + } + break; + } + case ORBIS_NP_SIGNALING_CONN_INFO_PEER_ADDR: { + if (!established) { + return ORBIS_NP_SIGNALING_ERROR_CONN_IN_PROGRESS; + } + if (out_a) { + *reinterpret_cast(out_a) = ci.addr; + *reinterpret_cast(reinterpret_cast(out_a) + 4) = ci.port; + } + if (out_b) { + *reinterpret_cast(out_b) = ci.addr; + *reinterpret_cast(reinterpret_cast(out_b) + 4) = ci.port; + } + result = ORBIS_OK; + break; + } + case ORBIS_NP_SIGNALING_CONN_INFO_MAPPED_ADDR: { + if (!established) { + return ORBIS_NP_SIGNALING_ERROR_CONN_IN_PROGRESS; + } + if (out_a) { + *reinterpret_cast(out_a) = ci.local_addr; + *reinterpret_cast(reinterpret_cast(out_a) + 4) = ci.local_port; + } + if (out_b) { + *reinterpret_cast(out_b) = ci.local_addr; + *reinterpret_cast(reinterpret_cast(out_b) + 4) = ci.local_port; + } + result = ORBIS_OK; + break; + } + case ORBIS_NP_SIGNALING_CONN_INFO_PACKET_LOSS: { + if (!established) { + return ORBIS_NP_SIGNALING_ERROR_CONN_IN_PROGRESS; + } + const s32 loss = ProbeLossPercent(ci); + if (out_a) { + *reinterpret_cast(out_a) = loss; + } + if (out_b) { + *reinterpret_cast(out_b) = static_cast(loss); + } + result = ORBIS_OK; + break; + } + case ORBIS_NP_SIGNALING_CONN_INFO_PEER_ADDRESS_A: { + if (out_a) { + result = ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + break; + } + if (out_b) { + const auto ctx_it = g_contexts.find(ctx_id); + if (ctx_it != g_contexts.end() && + (ctx_it->second.account_id != 0 || ctx_it->second.platform_type != 0)) { + auto* pair = reinterpret_cast(out_b); + pair->accountId = ctx_it->second.account_id; + pair->platformType = ctx_it->second.platform_type; + pair->_pad_0c = 0; + result = ORBIS_OK; + } else { + result = ORBIS_NP_SIGNALING_ERROR_CONN_IN_PROGRESS; + } + } + break; + } + default: + result = ORBIS_NP_SIGNALING_ERROR_INVALID_ARGUMENT; + break; + } + + return result; +} + +void SnapshotConnectionStatisticsLocked(u32* out_peak, u32* out_active, u32* out_transient, + u32* out_established) { + u32 transient = 0; + u32 established = 0; + for (const auto& [cid, ci] : g_connections) { + const s32 state = ConnectionStateFromStatus(ci.status); + if (state == 10) { + ++established; + } else if (state != 0) { + ++transient; + } + } + const u32 active = transient + established; + if (active > g_peak_connection_count) { + g_peak_connection_count = active; + } + if (out_peak) { + *out_peak = g_peak_connection_count; + } + if (out_active) { + *out_active = active; + } + if (out_transient) { + *out_transient = transient; + } + if (out_established) { + *out_established = established; + } +} + +OrbisNpSignalingRequestId StagePeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id) { + const OrbisNpSignalingRequestId req_id = + kPeerNetInfoRequestIdPrefix | (g_peer_netinfo_next_id++); + + PeerNetInfoResult result{}; + result.ctx_id = ctx_id; + result.conn_id = static_cast(req_id); + auto* netinfo = Common::Singleton::Instance(); + result.external_ipv4 = netinfo->GetExternalIp(); + result.nat_route_kind = static_cast(netinfo->GetNatType()); + g_peer_netinfo_results[result.conn_id] = result; + return req_id; +} + +bool TakePeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id, s32 req_or_conn_id, + PeerNetInfoResult* out) { + const auto it = g_peer_netinfo_results.find(req_or_conn_id); + if (it == g_peer_netinfo_results.end() || it->second.ctx_id != ctx_id) { + return false; + } + if (out) { + *out = it->second; + } + g_peer_netinfo_results.erase(it); + return true; +} + +bool DropPeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id, s32 req_or_conn_id) { + return TakePeerNetInfoResultLocked(ctx_id, req_or_conn_id, nullptr); +} + +void RecordRttSampleLocked(OrbisNpSignalingConnectionId conn_id, s32 rtt_us) { + const auto it = g_connections.find(conn_id); + if (it == g_connections.end()) { + return; + } + ConnectionInfo& ci = it->second; + const u32 idx = ci.probe_sample_write_index % ConnectionInfo::kProbeSampleCount; + ci.probe_rtt_samples[idx] = rtt_us; + ci.probe_sample_write_index = idx + 1; +} + +void SendEchoPings() { + constexpr s64 kEchoIntervalUs = 1'000'000; + + struct PingTarget { + s32 conn_id; + u32 addr; + u16 port; + s64 now_us; + }; + std::vector targets; + { + SignalingMutexGuard lock; + const s64 now = NowUs(); + for (auto& [cid, ci] : g_connections) { + if (ci.status != ORBIS_NP_SIGNALING_CONN_STATUS_ACTIVE || ci.addr == 0 || + ci.port == 0) { + continue; + } + if (ci.last_echo_ping_us != 0 && now - ci.last_echo_ping_us < kEchoIntervalUs) { + continue; + } + ci.last_echo_ping_us = now; + targets.push_back({cid, ci.addr, ci.port, now}); + } + } + + for (const PingTarget& t : targets) { + SignalingEchoPing ping{}; + ping.conn_id = static_cast(t.conn_id); + ping.send_ts_us = static_cast(t.now_us); + Stubs::SignalingSendTo(&ping, sizeof(ping), t.addr, t.port); + } +} + +static s32 StatusFromState(ConnState state) { + if (state == ConnState::Established) { + return ORBIS_NP_SIGNALING_CONN_STATUS_ACTIVE; + } + if (state == ConnState::Inactive) { + return ORBIS_NP_SIGNALING_CONN_STATUS_INACTIVE; + } + return ORBIS_NP_SIGNALING_CONN_STATUS_PENDING; +} + +static void SetConnStateLocked(ConnectionInfo& ci, ConnState new_state) { + ci.state = new_state; + ci.status = StatusFromState(new_state); +} + +static void StageBasicCallbackLocked(const NpSignalingContext& ctx, s32 ctx_id, s32 conn_id, + s32 event_type, s32 event_data) { + if (!ctx.callback) { + return; + } + QueuedDispatch dispatch; + dispatch.fire_at = std::chrono::steady_clock::now(); + dispatch.delay_ms = 0; + dispatch.ctx_id = ctx_id; + dispatch.conn_id = conn_id; + dispatch.event_type = static_cast(event_type); + dispatch.error_code = static_cast(event_data); + dispatch.callback = ctx.callback; + dispatch.callback_arg = ctx.callback_arg; + { + std::lock_guard dlock(g_dispatch_mutex); + if (g_dispatch_stop) { + return; + } + g_dispatch_queue.emplace(dispatch.fire_at, std::move(dispatch)); + } + g_dispatch_cv.notify_all(); +} + +void DispatchConnectionEvent(s32 conn_id, s32 event_type, s32 event_data) { + SignalingMutexGuard lock; + const auto conn_it = g_connections.find(conn_id); + if (conn_it == g_connections.end()) { + return; + } + const s32 owner_ctx = conn_it->second.ctx_id; + const auto ctx_it = g_contexts.find(owner_ctx); + if (ctx_it != g_contexts.end() && ctx_it->second.active) { + StageBasicCallbackLocked(ctx_it->second, owner_ctx, conn_id, event_type, event_data); + } +} + +void DispatchPeerActivatedEvent(s32 conn_id) { + SignalingMutexGuard lock; + const auto conn_it = g_connections.find(conn_id); + if (conn_it == g_connections.end()) { + return; + } + const s32 owner_ctx = conn_it->second.ctx_id; + const auto owner_it = g_contexts.find(owner_ctx); + const OrbisNpId local_npid = + owner_it != g_contexts.end() ? owner_it->second.owner_npid : OrbisNpId{}; + for (auto& [cid, ctx] : g_contexts) { + if (cid == owner_ctx || !ctx.active) { + continue; + } + if (std::memcmp(&ctx.owner_npid, &local_npid, sizeof(local_npid)) == 0) { + StageBasicCallbackLocked(ctx, cid, conn_id, ORBIS_NP_SIGNALING_EVENT_PEER_ACTIVATED, 0); + } + } +} + +void CloseConnectionAndDispatchDead(s32 conn_id, s32 error_code) { + bool fire = false; + { + SignalingMutexGuard lock; + const auto it = g_connections.find(conn_id); + if (it == g_connections.end()) { + return; + } + ConnectionInfo& ci = it->second; + if (ci.state == ConnState::Inactive || ci.dead_fired) { + return; + } + SetConnStateLocked(ci, ConnState::Inactive); + ci.dead_fired = true; + CancelAllCalloutsLocked(ci); + fire = true; + } + if (fire) { + LOG_DEBUG(Lib_NpSignaling, "Connection {} -> DEAD (errorCode={:#x})", conn_id, + static_cast(error_code)); + DispatchConnectionEvent(conn_id, ORBIS_NP_SIGNALING_EVENT_DEAD, error_code); + SignalingMutexGuard lock; + RemoveConnectionLocked(conn_id); + } +} + +static SignalingControl MakeControlLocked(const ConnectionInfo& ci, ControlKind kind); + +void EstablishConnection(s32 conn_id, bool peer_activated_hint) { + bool first_establish = false; + bool fire_established = false; + bool fire_peer_activated = false; + bool fire_mutual = false; + SignalingControl established_pkt{}; + bool send_established = false; + u32 peer_addr = 0; + u16 peer_port = 0; + { + SignalingMutexGuard lock; + const auto it = g_connections.find(conn_id); + if (it == g_connections.end()) { + return; + } + ConnectionInfo& ci = it->second; + if (peer_activated_hint) { + ci.peer_activated = true; + } + + if (!ci.established_fired) { + ci.state = ConnState::Established; + ci.status = ORBIS_NP_SIGNALING_CONN_STATUS_ACTIVE; + ci.established_fired = true; + CancelTimeoutCalloutLocked(ci); + ArmStepCalloutLocked(ci, kKeepaliveIntervalMs * 1000); + ci.last_peer_rx_us = NowUs(); + first_establish = true; + + if (ci.addr != 0 && ci.port != 0) { + established_pkt = MakeControlLocked(ci, ControlKind::Established); + send_established = true; + peer_addr = ci.addr; + peer_port = ci.port; + } + + if (!ci.locally_activated && !ci.peer_activated_fired) { + ci.peer_activated_fired = true; + fire_peer_activated = true; + } + } + + if (ci.locally_activated && ci.state == ConnState::Established && + !ci.established_event_fired) { + ci.established_event_fired = true; + fire_established = true; + } + + if (ci.locally_activated && ci.state == ConnState::Established && ci.peer_established && + !ci.mutual_fired) { + ci.mutual_fired = true; + fire_mutual = true; + } + } + + if (send_established) { + Stubs::ControlSendTo(&established_pkt, sizeof(established_pkt), peer_addr, peer_port); + } + if (first_establish) { + LOG_DEBUG(Lib_NpSignaling, "Connection {} -> ESTABLISHED ({})", conn_id, + fire_established ? "owner" : "answerer/non-owner"); + } + if (fire_peer_activated) { + DispatchPeerActivatedEvent(conn_id); + } + if (fire_established) { + DispatchConnectionEvent(conn_id, ORBIS_NP_SIGNALING_EVENT_ESTABLISHED, 0); + } + if (fire_mutual) { + DispatchConnectionEvent(conn_id, ORBIS_NP_SIGNALING_EVENT_MUTUAL_ACTIVATED, 0); + } +} + +void DeactivateConnectionFaithful(s32 conn_id) { + bool transient_close = false; + { + SignalingMutexGuard lock; + const auto it = g_connections.find(conn_id); + if (it == g_connections.end()) { + return; + } + ConnectionInfo& ci = it->second; + if (ci.state == ConnState::Established && !ci.dead_fired && ci.deactivate_lingering) { + return; + } + if (ci.state == ConnState::Established && !ci.dead_fired) { + SendControlCloseLocked(ci, ControlReason::Deactivate); + ci.locally_activated = false; + ci.deactivate_lingering = true; + CancelStepCalloutLocked(ci); + ArmTimeoutCalloutLocked(ci, 60'000'000); + LOG_DEBUG(Lib_NpSignaling, + "Connection {} deactivated (established) -> 60s keepalive linger before DEAD", + conn_id); + } else if (!ci.dead_fired) { + transient_close = true; + } + } + if (transient_close) { + CloseConnectionAndDispatchDead(conn_id, ORBIS_NP_SIGNALING_ERROR_TERMINATED_BY_MYSELF); + } +} + +void TerminateConnectionFaithful(s32 conn_id) { + { + SignalingMutexGuard lock; + const auto it = g_connections.find(conn_id); + if (it != g_connections.end()) { + SendControlCloseLocked(it->second, ControlReason::Terminate); + } + } + CloseConnectionAndDispatchDead(conn_id, ORBIS_NP_SIGNALING_ERROR_TERMINATED_BY_MYSELF); +} + +namespace { + +SignalingHandshake MakeHandshakeLocked(const ConnectionInfo& ci, HandshakeKind kind) { + SignalingHandshake pkt{}; + pkt.kind = static_cast(kind); + pkt.from_conn_id = static_cast(ci.conn_id); + pkt.to_conn_id = 0; + const auto ctx_it = g_contexts.find(ci.ctx_id); + if (ctx_it != g_contexts.end()) { + std::memcpy(pkt.online_id_from, ctx_it->second.owner_online_id.data, + ORBIS_NP_ONLINEID_MAX_LENGTH); + } + std::memcpy(pkt.online_id_to, ci.online_id.data, ORBIS_NP_ONLINEID_MAX_LENGTH); + auto* netinfo = Common::Singleton::Instance(); + pkt.mapped_addr = netinfo->GetExternalIp(); + pkt.mapped_port = 0; + return pkt; +} + +void SendHandshakeLocked(ConnectionInfo& ci, const SignalingHandshake& pkt) { + if (ci.addr == 0 || ci.port == 0) { + return; + } + ci.last_handshake_send_us = NowUs(); + const int sent = Stubs::SignalingSendTo(&pkt, sizeof(pkt), ci.addr, ci.port); + LOG_DEBUG(Lib_NpSignaling, "Handshake[{}] SEND kind={} -> {:#x}:{} ({} bytes, rc={})", + ci.conn_id, pkt.kind, ci.addr, sceNetNtohs(ci.port), sizeof(pkt), sent); +} + +} // namespace + +void StartHandshakeInitiator(OrbisNpSignalingConnectionId conn_id) { + SignalingHandshake offer{}; + bool send = false; + { + SignalingMutexGuard lock; + const auto it = g_connections.find(conn_id); + if (it == g_connections.end()) { + return; + } + ConnectionInfo& ci = it->second; + ci.is_initiator = true; + SetConnStateLocked(ci, ConnState::SendingOffer); + if (!ci.timeout_callout_armed) { + ArmTimeoutCalloutLocked(ci, kHandshakeConnectTimeoutMs * 1000); + } + ArmStepCalloutLocked(ci, kHandshakeRetransmitMs * 1000); + offer = MakeHandshakeLocked(ci, HandshakeKind::Offer); + SendHandshakeLocked(ci, offer); + send = true; + } + if (send) { + LOG_DEBUG(Lib_NpSignaling, "Handshake[{}]: -> SENDING_OFFER", conn_id); + } +} + +void QueueActivationLocked(OrbisNpSignalingConnectionId conn_id, std::string_view peer_online_id) { + g_pending_activations.push_back({conn_id, std::string(peer_online_id)}); +} + +void ProcessPendingActivations() { + std::vector work; + { + SignalingMutexGuard lock; + if (g_pending_activations.empty()) { + return; + } + work.swap(g_pending_activations); + } + + for (const PendingActivation& act : work) { + { + SignalingMutexGuard lock; + const auto it = g_connections.find(act.conn_id); + if (it == g_connections.end() || it->second.state == ConnState::Inactive) { + continue; + } + } + + u32 peer_addr = 0; + u16 peer_port = 0; + const bool resolved = Stubs::ResolvePeer(act.peer_online_id, &peer_addr, &peer_port) && + peer_addr != 0 && peer_port != 0; + + if (!resolved) { + LOG_WARNING(Lib_NpSignaling, "peer '{}' endpoint unresolved; connection {} 30s timeout", + act.peer_online_id, act.conn_id); + continue; + } + + { + SignalingMutexGuard lock; + const auto it = g_connections.find(act.conn_id); + if (it == g_connections.end() || it->second.state == ConnState::Inactive) { + continue; + } + it->second.addr = peer_addr; + it->second.port = peer_port; + SendActivationRequestLocked(it->second); + } + StartHandshakeInitiator(act.conn_id); + } +} + +void HandleHandshakePacket(u32 from_addr, u16 from_port, const SignalingHandshake& pkt) { + char from_id_buf[ORBIS_NP_ONLINEID_MAX_LENGTH + 1]{}; + std::memcpy(from_id_buf, pkt.online_id_from, ORBIS_NP_ONLINEID_MAX_LENGTH); + const std::string from_id(from_id_buf); + const HandshakeKind kind = static_cast(pkt.kind); + + s32 establish_conn = 0; + SignalingHandshake reply{}; + bool send_reply = false; + s32 reply_conn = 0; + + { + SignalingMutexGuard lock; + + s32 conn_id = 0; + for (auto& [cid, ci] : g_connections) { + if (ci.state == ConnState::Inactive || ci.dead_fired) { + continue; + } + if (OnlineIdEqualsString(ci.online_id, from_id)) { + conn_id = cid; + break; + } + } + + if (conn_id != 0) { + g_connections[conn_id].last_peer_rx_us = NowUs(); + } + + if (kind == HandshakeKind::Offer) { + if (conn_id == 0) { + return; + } + ConnectionInfo& ci = g_connections[conn_id]; + ci.addr = from_addr; + ci.port = from_port; + ci.peer_activated = true; + if (pkt.mapped_addr != 0) { + ci.addr = pkt.mapped_addr; + } + if (ci.state != ConnState::Established) { + SetConnStateLocked(ci, ConnState::SendingAccept); + if (!ci.timeout_callout_armed) { + ArmTimeoutCalloutLocked(ci, kHandshakeConnectTimeoutMs * 1000); + } + ArmStepCalloutLocked(ci, kHandshakeRetransmitMs * 1000); + reply = MakeHandshakeLocked(ci, HandshakeKind::Accept); + send_reply = true; + reply_conn = conn_id; + } + } else if (conn_id == 0) { + return; + } else if (kind == HandshakeKind::Accept) { + ConnectionInfo& ci = g_connections[conn_id]; + ci.peer_activated = true; + if (pkt.mapped_addr != 0) { + ci.addr = pkt.mapped_addr; + } else { + ci.addr = from_addr; + ci.port = from_port; + } + if (ci.state == ConnState::SendingOffer || ci.state == ConnState::WaitAccept || + ci.state == ConnState::SendingAccept || ci.state == ConnState::WaitOffer) { + SetConnStateLocked(ci, ConnState::ConnCheck); + ArmStepCalloutLocked(ci, kHandshakeRetransmitMs * 1000); + reply = MakeHandshakeLocked(ci, HandshakeKind::Check); + reply.nonce = static_cast(NowUs()); + send_reply = true; + reply_conn = conn_id; + } + } else if (kind == HandshakeKind::Check) { + ConnectionInfo& ci = g_connections[conn_id]; + reply = MakeHandshakeLocked(ci, HandshakeKind::CheckAck); + reply.nonce = pkt.nonce; + send_reply = true; + reply_conn = conn_id; + if (ci.state == ConnState::SendingAccept || ci.state == ConnState::WaitOffer) { + SetConnStateLocked(ci, ConnState::ConnCheck); + } + } else if (kind == HandshakeKind::CheckAck) { + ConnectionInfo& ci = g_connections[conn_id]; + const s64 rtt_us = NowUs() - static_cast(pkt.nonce); + if (rtt_us >= 0) { + RecordRttSampleLocked(conn_id, static_cast(rtt_us)); + } + if (ci.state != ConnState::Established) { + establish_conn = conn_id; + } + } + } + + if (send_reply) { + SignalingMutexGuard lock; + const auto it = g_connections.find(reply_conn); + if (it != g_connections.end()) { + SendHandshakeLocked(it->second, reply); + } + } + + if (establish_conn != 0) { + EstablishConnection(establish_conn, true); + } +} + +static void PS4_SYSV_ABI TimeoutCalloutHandler(u64 arg) { + const s32 conn_id = static_cast(arg); + bool fire_dead = false; + { + SignalingMutexGuard lock; + const auto it = g_connections.find(conn_id); + if (it == g_connections.end()) { + return; + } + ConnectionInfo& ci = it->second; + ci.timeout_callout_armed = false; + if (ci.state != ConnState::Established) { + fire_dead = true; + } else if (ci.deactivate_lingering) { + fire_dead = true; + } + } + if (fire_dead) { + CloseConnectionAndDispatchDead(conn_id, ORBIS_NP_SIGNALING_ERROR_TIMEOUT); + } +} + +static void PS4_SYSV_ABI StepCalloutHandler(u64 arg) { + const s32 conn_id = static_cast(arg); + bool liveness_dead = false; + { + SignalingMutexGuard lock; + const auto it = g_connections.find(conn_id); + if (it == g_connections.end()) { + return; + } + ConnectionInfo& ci = it->second; + ci.step_callout_armed = false; + const s64 now_us = NowUs(); + + if (ci.state == ConnState::Established) { + if (!ci.deactivate_lingering && ci.addr != 0 && ci.port != 0) { + if (ci.last_peer_rx_us != 0 && + now_us - ci.last_peer_rx_us >= kEstablishedLivenessTimeoutMs * 1000) { + liveness_dead = true; + } else { + SignalingHandshake pkt = MakeHandshakeLocked(ci, HandshakeKind::Check); + pkt.nonce = static_cast(now_us); + SendHandshakeLocked(ci, pkt); + ArmStepCalloutLocked(ci, kKeepaliveIntervalMs * 1000); + } + } + } else if (ci.state != ConnState::Inactive) { + if (ci.addr != 0 && ci.port != 0) { + HandshakeKind kind = HandshakeKind::Offer; + switch (ci.state) { + case ConnState::SendingOffer: + case ConnState::WaitAccept: + kind = HandshakeKind::Offer; + break; + case ConnState::SendingAccept: + case ConnState::WaitOffer: + kind = HandshakeKind::Accept; + break; + case ConnState::ConnCheck: + kind = HandshakeKind::Check; + break; + default: + break; + } + SignalingHandshake pkt = MakeHandshakeLocked(ci, kind); + if (kind == HandshakeKind::Check) { + pkt.nonce = static_cast(now_us); + } + SendHandshakeLocked(ci, pkt); + } + ArmStepCalloutLocked(ci, kHandshakeRetransmitMs * 1000); + } + } + if (liveness_dead) { + CloseConnectionAndDispatchDead(conn_id, ORBIS_NP_SIGNALING_ERROR_TIMEOUT); + } +} + +static SignalingControl MakeControlLocked(const ConnectionInfo& ci, ControlKind kind) { + SignalingControl pkt{}; + pkt.kind = static_cast(kind); + pkt.from_conn_id = static_cast(ci.conn_id); + const auto ctx_it = g_contexts.find(ci.ctx_id); + if (ctx_it != g_contexts.end()) { + std::memcpy(pkt.online_id_from, ctx_it->second.owner_online_id.data, + ORBIS_NP_ONLINEID_MAX_LENGTH); + } + std::memcpy(pkt.online_id_to, ci.online_id.data, ORBIS_NP_ONLINEID_MAX_LENGTH); + auto* netinfo = Common::Singleton::Instance(); + pkt.mapped_addr = netinfo->GetExternalIp(); + pkt.mapped_port = 0; + return pkt; +} + +void SendActivationRequestLocked(const ConnectionInfo& ci) { + if (ci.addr == 0 || ci.port == 0) { + return; + } + SignalingControl pkt = MakeControlLocked(ci, ControlKind::ActivationRequest); + Stubs::ControlSendTo(&pkt, sizeof(pkt), ci.addr, ci.port); +} + +void SendControlCloseLocked(const ConnectionInfo& ci, ControlReason reason) { + if (ci.addr == 0 || ci.port == 0) { + return; + } + SignalingControl pkt = MakeControlLocked(ci, ControlKind::Close); + pkt.reason = static_cast(reason); + Stubs::ControlSendTo(&pkt, sizeof(pkt), ci.addr, ci.port); +} + +void HandleControlPacket(u32 from_addr, u16 from_port, const SignalingControl& pkt) { + char from_id_buf[ORBIS_NP_ONLINEID_MAX_LENGTH + 1]{}; + std::memcpy(from_id_buf, pkt.online_id_from, ORBIS_NP_ONLINEID_MAX_LENGTH); + const std::string from_id(from_id_buf); + const ControlKind kind = static_cast(pkt.kind); + + s32 start_handshake_conn = 0; + s32 establish_conn = 0; + s32 dead_conn = 0; + s32 peer_deactivated_conn = 0; + SignalingControl ack{}; + bool send_ack = false; + u32 ack_addr = 0; + u16 ack_port = 0; + + { + SignalingMutexGuard lock; + + s32 conn_id = 0; + for (auto& [cid, ci] : g_connections) { + if (ci.state == ConnState::Inactive || ci.dead_fired) { + continue; + } + if (OnlineIdEqualsString(ci.online_id, from_id)) { + conn_id = cid; + break; + } + } + + if (kind == ControlKind::ActivationRequest) { + if (conn_id == 0) { + s32 ctx_id = 0; + char to_id_buf[ORBIS_NP_ONLINEID_MAX_LENGTH + 1]{}; + std::memcpy(to_id_buf, pkt.online_id_to, ORBIS_NP_ONLINEID_MAX_LENGTH); + const std::string to_id(to_id_buf); + for (auto& [cid, ctx] : g_contexts) { + if (ctx.active && OnlineIdEqualsString(ctx.owner_online_id, to_id)) { + ctx_id = cid; + break; + } + } + if (ctx_id == 0) { + return; + } + conn_id = AllocateConnectionIdLocked(); + if (conn_id < 0) { + return; + } + ConnectionInfo ci{}; + ci.conn_id = conn_id; + ci.ctx_id = ctx_id; + ci.locally_activated = false; + std::memcpy(ci.online_id.data, pkt.online_id_from, ORBIS_NP_ONLINEID_MAX_LENGTH); + ci.npid.handle = ci.online_id; + g_connections[conn_id] = std::move(ci); + g_npid_to_conn[MakeCtxNpIdKey(ctx_id, g_connections[conn_id].npid)] = conn_id; + LOG_INFO(Lib_NpSignaling, + "Control: ActivationRequest from '{}' -> created answerer connection {}", + from_id, conn_id); + } + ConnectionInfo& ci = g_connections[conn_id]; + ci.peer_activated = true; + ci.addr = pkt.mapped_addr != 0 ? pkt.mapped_addr : from_addr; + ci.port = from_port; + if (ci.state == ConnState::Inactive) { + if (!ci.timeout_callout_armed) { + ArmTimeoutCalloutLocked(ci, kHandshakeConnectTimeoutMs * 1000); + } + start_handshake_conn = conn_id; + } + ack = MakeControlLocked(ci, ControlKind::ActivationAck); + send_ack = true; + ack_addr = ci.addr; + ack_port = ci.port; + } else if (conn_id == 0) { + return; + } else if (kind == ControlKind::ActivationAck) { + ConnectionInfo& ci = g_connections[conn_id]; + ci.peer_activated = true; + if (pkt.mapped_addr != 0) { + ci.addr = pkt.mapped_addr; + } + } else if (kind == ControlKind::Established) { + ConnectionInfo& ci = g_connections[conn_id]; + ci.peer_established = true; + if (ci.state == ConnState::Established) { + establish_conn = conn_id; + } + } else if (kind == ControlKind::Close) { + ConnectionInfo& ci = g_connections[conn_id]; + const ControlReason reason = static_cast(pkt.reason); + if (reason == ControlReason::Deactivate && ci.state == ConnState::Established) { + peer_deactivated_conn = conn_id; + } else { + dead_conn = conn_id; + } + } + } + + if (send_ack && ack_addr != 0 && ack_port != 0) { + Stubs::ControlSendTo(&ack, sizeof(ack), ack_addr, ack_port); + } + if (start_handshake_conn != 0) { + StartHandshakeInitiator(start_handshake_conn); + } + if (establish_conn != 0) { + EstablishConnection(establish_conn, false); + } + if (peer_deactivated_conn != 0) { + DispatchConnectionEvent(peer_deactivated_conn, ORBIS_NP_SIGNALING_EVENT_PEER_DEACTIVATED, + 0); + } + if (dead_conn != 0) { + CloseConnectionAndDispatchDead(dead_conn, ORBIS_NP_SIGNALING_ERROR_TERMINATED_BY_PEER); + } +} + +void SendStunPing(s32 ctx_id) { + OrbisNpOnlineId online_id{}; + + { + SignalingMutexGuard lock; + const auto it = g_contexts.find(ctx_id); + if (it == g_contexts.end() || !it->second.active) { + return; + } + online_id = it->second.owner_online_id; + } + + if (!Stubs::TransportIsReady()) { + return; + } + + const u32 server_addr = Stubs::MmServerAddr(); + const u16 server_udp = Stubs::MmServerUdpPort(); + + if (server_addr == 0 || server_udp == 0) { + LOG_WARNING(Lib_NpSignaling, "ctxId={} skipped (server_addr={:#x} udp_port={})", ctx_id, + server_addr, sceNetNtohs(server_udp)); + return; + } + + StunPing ping{}; + ping.cmd = 0x01; + std::memcpy(ping.online_id, online_id.data, ORBIS_NP_ONLINEID_MAX_LENGTH); + + LOG_DEBUG(Lib_NpSignaling, "ctxId={} online_id='{}' server={:#x}:{}", ctx_id, + OnlineIdToString(online_id), server_addr, sceNetNtohs(server_udp)); + + Stubs::SignalingSendTo(&ping, sizeof(ping), server_addr, server_udp); +} + +s32 GetActiveConnectionIdForPeer(std::string_view online_id) { + SignalingMutexGuard lock; + for (const auto& [conn_id, ci] : g_connections) { + if (OnlineIdEqualsString(ci.online_id, online_id) && + ci.status == ORBIS_NP_SIGNALING_CONN_STATUS_ACTIVE) { + return conn_id; + } + } + return 0; +} + +s32 GetConnectionStatusForPeer(std::string_view online_id, s32* out_conn_id) { + SignalingMutexGuard lock; + for (const auto& [conn_id, ci] : g_connections) { + if (OnlineIdEqualsString(ci.online_id, online_id)) { + if (out_conn_id) { + *out_conn_id = conn_id; + } + return ci.status; + } + } + if (out_conn_id) { + *out_conn_id = 0; + } + return ORBIS_NP_SIGNALING_CONN_STATUS_INACTIVE; +} + +void ClearActiveDeadlines() {} +void SetRoomLeft() {} +void ClearConnectionDeadlineForPeer(std::string_view) {} + +bool GetPeerAddress(std::string_view online_id, u32* out_addr, u16* out_port) { + SignalingMutexGuard lock; + for (const auto& [conn_id, ci] : g_connections) { + if (OnlineIdEqualsString(ci.online_id, online_id)) { + if (out_addr) + *out_addr = ci.addr; + if (out_port) + *out_port = ci.port; + return true; + } + } + return false; +} + +} // namespace Libraries::Np::NpSignaling diff --git a/src/core/libraries/np/np_signaling/np_signaling_state.h b/src/core/libraries/np/np_signaling/np_signaling_state.h new file mode 100644 index 000000000..3d0e7c662 --- /dev/null +++ b/src/core/libraries/np/np_signaling/np_signaling_state.h @@ -0,0 +1,355 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/types.h" +#include "core/libraries/kernel/threads.h" +#include "core/libraries/np/np_common.h" +#include "core/libraries/np/np_signaling/np_signaling.h" +#include "core/libraries/np/np_types.h" + +namespace Libraries::Np::NpSignaling { + +extern NpCommon::OrbisNpCalloutContext g_callout_ctx; +extern bool g_callout_ctx_active; + +constexpr s64 kActivateCooldownUs = 6'000'000; +constexpr s64 kActivateBudgetMaxUs = 600'000'000; +constexpr u32 kSigRetryMs = 500; +constexpr u32 kSigPingMs = 5'000; + +#pragma pack(push, 1) +struct StunPing { + u8 cmd = 0x01; + u8 online_id[16]{}; + u32 local_ip = 0; +}; +static_assert(sizeof(StunPing) == 21, "StunPing must be exactly 21 bytes"); +#pragma pack(pop) + +#pragma pack(push, 1) +struct StunEcho { + u32 ext_ip = 0; + u16 ext_port = 0; +}; +static_assert(sizeof(StunEcho) == 6, "StunEcho must be exactly 6 bytes"); +#pragma pack(pop) + +inline constexpr u8 kSignalingMagic[4] = {'S', 'H', 'A', 'D'}; + +enum class SignalingPacketType : u8 { + EchoPing = 0x05, + EchoPong = 0x06, + Handshake = 0x07, + Control = 0x10, +}; + +#pragma pack(push, 1) +struct SignalingEchoPing { + u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]}; + u8 type = static_cast(SignalingPacketType::EchoPing); + u32 conn_id = 0; + u64 send_ts_us = 0; +}; +static_assert(sizeof(SignalingEchoPing) == 17); +struct SignalingEchoPong { + u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]}; + u8 type = static_cast(SignalingPacketType::EchoPong); + u32 conn_id = 0; + u64 orig_ts_us = 0; +}; +static_assert(sizeof(SignalingEchoPong) == 17); +#pragma pack(pop) + +#pragma pack(push, 1) +struct SignalingHandshake { + u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]}; + u8 type = static_cast(SignalingPacketType::Handshake); + u8 kind = 0; + u8 online_id_from[16]{}; + u8 online_id_to[16]{}; + u32 from_conn_id = 0; + u32 to_conn_id = 0; + u32 mapped_addr = 0; + u16 mapped_port = 0; + u16 reserved = 0; + u64 nonce = 0; +}; +static_assert(sizeof(SignalingHandshake) == 0x3e); +#pragma pack(pop) + +enum class HandshakeKind : u8 { + Offer = 1, + Accept = 2, + Check = 3, + CheckAck = 4, +}; + +#pragma pack(push, 1) +struct SignalingControl { + u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]}; + u8 type = static_cast(SignalingPacketType::Control); + u8 kind = 0; + u8 online_id_from[16]{}; + u8 online_id_to[16]{}; + u32 from_conn_id = 0; + u32 mapped_addr = 0; + u16 mapped_port = 0; + u16 reason = 0; +}; +static_assert(sizeof(SignalingControl) == 0x32); +#pragma pack(pop) + +enum class ControlKind : u8 { + ActivationRequest = 1, + ActivationAck = 2, + Close = 3, + Established = 4, +}; + +enum class ControlReason : u16 { + Terminate = 0, + Deactivate = 1, +}; + +enum class ConnState : s32 { + Inactive = 0, + SendingOffer = 5, + WaitOffer = 6, + ConnCheck = 7, + SendingAccept = 8, + WaitAccept = 9, + Established = 10, +}; + +struct ConnectionInfo { + OrbisNpSignalingConnectionId conn_id = 0; + OrbisNpSignalingContextId ctx_id = 0; + + u32 addr = 0; + u16 port = 0; + + u32 local_addr = 0; + u16 local_port = 0; + + s32 status = ORBIS_NP_SIGNALING_CONN_STATUS_INACTIVE; + + OrbisNpId npid{}; + OrbisNpOnlineId online_id{}; + + s32 activation_mode = 0; + + static constexpr u32 kProbeSampleCount = 6; + s32 probe_rtt_samples[kProbeSampleCount] = {-1, -1, -1, -1, -1, -1}; + u32 probe_sample_write_index = 0; + + s64 last_echo_ping_us = 0; + + s64 last_peer_rx_us = 0; + + ConnState state = ConnState::Inactive; + bool established_fired = false; + bool established_event_fired = false; + bool peer_activated = false; + bool locally_activated = false; + bool peer_activated_fired = false; + bool peer_established = false; + bool mutual_fired = false; + bool dead_fired = false; + bool is_initiator = false; + s64 last_handshake_send_us = 0; + + NpCommon::OrbisNpCalloutEntry timeout_callout{}; + bool timeout_callout_armed = false; + NpCommon::OrbisNpCalloutEntry step_callout{}; + bool step_callout_armed = false; + bool deactivate_lingering = false; + + u32 echo_derived_value = 0; +}; + +struct NpSignalingContext { + OrbisNpSignalingHandler callback{nullptr}; + void* callback_arg{nullptr}; + bool active{false}; + + u32 flags{0}; + + u32 compiled_sdk_version{0}; + + u64 account_id{0}; + u32 platform_type{0}; + + OrbisNpId owner_npid{}; + OrbisNpOnlineId owner_online_id{}; + + u16 bound_port{0}; + + std::atomic ext_addr{0}; + std::atomic ext_port{0}; + std::mutex stun_mutex{}; + std::condition_variable stun_cv{}; + + s64 activate_budget_us{0}; + s64 activate_last_update_us{0}; +}; + +struct CtxNpIdKey { + OrbisNpSignalingContextId ctx_id = 0; + OrbisNpId npid{}; +}; + +struct CtxNpIdKeyHash { + size_t operator()(const CtxNpIdKey& key) const noexcept { + size_t hash = static_cast(key.ctx_id); + const auto* bytes = reinterpret_cast(&key.npid); + for (size_t i = 0; i < sizeof(key.npid); ++i) { + hash ^= static_cast(bytes[i]) + 0x9e3779b9 + (hash << 6) + (hash >> 2); + } + return hash; + } +}; + +inline bool operator==(const CtxNpIdKey& lhs, const CtxNpIdKey& rhs) { + return lhs.ctx_id == rhs.ctx_id && std::memcmp(&lhs.npid, &rhs.npid, sizeof(lhs.npid)) == 0; +} + +struct QueuedDispatch { + std::chrono::steady_clock::time_point fire_at{}; + u32 delay_ms = 0; + OrbisNpSignalingContextId ctx_id = 0; + OrbisNpSignalingConnectionId conn_id = 0; + u32 event_type = 0; + u32 error_code = 0; + OrbisNpSignalingHandler callback = nullptr; + void* callback_arg = nullptr; +}; + +constexpr u32 kPeerNetInfoRequestIdPrefix = 0x21000000; + +struct PeerNetInfoResult { + OrbisNpSignalingContextId ctx_id = 0; + OrbisNpSignalingConnectionId conn_id = 0; + u32 local_ipv4 = 0; + u32 external_ipv4 = 0; + u32 nat_route_kind = 0; +}; + +extern std::unordered_map g_contexts; +extern std::unordered_map g_connections; +extern std::unordered_map g_npid_to_conn; +extern std::unordered_map g_peer_netinfo_results; +struct PendingActivation { + OrbisNpSignalingConnectionId conn_id = 0; + std::string peer_online_id; +}; +extern std::vector g_pending_activations; +extern u32 g_peer_netinfo_next_id; +extern u32 g_peak_connection_count; +extern u32 g_last_assigned_context_id; +extern u32 g_connection_id_seed; +extern bool g_initialized; + +extern Libraries::Kernel::PthreadMutexT g_mutex_storage; + +struct SignalingMutexGuard { + SignalingMutexGuard(); + ~SignalingMutexGuard(); + SignalingMutexGuard(const SignalingMutexGuard&) = delete; + SignalingMutexGuard& operator=(const SignalingMutexGuard&) = delete; +}; + +void InitSignalingMutex(); +void DestroySignalingMutex(); + +extern std::multimap g_dispatch_queue; +extern std::mutex g_dispatch_mutex; +extern std::condition_variable g_dispatch_cv; +extern bool g_dispatch_stop; + +long long NowMs(); +s64 NowUs(); +std::string OnlineIdToString(const OrbisNpOnlineId& online_id_in); +std::string OnlineIdFromNpId(const OrbisNpId& np_id); +OrbisNpId NpIdFromOnlineId(const OrbisNpOnlineId& online_id); +bool OnlineIdEqualsString(const OrbisNpOnlineId& online_id, std::string_view value); +bool IsValidNpId(const OrbisNpId& np_id); +s32 NormalizeNpId(const void* np_id, OrbisNpId* out_npid, OrbisNpOnlineId* out_online_id = nullptr); +CtxNpIdKey MakeCtxNpIdKey(OrbisNpSignalingContextId ctx_id, const OrbisNpId& npid); + +bool IsContextValidLocked(OrbisNpSignalingContextId ctx_id); +OrbisNpSignalingContextId AllocateContextIdLocked(); +OrbisNpSignalingConnectionId AllocateConnectionIdLocked(); +void RemoveConnectionLocked(OrbisNpSignalingConnectionId conn_id); +void RemoveContextConnectionsLocked(OrbisNpSignalingContextId ctx_id); + +s32 ConnectionStateFromStatus(s32 status); + +u32 CaptureCompiledSdkVersion(); + +u32 ParseIpv4Nbo(const std::string& dotted); + +s32 GetConnectionInfoInternal(OrbisNpSignalingContextId ctx_id, + OrbisNpSignalingConnectionId conn_id, s32 info_code, void* out_a, + void* out_b); + +bool ConsumeActivationBudgetGatedLocked(NpSignalingContext& ctx); + +void SnapshotConnectionStatisticsLocked(u32* out_peak, u32* out_active, u32* out_transient, + u32* out_established); + +OrbisNpSignalingRequestId StagePeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id); +bool TakePeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id, s32 req_or_conn_id, + PeerNetInfoResult* out); +bool DropPeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id, s32 req_or_conn_id); + +void RecordRttSampleLocked(OrbisNpSignalingConnectionId conn_id, s32 rtt_us); +void SendEchoPings(); + +void DispatchConnectionEvent(OrbisNpSignalingConnectionId conn_id, s32 event_type, s32 event_data); + +void DispatchPeerActivatedEvent(OrbisNpSignalingConnectionId conn_id); + +void CloseConnectionAndDispatchDead(OrbisNpSignalingConnectionId conn_id, s32 error_code); + +void EstablishConnection(OrbisNpSignalingConnectionId conn_id, bool peer_activated_hint); + +void DeactivateConnectionFaithful(OrbisNpSignalingConnectionId conn_id); + +void TerminateConnectionFaithful(OrbisNpSignalingConnectionId conn_id); + +void StartHandshakeInitiator(OrbisNpSignalingConnectionId conn_id); + +void QueueActivationLocked(OrbisNpSignalingConnectionId conn_id, std::string_view peer_online_id); +void ProcessPendingActivations(); + +void HandleHandshakePacket(u32 from_addr, u16 from_port, const SignalingHandshake& pkt); + +void ArmConnectTimeoutLocked(OrbisNpSignalingConnectionId conn_id); +void ClearLingerAndTimeoutLocked(OrbisNpSignalingConnectionId conn_id); + +void SendActivationRequestLocked(const ConnectionInfo& ci); + +void SendControlCloseLocked(const ConnectionInfo& ci, ControlReason reason); + +void HandleControlPacket(u32 from_addr, u16 from_port, const SignalingControl& pkt); + +const char* SignalingEventName(u32 event_type); +bool ConsumeActivationBudgetLocked(NpSignalingContext& ctx); +void SendStunPing(OrbisNpSignalingContextId ctx_id); +void ClearActiveDeadlines(); +void ClearConnectionDeadlineForPeer(std::string_view npid); +void SetRoomLeft(); + +} // namespace Libraries::Np::NpSignaling diff --git a/src/core/libraries/np/np_signaling/np_signaling_stubs.cpp b/src/core/libraries/np/np_signaling/np_signaling_stubs.cpp new file mode 100644 index 000000000..d1ec9509d --- /dev/null +++ b/src/core/libraries/np/np_signaling/np_signaling_stubs.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/libraries/np/np_signaling/np_signaling_stubs.h" + +namespace Libraries::Np::NpSignaling::Stubs { + +namespace { +TransportHooks g_transport{}; +PeerResolver g_peer_resolver = nullptr; +u32 g_mm_server_addr = 0; +u16 g_mm_server_udp_port = 0; +} // namespace + +void SetTransportHooks(const TransportHooks& hooks) { + g_transport = hooks; +} + +int SignalingSendTo(const void* data, u32 len, u32 dest_addr, u16 dest_port) { + return g_transport.signaling_send ? g_transport.signaling_send(data, len, dest_addr, dest_port) + : -1; +} + +int SignalingRecvFrom(void* buf, u32 len, u32* from_addr, u16* from_port) { + return g_transport.signaling_recv ? g_transport.signaling_recv(buf, len, from_addr, from_port) + : -1; +} + +int ControlSendTo(const void* data, u32 len, u32 dest_addr, u16 dest_port) { + return g_transport.control_send ? g_transport.control_send(data, len, dest_addr, dest_port) + : -1; +} + +int ControlRecvFrom(void* buf, u32 len, u32* from_addr, u16* from_port) { + return g_transport.control_recv ? g_transport.control_recv(buf, len, from_addr, from_port) : -1; +} + +bool TransportIsReady() { + return g_transport.transport_ready ? g_transport.transport_ready() : false; +} + +u16 ConfiguredPort() { + return g_transport.configured_port ? g_transport.configured_port() : 0; +} + +void SetPeerResolver(PeerResolver fn) { + g_peer_resolver = fn; +} + +void SetMmServerEndpoint(u32 addr, u16 udp_port) { + g_mm_server_addr = addr; + g_mm_server_udp_port = udp_port; +} + +bool ResolvePeer(std::string_view online_id, u32* out_addr, u16* out_port) { + return g_peer_resolver ? g_peer_resolver(online_id, out_addr, out_port) : false; +} + +u32 MmServerAddr() { + return g_mm_server_addr; +} + +u16 MmServerUdpPort() { + return g_mm_server_udp_port; +} + +} // namespace Libraries::Np::NpSignaling::Stubs diff --git a/src/core/libraries/np/np_signaling/np_signaling_stubs.h b/src/core/libraries/np/np_signaling/np_signaling_stubs.h new file mode 100644 index 000000000..7645cb98a --- /dev/null +++ b/src/core/libraries/np/np_signaling/np_signaling_stubs.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "common/types.h" + +namespace Libraries::Np::NpSignaling::Stubs { + +struct TransportHooks { + int (*signaling_send)(const void* data, u32 len, u32 dest_addr, u16 dest_port) = nullptr; + int (*signaling_recv)(void* buf, u32 len, u32* from_addr, u16* from_port) = nullptr; + int (*control_send)(const void* data, u32 len, u32 dest_addr, u16 dest_port) = nullptr; + int (*control_recv)(void* buf, u32 len, u32* from_addr, u16* from_port) = nullptr; + bool (*transport_ready)() = nullptr; + u16 (*configured_port)() = nullptr; +}; +void SetTransportHooks(const TransportHooks& hooks); + +int SignalingSendTo(const void* data, u32 len, u32 dest_addr, u16 dest_port); +int SignalingRecvFrom(void* buf, u32 len, u32* from_addr, u16* from_port); +int ControlSendTo(const void* data, u32 len, u32 dest_addr, u16 dest_port); +int ControlRecvFrom(void* buf, u32 len, u32* from_addr, u16* from_port); +bool TransportIsReady(); +u16 ConfiguredPort(); + +using PeerResolver = bool (*)(std::string_view online_id, u32* out_addr, u16* out_port); +void SetPeerResolver(PeerResolver fn); +void SetMmServerEndpoint(u32 addr, u16 udp_port); + +bool ResolvePeer(std::string_view online_id, u32* out_addr, u16* out_port); +u32 MmServerAddr(); +u16 MmServerUdpPort(); + +} // namespace Libraries::Np::NpSignaling::Stubs