feat(libSceNpMatching2): model contexts offline

The context calls, the callback registrations and
SetDefaultRequestOptParam threw. They now follow shadPS4's checks and
context ids, ContextStart returns SCE_NET_ERROR_RESOLVER_ETIMEDOUT, the
answer shadPS4 records for a console without internet access, and
Terminate releases the contexts.
This commit is contained in:
Federico Marras committed 2026-10-22 22:55:51 +02:00
1 parent 26469c711a
commit 4bb91851bb
4 files changed
+213 -26

No files matched your search

@@ -0,0 +1,16 @@
add_library(libSceNpMatching2 SHARED EXCLUDE_FROM_ALL Export.cpp)
target_include_directories(libSceNpMatching2 PRIVATE ${LIBS_INCLUDE_DIR})
configure_windows_unwind(libSceNpMatching2)
if(BUILD_TESTING)
add_test_executable(guest_np_matching2_offline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpMatching2Offline.cpp)
target_include_directories(guest_np_matching2_offline_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_np_matching2_offline_tests PRIVATE libSceNpMatching2)
configure_windows_unwind(guest_np_matching2_offline_tests)
add_test(NAME guest_np_matching2_offline COMMAND guest_np_matching2_offline_tests)
set_tests_properties(guest_np_matching2_offline PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_np_matching2_offline PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpMatching2>")
endif()
endif()
+77 -26
View File
@@ -1,12 +1,19 @@
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <unordered_set>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
static constexpr int SCE_NP_MATCHING2_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80550C02);
static constexpr int SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED = static_cast<int>(0x80550C03);
static constexpr int SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80550C0A);
static constexpr int SCE_NP_MATCHING2_ERROR_CONTEXT_MAX = static_cast<int>(0x80550C04);
static constexpr int SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_STARTED = static_cast<int>(0x80550C08);
static constexpr int SCE_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID = static_cast<int>(0x80550C0B);
static constexpr int SCE_NET_ERROR_RESOLVER_ETIMEDOUT = static_cast<int>(0x804101E2);
static constexpr int MAX_CONTEXTS = 10;
static constexpr std::uint64_t CREATE_CONTEXT_PARAMETER_SIZE = 0x28;
namespace {
@@ -22,26 +29,56 @@ struct NpMatching2InitializeParameter {
static_assert(offsetof(NpMatching2InitializeParameter, size) == 0x20);
static_assert(sizeof(NpMatching2InitializeParameter) == 0x30);
struct NpMatching2CreateContextParameter {
const void* npId;
const void* npCommunicationId;
const void* npPassphrase;
std::uint32_t serviceLabel;
std::uint64_t size;
};
static_assert(sizeof(NpMatching2CreateContextParameter) == CREATE_CONTEXT_PARAMETER_SIZE);
std::mutex matching2Mutex;
bool matching2Initialized = false;
std::unordered_set<std::uint16_t> contexts;
int nextContextId = 1;
int CheckContext(std::uint16_t ctxId) {
if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED;
return contexts.contains(ctxId) ? 0 : SCE_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
}
extern "C" {
int APS5_VABI sceNpMatching2ContextStart() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceNpMatching2ContextStart(std::uint16_t ctxId, std::uint64_t timeout) {
(void)ctxId;
(void)timeout;
std::lock_guard lock(matching2Mutex);
if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED;
return SCE_NET_ERROR_RESOLVER_ETIMEDOUT;
}
int APS5_VABI sceNpMatching2ContextStop() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceNpMatching2ContextStop(std::uint16_t ctxId) {
std::lock_guard lock(matching2Mutex);
if (const int error = CheckContext(ctxId)) return error;
return SCE_NP_MATCHING2_ERROR_CONTEXT_NOT_STARTED;
}
int APS5_VABI sceNpMatching2CreateContext() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceNpMatching2CreateContext(const NpMatching2CreateContextParameter* param, std::uint16_t* ctxId) {
std::lock_guard lock(matching2Mutex);
if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED;
if (!param || param->size != CREATE_CONTEXT_PARAMETER_SIZE || !ctxId) return SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
for (int tried = 0; tried < MAX_CONTEXTS; ++tried) {
const auto candidate = static_cast<std::uint16_t>((nextContextId - 1 + tried) % MAX_CONTEXTS + 1);
if (contexts.contains(candidate)) continue;
contexts.insert(candidate);
nextContextId = candidate % MAX_CONTEXTS + 1;
*ctxId = candidate;
return 0;
}
return SCE_NP_MATCHING2_ERROR_CONTEXT_MAX;
}
int APS5_VABI sceNpMatching2CreateJoinRoom() {
@@ -49,8 +86,10 @@ int APS5_VABI sceNpMatching2CreateJoinRoom() {
return 0;
}
int APS5_VABI sceNpMatching2DestroyContext() {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceNpMatching2DestroyContext(std::uint16_t ctxId) {
std::lock_guard lock(matching2Mutex);
if (const int error = CheckContext(ctxId)) return error;
contexts.erase(ctxId);
return 0;
}
@@ -107,24 +146,32 @@ int APS5_VABI sceNpMatching2LeaveRoom() {
return 0;
}
int APS5_VABI sceNpMatching2RegisterContextCallback() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceNpMatching2RegisterContextCallback(void* callback, void* userdata) {
(void)callback;
(void)userdata;
std::lock_guard lock(matching2Mutex);
return matching2Initialized ? 0 : SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
int APS5_VABI sceNpMatching2RegisterRoomEventCallback() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceNpMatching2RegisterRoomEventCallback(std::uint16_t ctxId, void* callback, void* userdata) {
(void)callback;
(void)userdata;
std::lock_guard lock(matching2Mutex);
return CheckContext(ctxId);
}
int APS5_VABI sceNpMatching2RegisterRoomMessageCallback() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceNpMatching2RegisterRoomMessageCallback(std::uint16_t ctxId, void* callback, void* userdata) {
(void)callback;
(void)userdata;
std::lock_guard lock(matching2Mutex);
return CheckContext(ctxId);
}
int APS5_VABI sceNpMatching2RegisterSignalingCallback() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceNpMatching2RegisterSignalingCallback(std::uint16_t ctxId, void* callback, void* userdata) {
(void)callback;
(void)userdata;
std::lock_guard lock(matching2Mutex);
return CheckContext(ctxId);
}
int APS5_VABI sceNpMatching2SearchRoom() {
@@ -142,9 +189,11 @@ int APS5_VABI sceNpMatching2SendRoomMessage() {
return 0;
}
int APS5_VABI sceNpMatching2SetDefaultRequestOptParam() {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceNpMatching2SetDefaultRequestOptParam(std::uint16_t ctxId, const void* requestOpt) {
std::lock_guard lock(matching2Mutex);
if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED;
if (!requestOpt) return SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
return CheckContext(ctxId);
}
int APS5_VABI sceNpMatching2SetRoomDataExternal() {
@@ -176,6 +225,8 @@ int APS5_VABI sceNpMatching2Terminate(void) {
std::lock_guard lock(matching2Mutex);
if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED;
matching2Initialized = false;
contexts.clear();
nextContextId = 1;
return 0;
}
+119
View File
@@ -0,0 +1,119 @@
#include "SceTypes.hpp"
#include <cstdint>
#include <cstdio>
struct InitializeParameter {
std::uint64_t poolSize;
std::uint64_t reserved;
std::int32_t priority;
std::uint8_t padding[4];
std::uint64_t stackSize;
std::uint64_t size;
std::uint64_t sslPoolSize;
};
struct CreateContextParameter {
const void* npId;
const void* npCommunicationId;
const void* npPassphrase;
std::uint32_t serviceLabel;
std::uint64_t size;
};
extern "C" {
int APS5_VABI sceNpMatching2Initialize(const InitializeParameter* param);
int APS5_VABI sceNpMatching2Terminate();
int APS5_VABI sceNpMatching2CreateContext(const CreateContextParameter* param, std::uint16_t* ctxId);
int APS5_VABI sceNpMatching2DestroyContext(std::uint16_t ctxId);
int APS5_VABI sceNpMatching2ContextStart(std::uint16_t ctxId, std::uint64_t timeout);
int APS5_VABI sceNpMatching2ContextStop(std::uint16_t ctxId);
int APS5_VABI sceNpMatching2RegisterContextCallback(void* callback, void* userdata);
int APS5_VABI sceNpMatching2RegisterRoomEventCallback(std::uint16_t ctxId, void* callback, void* userdata);
int APS5_VABI sceNpMatching2RegisterRoomMessageCallback(std::uint16_t ctxId, void* callback, void* userdata);
int APS5_VABI sceNpMatching2RegisterSignalingCallback(std::uint16_t ctxId, void* callback, void* userdata);
int APS5_VABI sceNpMatching2SetDefaultRequestOptParam(std::uint16_t ctxId, const void* requestOpt);
}
namespace {
constexpr int ResolverTimedOut = static_cast<int>(0x804101E2u);
constexpr int NotInitialized = static_cast<int>(0x80550C03u);
constexpr int ContextMax = static_cast<int>(0x80550C04u);
constexpr int ContextNotStarted = static_cast<int>(0x80550C08u);
constexpr int InvalidArgument = static_cast<int>(0x80550C0Au);
constexpr int InvalidContextId = static_cast<int>(0x80550C0Bu);
int failures = 0;
void Require(bool value, const char* name, const char* what) {
if (value) return;
std::fprintf(stderr, "%s: %s\n", name, what);
++failures;
}
}
int main() {
InitializeParameter init{};
CreateContextParameter create{};
create.size = 0x28;
unsigned char requestOpt[24] = {};
std::uint16_t context = 77;
Require(sceNpMatching2CreateContext(nullptr, nullptr) == NotInitialized, "sceNpMatching2CreateContext", "a call before initialization did not return NOT_INITIALIZED first");
Require(sceNpMatching2DestroyContext(1) == NotInitialized, "sceNpMatching2DestroyContext", "a call before initialization did not return NOT_INITIALIZED");
Require(sceNpMatching2ContextStart(1, 0) == NotInitialized, "sceNpMatching2ContextStart", "a call before initialization did not return NOT_INITIALIZED");
Require(sceNpMatching2ContextStop(1) == NotInitialized, "sceNpMatching2ContextStop", "a call before initialization did not return NOT_INITIALIZED");
Require(sceNpMatching2RegisterContextCallback(nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterContextCallback", "a call before initialization did not return NOT_INITIALIZED");
Require(sceNpMatching2RegisterRoomEventCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterRoomEventCallback", "a call before initialization did not return NOT_INITIALIZED");
Require(sceNpMatching2RegisterRoomMessageCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterRoomMessageCallback", "a call before initialization did not return NOT_INITIALIZED");
Require(sceNpMatching2RegisterSignalingCallback(1, nullptr, nullptr) == NotInitialized, "sceNpMatching2RegisterSignalingCallback", "a call before initialization did not return NOT_INITIALIZED");
Require(sceNpMatching2SetDefaultRequestOptParam(1, nullptr) == NotInitialized, "sceNpMatching2SetDefaultRequestOptParam", "a call before initialization did not return NOT_INITIALIZED first");
init.size = 0x30;
Require(sceNpMatching2Initialize(&init) == 0, "sceNpMatching2Initialize", "initialization failed");
Require(sceNpMatching2CreateContext(nullptr, &context) == InvalidArgument, "sceNpMatching2CreateContext", "a null parameter was taken");
create.size = 0x30;
Require(sceNpMatching2CreateContext(&create, &context) == InvalidArgument, "sceNpMatching2CreateContext", "parameter size 0x30 was taken");
create.size = 0x28;
Require(sceNpMatching2CreateContext(&create, nullptr) == InvalidArgument, "sceNpMatching2CreateContext", "a null output was taken");
Require(context == 77, "sceNpMatching2CreateContext", "a refused call wrote the context id");
Require(sceNpMatching2CreateContext(&create, &context) == 0 && context == 1, "sceNpMatching2CreateContext", "the first context is not id 1");
Require(sceNpMatching2DestroyContext(1) == 0, "sceNpMatching2DestroyContext", "destroying context 1 failed");
std::uint16_t created = 0;
for (std::uint16_t expected = 2; expected <= 10; ++expected) {
Require(sceNpMatching2CreateContext(&create, &created) == 0 && created == expected, "sceNpMatching2CreateContext", "a context id was reused before the ids up to 10 were handed out");
}
Require(sceNpMatching2CreateContext(&create, &created) == 0 && created == 1, "sceNpMatching2CreateContext", "after id 10 the allocation did not wrap to the free id 1");
created = 77;
Require(sceNpMatching2CreateContext(&create, &created) == ContextMax && created == 77, "sceNpMatching2CreateContext", "an eleventh context did not return CONTEXT_MAX");
Require(sceNpMatching2DestroyContext(11) == InvalidContextId, "sceNpMatching2DestroyContext", "an unknown context was destroyed");
Require(sceNpMatching2ContextStart(1, 0) == ResolverTimedOut, "sceNpMatching2ContextStart", "starting a context did not return the offline resolver timeout");
Require(sceNpMatching2ContextStart(99, 0) == ResolverTimedOut, "sceNpMatching2ContextStart", "the offline answer did not come before the context check");
Require(sceNpMatching2ContextStop(1) == ContextNotStarted, "sceNpMatching2ContextStop", "a context that never started did not return CONTEXT_NOT_STARTED");
Require(sceNpMatching2ContextStop(99) == InvalidContextId, "sceNpMatching2ContextStop", "an unknown context was taken");
Require(sceNpMatching2RegisterContextCallback(nullptr, nullptr) == 0, "sceNpMatching2RegisterContextCallback", "registering the context callback failed");
Require(sceNpMatching2RegisterRoomEventCallback(99, nullptr, nullptr) == InvalidContextId, "sceNpMatching2RegisterRoomEventCallback", "an unknown context was taken");
Require(sceNpMatching2RegisterRoomEventCallback(1, nullptr, nullptr) == 0, "sceNpMatching2RegisterRoomEventCallback", "registering failed");
Require(sceNpMatching2RegisterRoomMessageCallback(99, nullptr, nullptr) == InvalidContextId, "sceNpMatching2RegisterRoomMessageCallback", "an unknown context was taken");
Require(sceNpMatching2RegisterRoomMessageCallback(1, nullptr, nullptr) == 0, "sceNpMatching2RegisterRoomMessageCallback", "registering failed");
Require(sceNpMatching2RegisterSignalingCallback(99, nullptr, nullptr) == InvalidContextId, "sceNpMatching2RegisterSignalingCallback", "an unknown context was taken");
Require(sceNpMatching2RegisterSignalingCallback(1, nullptr, nullptr) == 0, "sceNpMatching2RegisterSignalingCallback", "registering failed");
Require(sceNpMatching2SetDefaultRequestOptParam(99, nullptr) == InvalidArgument, "sceNpMatching2SetDefaultRequestOptParam", "a null option was not refused before the context");
Require(sceNpMatching2SetDefaultRequestOptParam(99, requestOpt) == InvalidContextId, "sceNpMatching2SetDefaultRequestOptParam", "an unknown context was taken");
Require(sceNpMatching2SetDefaultRequestOptParam(1, requestOpt) == 0, "sceNpMatching2SetDefaultRequestOptParam", "setting the default option failed");
Require(sceNpMatching2Terminate() == 0, "sceNpMatching2Terminate", "termination failed");
Require(sceNpMatching2DestroyContext(1) == NotInitialized, "sceNpMatching2DestroyContext", "a context was destroyed after termination");
Require(sceNpMatching2Initialize(&init) == 0, "sceNpMatching2Initialize", "initializing again failed");
Require(sceNpMatching2RegisterRoomEventCallback(5, nullptr, nullptr) == InvalidContextId, "sceNpMatching2Terminate", "a context created before termination is still found");
Require(sceNpMatching2CreateContext(&create, &created) == 0 && created == 1, "sceNpMatching2Terminate", "termination did not restart the context ids at 1");
Require(sceNpMatching2Terminate() == 0, "sceNpMatching2Terminate", "the second termination failed");
if (failures != 0) return 1;
std::puts("NpMatching2 offline tests passed");
return 0;
}
+1
View File
@@ -3,3 +3,4 @@
### Functional
- [sceNpMatching2Initialize, sceNpMatching2Terminate](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - only the library state is kept: initialization checks the parameter's `size` field (0x28, or 0x30 with an SSL pool) as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_matching2/np_matching2.cpp) does and starts no thread; a second initialization returns `ALREADY_INITIALIZED` and a termination without one `NOT_INITIALIZED`. The pool, thread and SSL settings are not used: no matching server is reached. Not measured on a console
- [sceNpMatching2CreateContext and the context calls](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - offline only, not measured on a PS5: the checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_matching2/np_matching2.cpp#L26-L349) and its [context manager](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_matching2/np_matching2_internal.cpp#L1348-L1457) (a `0x28`-byte parameter, ten ids handed out round-robin). `sceNpMatching2ContextStart` returns `0x804101E2` (`SCE_NET_ERROR_RESOLVER_ETIMEDOUT`), which shadPS4 says was confirmed on a console disconnected from the internet, so no context starts and `sceNpMatching2ContextStop` answers `CONTEXT_NOT_STARTED`. Callbacks and the default request option are checked but never used. The room, user, world, server-id and signaling-info calls still throw