Matching2 P3 + P4 (#4643)

* matching2 part 3: context states, initial payload holder, additional constants, enums.  Matchmaking block, comid additon, ctx_id additon to shadnet.proto.

* Matching2 P4: this time its matching: starting rewrites on external APIs.  Shadnet changes to mirror on emu side.

* Matching P5: SearchRoom
This commit is contained in:
metr1k-91 2026-06-29 01:02:46 -05:00 committed by GitHub
parent 8ead142315
commit c581ce89a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1528 additions and 354 deletions

View File

@ -634,6 +634,8 @@ set(NP_LIBS src/core/libraries/np/np_error.h
src/core/libraries/np/np_matching2/np_matching2.h
src/core/libraries/np/np_matching2/np_matching2_internal.cpp
src/core/libraries/np/np_matching2/np_matching2_internal.h
src/core/libraries/np/np_matching2/np_matching2_mm.cpp
src/core/libraries/np/np_matching2/np_matching2_mm.h
src/core/libraries/np/np_matching2/np_matching2_types.h
src/core/libraries/np/np_trophy.cpp
src/core/libraries/np/np_trophy.h

View File

@ -10,6 +10,7 @@
#include "core/emulator_settings.h"
#include "core/libraries/np/np_error.h"
#include "core/libraries/np/np_manager.h"
#include "core/libraries/np/np_matching2/np_matching2_mm.h"
#include "core/libraries/np/np_score/np_score.h"
#include "core/libraries/np/np_web_api/np_web_api.h"
#include "core/user_settings.h"
@ -167,7 +168,7 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons
};
client->onAsyncReply = [this, user_id](ShadNet::CommandType cmd, u64 pkt_id,
ShadNet::ErrorType err, const std::vector<u8>& body) {
OnScoreReply(user_id, cmd, pkt_id, err, body);
OnAsyncReply(user_id, cmd, pkt_id, err, body);
};
client->onLoginResult = [this, user_id](const ShadNet::LoginResult& res) {
OnLoginResult(user_id, res);
@ -194,6 +195,8 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons
LOG_INFO(NpHandler, "user_id={} signed in npid='{}' accountId={}", user_id, npid,
client->GetUserId());
NpMatching2::SetMmShadNetClient(client, host, port);
// Build OrbisNpId
{
OrbisNpId np_id{};
@ -1603,6 +1606,16 @@ static u32 FillRankArrayFromProto(const shadnet::GetScoreResponse& resp,
return found;
}
void NpHandler::OnAsyncReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id,
ShadNet::ErrorType error, const std::vector<u8>& body) {
const auto cmd_val = static_cast<u16>(cmd);
if (cmd_val >= 12 && cmd_val <= 23) {
NpMatching2::OnMatchingReply(cmd, pkt_id, error, body);
} else {
OnScoreReply(user_id, cmd, pkt_id, error, body);
}
}
void NpHandler::OnScoreReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id,
ShadNet::ErrorType error, const std::vector<u8>& body) {
PendingScoreRequest pending;

View File

@ -215,8 +215,11 @@ private:
void OnWebApiPushEvent(s32 user_id, const ShadNet::NotifyWebApiPushEvent& n);
void OnLoginResult(s32 user_id, const ShadNet::LoginResult& res);
// Async reply dispatch for score commands. Called from the per-user
// ShadNetClient on the reader thread.
// General async reply dispatch. Routes a reply to the owning subsystem by
// command. Called from the per-user ShadNetClient on the reader thread.
void OnAsyncReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
const std::vector<u8>& body);
void OnScoreReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
const std::vector<u8>& body);

View File

@ -1,8 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <deque>
#include <mutex>
#include <chrono>
#include "common/logging/log.h"
#include "core/emulator_settings.h"
@ -12,6 +11,7 @@
#include "core/libraries/np/np_manager.h"
#include "core/libraries/np/np_matching2/np_matching2.h"
#include "core/libraries/np/np_matching2/np_matching2_internal.h"
#include "core/libraries/np/np_matching2/np_matching2_mm.h"
#include "core/libraries/np/np_matching2/np_matching2_types.h"
#include "core/libraries/np/np_types.h"
#include "core/libraries/system/userservice.h"
@ -19,24 +19,18 @@
namespace Libraries::Np::NpMatching2 {
static bool g_initialized = false;
static OrbisNpMatching2ContextId contextId = 1;
static std::mutex g_events_mutex;
static std::deque<NpMatching2ContextEvent> g_ctx_events;
static std::deque<NpMatching2LobbyEvent> g_lobby_events;
static std::deque<NpMatching2RoomEvent> g_room_events;
static std::mutex g_responses_mutex;
static std::deque<std::function<void()>> g_responses;
int PS4_SYSV_ABI sceNpMatching2CreateContext(const OrbisNpMatching2CreateContextParameter* param,
OrbisNpMatching2ContextId* ctxId) {
LOG_DEBUG(Lib_NpMatching2, "called, npId = {}, serviceLabel = {}, size = {}",
param->npId->handle.data, param->serviceLabel, param->size);
LOG_INFO(Lib_NpMatching2, "called, npId = {}, serviceLabel = {}, size = {}",
param->npId->handle.data, param->serviceLabel, param->size);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!param || param->size != 0x28 || !ctxId) {
LOG_ERROR(Lib_NpMatching2, "param null, invalid size, or ctxId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
@ -45,13 +39,15 @@ int PS4_SYSV_ABI sceNpMatching2CreateContext(const OrbisNpMatching2CreateContext
int PS4_SYSV_ABI sceNpMatching2CreateContextA(const OrbisNpMatching2CreateContextParameterA* param,
OrbisNpMatching2ContextId* ctxId) {
LOG_DEBUG(Lib_NpMatching2, "called, userId = {}, serviceLabel = {}, size = {}", param->userId,
param->serviceLabel, param->size);
LOG_INFO(Lib_NpMatching2, "called, userId = {}, serviceLabel = {}, size = {}", param->userId,
param->serviceLabel, param->size);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!param || param->size != 0x10 || !ctxId) {
LOG_ERROR(Lib_NpMatching2, "param null, invalid size, or ctxId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
@ -61,219 +57,215 @@ int PS4_SYSV_ABI sceNpMatching2CreateContextA(const OrbisNpMatching2CreateContex
return ContextManager::Instance().CreateContext(&np_id, param->serviceLabel, ctxId);
}
static std::optional<OrbisNpMatching2RequestOptParam> defaultRequestOptParam = std::nullopt;
static void StoreRequestCallback(ContextObject* ctx, OrbisNpMatching2RequestOptParam* requestOpt) {
if (requestOpt && requestOpt->callback) {
ctx->default_request_callback = requestOpt->callback;
ctx->default_request_callback_arg = requestOpt->arg;
}
}
auto GetOptParam(OrbisNpMatching2RequestOptParam* requestOpt) {
return requestOpt ? *requestOpt
: (defaultRequestOptParam ? defaultRequestOptParam
: std::optional<OrbisNpMatching2RequestOptParam>{});
int PS4_SYSV_ABI sceNpMatching2CreateJoinRoom(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2CreateJoinRoomRequest* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}", ctxId);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
LOG_ERROR(Lib_NpMatching2, "request or requestId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
StoreRequestCallback(ctx, requestOpt);
const OrbisNpMatching2RequestId reqId = AllocRequestId();
*requestId = reqId;
MmCreateJoinRoom(ctxId, reqId, *request);
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2JoinRoom(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2JoinRoomRequest* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}", ctxId);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
LOG_ERROR(Lib_NpMatching2, "request or requestId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
StoreRequestCallback(ctx, requestOpt);
const OrbisNpMatching2RequestId reqId = AllocRequestId();
*requestId = reqId;
MmJoinRoom(ctxId, reqId, *request);
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2CreateJoinRoomA(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2CreateJoinRoomRequestA* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
LOG_ERROR(Lib_NpMatching2, "request or requestId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
LOG_DEBUG(Lib_NpMatching2,
"maxSlot = {}, teamId = {}, worldId = {}, lobbyId = {}, groupConfig = {}, "
"joinGroupLabel = {}",
request->maxSlot, request->teamId, request->worldId, request->lobbyId,
request->groupConfig, request->joinGroupLabel);
static OrbisNpMatching2RequestId id = 10;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
auto requestCopy = *request;
g_responses.emplace_back([=]() {
Libraries::Np::OrbisNpOnlineId onlineId{};
if (NpManager::sceNpGetOnlineId(1, &onlineId) != ORBIS_OK) {
return;
}
OrbisNpMatching2RoomMemberDataInternalA me{
nullptr,
0,
{0xace104e, Libraries::Np::OrbisNpPlatformType::PS4},
onlineId,
{0, 0, 0, 0},
1,
requestCopy.teamId,
1,
0,
nullptr,
nullptr,
0};
OrbisNpMatching2RoomDataInternal room{requestCopy.maxSlot,
0,
static_cast<u16>(requestCopy.maxSlot - 1u),
0,
15,
0xac,
requestCopy.worldId,
requestCopy.lobbyId,
0x10,
0,
0,
nullptr,
0,
0,
{0, 0, 0, 0},
nullptr,
0};
OrbisNpMatching2CreateJoinRoomResponseA resp{&room, {&me, 1, &me, &me}};
optParam->callback(ctxId, reqIdCopy,
ORBIS_NP_MATCHING2_REQUEST_EVENT_CREATE_JOIN_ROOM_A, 0, &resp,
optParam->arg);
});
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
StoreRequestCallback(ctx, requestOpt);
const OrbisNpMatching2RequestId reqId = AllocRequestId();
*requestId = reqId;
LOG_WARNING(Lib_NpMatching2, "not implemented");
return ORBIS_OK;
}
std::function<void(const NpMatching2ContextEvent*)> npMatching2ContextCallback = nullptr;
int PS4_SYSV_ABI sceNpMatching2RegisterContextCallback(OrbisNpMatching2ContextCallback callback,
void* userdata) {
LOG_DEBUG(Lib_NpMatching2, "called, userdata = {}", userdata);
LOG_INFO(Lib_NpMatching2, "called, userdata = {}", userdata);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
npMatching2ContextCallback = [callback, userdata](auto arg) {
callback(arg->contextId, arg->event, arg->cause, arg->errorCode, userdata);
};
ContextManager::Instance().ApplyContextCallback(callback, userdata);
return ORBIS_OK;
}
std::function<void(const NpMatching2LobbyEvent*)> npMatching2LobbyCallback = nullptr;
int PS4_SYSV_ABI sceNpMatching2RegisterLobbyEventCallback(
OrbisNpMatching2ContextId ctxId, OrbisNpMatching2LobbyEventCallback callback, void* userdata) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata);
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
npMatching2LobbyCallback = [callback, userdata](auto arg) {
callback(arg->contextId, arg->lobbyId, arg->event, arg->data, userdata);
};
ctx->lobby_event_callback = callback;
ctx->lobby_event_callback_arg = userdata;
return ORBIS_OK;
}
std::function<void(const NpMatching2RoomEvent*)> npMatching2RoomCallback = nullptr;
int PS4_SYSV_ABI sceNpMatching2RegisterRoomEventCallback(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2RoomEventCallback callback,
void* userdata) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata);
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
npMatching2RoomCallback = [callback, userdata](auto arg) {
callback(arg->contextId, arg->roomId, arg->event, arg->data, userdata);
};
ctx->room_event_callback = callback;
ctx->room_event_callback_arg = userdata;
return ORBIS_OK;
}
std::function<void(const OrbisNpMatching2SignalingEvent*)> npMatching2SignalingCallback = nullptr;
int PS4_SYSV_ABI sceNpMatching2RegisterSignalingCallback(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2SignalingCallback callback,
void* userdata) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata);
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
npMatching2SignalingCallback = [callback, userdata](auto arg) {
callback(arg->contextId, arg->roomId, arg->roomMemberId, arg->event, arg->errorCode,
userdata);
};
ctx->signaling_callback = callback;
ctx->signaling_callback_arg = userdata;
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2ContextStart(OrbisNpMatching2ContextId ctxId, u64 timeout) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, timeout = {}", ctxId, timeout);
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, timeout = {}", ctxId, timeout);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
std::scoped_lock lk{g_events_mutex};
if (EmulatorSettings.IsConnectedToNetwork() && EmulatorSettings.IsShadNetEnabled()) {
g_ctx_events.emplace_back(ctxId, ORBIS_NP_MATCHING2_CONTEXT_EVENT_STARTED,
ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ACTION, 0);
} else {
if (!EmulatorSettings.IsConnectedToNetwork() || !EmulatorSettings.IsShadNetEnabled()) {
// error confirmed with a real console disconnected from the internet
constexpr int ORBIS_NET_ERROR_RESOLVER_ETIMEDOUT = 0x804101e2;
g_ctx_events.emplace_back(ctxId, ORBIS_NP_MATCHING2_CONTEXT_EVENT_START_OVER,
ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ERROR,
ORBIS_NET_ERROR_RESOLVER_ETIMEDOUT);
LOG_ERROR(Lib_NpMatching2, "not connected to network");
return ORBIS_NET_ERROR_RESOLVER_ETIMEDOUT;
}
const s32 rc = ContextManager::Instance().Start(ctxId);
if (rc != ORBIS_OK) {
return rc;
}
MmContextStart(ctxId);
return ORBIS_OK;
}
void ProcessEvents() {
{
std::scoped_lock lk{g_events_mutex};
int PS4_SYSV_ABI sceNpMatching2ContextStop(OrbisNpMatching2ContextId ctxId) {
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}", ctxId);
if (npMatching2ContextCallback) {
while (!g_ctx_events.empty()) {
npMatching2ContextCallback(&g_ctx_events.front());
g_ctx_events.pop_front();
}
}
if (npMatching2LobbyCallback) {
while (!g_lobby_events.empty()) {
npMatching2LobbyCallback(&g_lobby_events.front());
g_lobby_events.pop_front();
}
}
if (npMatching2RoomCallback) {
while (!g_room_events.empty()) {
npMatching2RoomCallback(&g_room_events.front());
g_room_events.pop_front();
}
}
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
std::scoped_lock lk{g_responses_mutex};
while (!g_responses.empty()) {
(g_responses.front())();
g_responses.pop_front();
const s32 rc = ContextManager::Instance().Stop(ctxId);
if (rc != ORBIS_OK) {
return rc;
}
MmContextStop(ctxId);
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2Initialize(OrbisNpMatching2InitializeParameter* param) {
LOG_DEBUG(Lib_NpMatching2, "called");
LOG_INFO(Lib_NpMatching2, "called");
if (g_initialized) {
LOG_ERROR(Lib_NpMatching2, "already initialized");
return ORBIS_NP_MATCHING2_ERROR_ALREADY_INITIALIZED;
}
if (!param || (param->size != 0x30 && param->size != 0x28)) {
LOG_ERROR(Lib_NpMatching2, "param null or invalid size");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
@ -287,21 +279,20 @@ int PS4_SYSV_ABI sceNpMatching2Initialize(OrbisNpMatching2InitializeParameter* p
g_initialized = true;
g_state.initialized.store(true);
Libraries::Np::NpManager::RegisterNpCallback("NpMatching2", ProcessEvents);
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2Terminate() {
LOG_DEBUG(Lib_NpMatching2, "called");
LOG_INFO(Lib_NpMatching2, "called");
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
g_initialized = false;
g_state.initialized.store(false);
Libraries::Np::NpManager::DeregisterNpCallback("NpMatching2");
TermEventDispatcher();
ContextManager::Instance().Reset();
@ -311,32 +302,42 @@ int PS4_SYSV_ABI sceNpMatching2Terminate() {
int PS4_SYSV_ABI sceNpMatching2SetDefaultRequestOptParam(
OrbisNpMatching2ContextId ctxId, OrbisNpMatching2RequestOptParam* requestOpt) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}", ctxId);
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}", ctxId);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!requestOpt) {
LOG_ERROR(Lib_NpMatching2, "requestOpt null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
defaultRequestOptParam = *requestOpt;
ctx->default_request_callback = requestOpt->callback;
ctx->default_request_callback_arg = requestOpt->arg;
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2GetServerId(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2ServerId* serverId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}", ctxId);
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}", ctxId);
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!serverId) {
LOG_ERROR(Lib_NpMatching2, "serverId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
*serverId = 0xac;
*serverId = 1;
return ORBIS_OK;
}
@ -345,33 +346,28 @@ int PS4_SYSV_ABI sceNpMatching2GetWorldInfoList(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2GetWorldInfoListRequest* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, request.serverId = {}, requestOpt = {}", ctxId,
request ? request->serverId : 0xFFFF, fmt::ptr(requestOpt));
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, request.serverId = {}, requestOpt = {}", ctxId,
request ? request->serverId : 0xFFFF, fmt::ptr(requestOpt));
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
LOG_ERROR(Lib_NpMatching2, "request or requestId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 1;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
auto reqIdCopy = *requestId;
std::scoped_lock lk{g_responses_mutex};
g_responses.emplace_back([=]() {
OrbisNpMatching2World w{nullptr, 1, 10, 0, 10, 0, {}};
OrbisNpMatching2GetWorldInfoListResponse resp{&w, 1};
optParam->callback(ctxId, reqIdCopy,
ORBIS_NP_MATCHING2_REQUEST_EVENT_GET_WORLD_INFO_LIST, 0, &resp,
optParam->arg);
});
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
StoreRequestCallback(ctx, requestOpt);
const OrbisNpMatching2RequestId reqId = AllocRequestId();
*requestId = reqId;
MmGetWorldInfoList(ctxId, reqId, *request);
return ORBIS_OK;
}
@ -379,29 +375,26 @@ int PS4_SYSV_ABI sceNpMatching2LeaveRoom(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2LeaveRoomRequest* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
LOG_ERROR(Lib_NpMatching2, "request or requestId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 500;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
g_responses.emplace_back([=]() {
optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_LEAVE_ROOM, 0,
nullptr, optParam->arg);
});
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
StoreRequestCallback(ctx, requestOpt);
const OrbisNpMatching2RequestId reqId = AllocRequestId();
*requestId = reqId;
MmLeaveRoom(ctxId, reqId, *request);
return ORBIS_OK;
}
@ -409,31 +402,27 @@ int PS4_SYSV_ABI sceNpMatching2SearchRoom(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2SearchRoomRequest* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
LOG_ERROR(Lib_NpMatching2, "request or requestId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 1;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
auto requestCopy = *request;
g_responses.emplace_back([=]() {
OrbisNpMatching2SearchRoomResponseA resp{{0, 0, 0, {}}, nullptr};
optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_SEARCH_ROOM_A, 0,
&resp, optParam->arg);
});
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
StoreRequestCallback(ctx, requestOpt);
const OrbisNpMatching2RequestId reqId = AllocRequestId();
*requestId = reqId;
MmSearchRoom(ctxId, reqId, *request);
return ORBIS_OK;
}
@ -441,118 +430,104 @@ int PS4_SYSV_ABI sceNpMatching2SetUserInfo(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2SetUserInfoRequest* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
LOG_ERROR(Lib_NpMatching2, "request or requestId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 100;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
g_responses.emplace_back([=]() {
optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_USER_INFO, 0,
nullptr, optParam->arg);
});
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
StoreRequestCallback(ctx, requestOpt);
*requestId = AllocRequestId();
LOG_WARNING(Lib_NpMatching2, "not implemented");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2SendRoomMessage(OrbisNpMatching2ContextId ctxId, void* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
LOG_ERROR(Lib_NpMatching2, "request or requestId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 1000;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
g_responses.emplace_back([=]() {
optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_SEND_ROOM_MESSAGE,
0, nullptr, optParam->arg);
});
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
StoreRequestCallback(ctx, requestOpt);
*requestId = AllocRequestId();
LOG_WARNING(Lib_NpMatching2, "not implemented");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2SetRoomDataExternal(OrbisNpMatching2ContextId ctxId, void* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
LOG_ERROR(Lib_NpMatching2, "request or requestId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 800;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
g_responses.emplace_back([=]() {
optParam->callback(ctxId, reqIdCopy,
ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_ROOM_DATA_EXTERNAL, 0, nullptr,
optParam->arg);
});
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
StoreRequestCallback(ctx, requestOpt);
*requestId = AllocRequestId();
LOG_WARNING(Lib_NpMatching2, "not implemented");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2SetRoomDataInternal(OrbisNpMatching2ContextId ctxId, void* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
LOG_INFO(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
LOG_ERROR(Lib_NpMatching2, "not initialized");
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
LOG_ERROR(Lib_NpMatching2, "request or requestId null");
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 200;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
g_responses.emplace_back([=]() {
optParam->callback(ctxId, reqIdCopy,
ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_ROOM_DATA_INTERNAL, 0, nullptr,
optParam->arg);
});
ContextObject* ctx = ContextManager::Instance().Get(ctxId);
if (!ctx) {
LOG_ERROR(Lib_NpMatching2, "invalid context id");
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
StoreRequestCallback(ctx, requestOpt);
*requestId = AllocRequestId();
LOG_WARNING(Lib_NpMatching2, "not implemented");
return ORBIS_OK;
}
@ -567,6 +542,10 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
sceNpMatching2CreateContextA);
LIB_FUNCTION("V6KSpKv9XJE", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2CreateJoinRoomA);
LIB_FUNCTION("zCWZmXXN600", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2CreateJoinRoom);
LIB_FUNCTION("CSIMDsVjs-g", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2JoinRoom);
LIB_FUNCTION("fQQfP87I7hs", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2RegisterContextCallback);
LIB_FUNCTION("4Nj7u5B5yCA", "libSceNpMatching2", 1, "libSceNpMatching2",
@ -577,6 +556,8 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
sceNpMatching2RegisterSignalingCallback);
LIB_FUNCTION("7vjNQ6Z1op0", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2ContextStart);
LIB_FUNCTION("-f6M4caNe8k", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2ContextStop);
LIB_FUNCTION("LhCPctIICxQ", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2GetServerId);
LIB_FUNCTION("rJNPJqDCpiI", "libSceNpMatching2", 1, "libSceNpMatching2",

View File

@ -85,6 +85,44 @@ enum OrbisNpMatching2SignalingType : u8 {
ORBIS_NP_MATCHING2_SIGNALING_TYPE_STAR = 2,
};
enum OrbisNpMatching2AttrId : OrbisNpMatching2AttributeId {
ORBIS_NP_MATCHING2_ROOM_SEARCHABLE_INT_ATTR_EXTERNAL_1_ID = 0x004C,
ORBIS_NP_MATCHING2_ROOM_SEARCHABLE_INT_ATTR_EXTERNAL_2_ID = 0x004D,
ORBIS_NP_MATCHING2_ROOM_SEARCHABLE_INT_ATTR_EXTERNAL_3_ID = 0x004E,
ORBIS_NP_MATCHING2_ROOM_SEARCHABLE_INT_ATTR_EXTERNAL_4_ID = 0x004F,
ORBIS_NP_MATCHING2_ROOM_SEARCHABLE_INT_ATTR_EXTERNAL_5_ID = 0x0050,
ORBIS_NP_MATCHING2_ROOM_SEARCHABLE_INT_ATTR_EXTERNAL_6_ID = 0x0051,
ORBIS_NP_MATCHING2_ROOM_SEARCHABLE_INT_ATTR_EXTERNAL_7_ID = 0x0052,
ORBIS_NP_MATCHING2_ROOM_SEARCHABLE_INT_ATTR_EXTERNAL_8_ID = 0x0053,
ORBIS_NP_MATCHING2_ROOM_SEARCHABLE_BIN_ATTR_EXTERNAL_1_ID = 0x0054,
ORBIS_NP_MATCHING2_ROOM_BIN_ATTR_EXTERNAL_1_ID = 0x0055,
ORBIS_NP_MATCHING2_ROOM_BIN_ATTR_EXTERNAL_2_ID = 0x0056,
ORBIS_NP_MATCHING2_ROOM_BIN_ATTR_INTERNAL_1_ID = 0x0057,
ORBIS_NP_MATCHING2_ROOM_BIN_ATTR_INTERNAL_2_ID = 0x0058,
ORBIS_NP_MATCHING2_ROOMMEMBER_BIN_ATTR_INTERNAL_1_ID = 0x0059,
};
enum OrbisNpMatching2RoomFlagAttr : OrbisNpMatching2Flags {
ORBIS_NP_MATCHING2_ROOM_FLAG_ATTR_OWNER_AUTO_GRANT = 0x80000000,
ORBIS_NP_MATCHING2_ROOM_FLAG_ATTR_CLOSED = 0x40000000,
ORBIS_NP_MATCHING2_ROOM_FLAG_ATTR_FULL = 0x20000000,
ORBIS_NP_MATCHING2_ROOM_FLAG_ATTR_HIDDEN = 0x10000000,
ORBIS_NP_MATCHING2_ROOM_FLAG_ATTR_NAT_TYPE_RESTRICTION = 0x04000000,
ORBIS_NP_MATCHING2_ROOM_FLAG_ATTR_PROHIBITIVE_MODE = 0x02000000,
};
enum OrbisNpMatching2RoomMemberFlagAttr : OrbisNpMatching2Flags {
ORBIS_NP_MATCHING2_ROOMMEMBER_FLAG_ATTR_OWNER = 0x80000000,
};
enum OrbisNpMatching2Role : u8 {
ORBIS_NP_MATCHING2_ROLE_MEMBER = 1,
ORBIS_NP_MATCHING2_ROLE_OWNER = 2,
};
constexpr u32 ORBIS_NP_MATCHING2_SESSION_PASSWORD_SIZE = 8;
constexpr u32 ORBIS_NP_MATCHING2_GROUP_LABEL_SIZE = 8;
enum OrbisNpMatching2InitDefault : u64 {
ORBIS_NP_MATCHING2_THREAD_STACK_SIZE_DEFAULT = 32768,
ORBIS_NP_MATCHING2_POOLSIZE_DEFAULT = 131072,

View File

@ -1,17 +1,268 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstring>
#include "common/logging/log.h"
#include "common/thread.h"
#include "core/libraries/np/np_common.h"
#include "core/libraries/np/np_error.h"
#include "core/libraries/np/np_matching2/np_matching2_internal.h"
#include "shadnet.pb.h"
namespace Libraries::Np::NpMatching2 {
NpMatching2State g_state;
void* BuildCreateJoinRoomPayload(ContextObject& ctx, const shadnet::CreateJoinRoomResponse& resp) {
CallbackPayload& p = ctx.request_payload;
p.Reset();
const auto& rd = resp.room_data();
p.room_groups.resize(rd.groups_size());
for (int i = 0; i < rd.groups_size(); ++i) {
const auto& g = rd.groups(i);
auto& dst = p.room_groups[i];
dst.id = static_cast<OrbisNpMatching2RoomGroupId>(g.group_id());
dst.hasPasswd = g.has_passwd();
dst.hasLabel = g.has_label();
std::memset(dst.label.data, 0, sizeof(dst.label.data));
std::memcpy(dst.label.data, g.label().data(),
std::min(g.label().size(), sizeof(dst.label.data)));
dst.slots = g.slot_count();
dst.groupMembers = g.num_members();
}
p.room_bin_attrs.resize(rd.bin_attrs_internal_size());
for (int i = 0; i < rd.bin_attrs_internal_size(); ++i) {
const auto& a = rd.bin_attrs_internal(i);
p.bin_buffers.emplace_back(a.data().begin(), a.data().end());
auto& buf = p.bin_buffers.back();
auto& dst = p.room_bin_attrs[i];
dst = {};
dst.binAttr.id = static_cast<OrbisNpMatching2AttributeId>(a.attr_id());
dst.binAttr.data = buf.empty() ? nullptr : buf.data();
dst.binAttr.dataSize = buf.size();
}
p.room_data = std::make_unique<OrbisNpMatching2RoomDataInternal>();
auto& room = *p.room_data;
room = {};
room.publicSlots = static_cast<u16>(rd.public_slots());
room.privateSlots = static_cast<u16>(rd.private_slots());
room.openPublicSlots = static_cast<u16>(rd.open_public_slots());
room.openPrivateSlots = static_cast<u16>(rd.open_private_slots());
room.maxSlot = static_cast<u16>(rd.max_slot());
room.serverId = static_cast<OrbisNpMatching2ServerId>(rd.server_id());
room.worldId = rd.world_id();
room.lobbyId = rd.lobby_id();
room.roomId = rd.room_id();
room.passwdSlotMask = rd.passwd_slot_mask();
room.joinedSlotMask = rd.joined_slot_mask();
room.flags = rd.flags();
room.roomGroup = p.room_groups.empty() ? nullptr : p.room_groups.data();
room.roomGroups = p.room_groups.size();
room.internalBinAttr = p.room_bin_attrs.empty() ? nullptr : p.room_bin_attrs.data();
room.internalBinAttrs = p.room_bin_attrs.size();
const int member_count = resp.members_size();
p.member_data.resize(member_count);
p.member_bin_attrs.clear();
for (int i = 0; i < member_count; ++i) {
const auto& m = resp.members(i);
auto& dst = p.member_data[i];
dst = {};
dst.next = (i + 1 < member_count) ? &p.member_data[i + 1] : nullptr;
std::strncpy(dst.npId.handle.data, m.npid().c_str(), sizeof(dst.npId.handle.data) - 1);
dst.memberId = static_cast<OrbisNpMatching2RoomMemberId>(m.member_id());
dst.teamId = static_cast<OrbisNpMatching2TeamId>(m.team_id());
if (m.is_owner()) {
dst.flagAttr |= ORBIS_NP_MATCHING2_ROOMMEMBER_FLAG_ATTR_OWNER;
}
}
p.create_join_response = std::make_unique<OrbisNpMatching2CreateJoinRoomResponse>();
auto& out = *p.create_join_response;
out.roomData = p.room_data.get();
out.members.members = p.member_data.empty() ? nullptr : p.member_data.data();
out.members.membersNum = p.member_data.size();
out.members.me = nullptr;
out.members.owner = nullptr;
for (auto& md : p.member_data) {
if (md.memberId == static_cast<OrbisNpMatching2RoomMemberId>(resp.me_member_id())) {
out.members.me = &md;
}
if (md.memberId == static_cast<OrbisNpMatching2RoomMemberId>(resp.owner_member_id())) {
out.members.owner = &md;
}
}
RoomCache& rc = ctx.room_cache[room.roomId];
rc = RoomCache{};
rc.num_slots = room.maxSlot;
rc.mask_password = room.passwdSlotMask;
rc.owner = resp.me_member_id() == resp.owner_member_id();
for (const auto& g : p.room_groups) {
rc.groups[g.id] = g;
}
for (int i = 0; i < member_count; ++i) {
const auto& m = resp.members(i);
MemberCache& mc = rc.members[static_cast<OrbisNpMatching2RoomMemberId>(m.member_id())];
mc = MemberCache{};
mc.member_id = static_cast<OrbisNpMatching2RoomMemberId>(m.member_id());
mc.team_id = static_cast<OrbisNpMatching2TeamId>(m.team_id());
mc.nat_type = static_cast<OrbisNpMatching2NatType>(m.nat_type());
mc.flag_attr = m.flag_attr();
mc.group_id = static_cast<OrbisNpMatching2RoomGroupId>(m.group_id());
mc.join_date = m.join_date();
mc.addr = 0;
mc.port = 0;
std::strncpy(mc.np_id.handle.data, m.npid().c_str(), sizeof(mc.np_id.handle.data) - 1);
for (const auto& a : m.bin_attrs_internal()) {
MemberBinCache& b = mc.bins[a.attr_id()];
b.id = static_cast<OrbisNpMatching2AttributeId>(a.attr_id());
b.data.assign(a.data().begin(), a.data().end());
}
}
p.request_data = p.create_join_response.get();
return p.request_data;
}
void* BuildGetWorldInfoListPayload(ContextObject& ctx, const shadnet::GetWorldInfoListReply& resp) {
CallbackPayload& p = ctx.request_payload;
p.Reset();
p.world_list.resize(resp.worlds_size());
for (int i = 0; i < resp.worlds_size(); ++i) {
const auto& w = resp.worlds(i);
auto& dst = p.world_list[i];
dst = OrbisNpMatching2World{};
dst.worldId = static_cast<OrbisNpMatching2WorldId>(w.world_id());
dst.lobbiesNum = w.lobbies_num();
dst.maxLobbyMembersNum = w.max_lobby_members();
dst.lobbyMembersNum = w.lobby_members_num();
dst.roomsNum = w.rooms_num();
dst.roomMembersNum = w.room_members_num();
}
for (size_t i = 0; i + 1 < p.world_list.size(); ++i) {
p.world_list[i].next = &p.world_list[i + 1];
}
p.world_info_response = std::make_unique<OrbisNpMatching2GetWorldInfoListResponse>();
p.world_info_response->world = p.world_list.empty() ? nullptr : p.world_list.data();
p.world_info_response->worldNum = p.world_list.size();
p.request_data = p.world_info_response.get();
return p.request_data;
}
void* BuildSearchRoomPayload(ContextObject& ctx, const shadnet::SearchRoomReply& resp) {
CallbackPayload& p = ctx.request_payload;
p.Reset();
const int room_count = resp.rooms_size();
p.room_data_external.resize(room_count);
for (int i = 0; i < room_count; ++i) {
const auto& r = resp.rooms(i);
auto& dst = p.room_data_external[i];
dst = OrbisNpMatching2RoomDataExternal{};
dst.next = (i + 1 < room_count) ? &p.room_data_external[i + 1] : nullptr;
dst.maxSlot = static_cast<u16>(r.max_slot());
dst.curMembers = static_cast<u16>(r.cur_members());
dst.flags = r.flags();
dst.serverId = static_cast<OrbisNpMatching2ServerId>(r.server_id());
dst.worldId = static_cast<OrbisNpMatching2WorldId>(r.world_id());
dst.lobbyId = r.lobby_id();
dst.roomId = r.room_id();
dst.passwdSlotMask = r.passwd_slot_mask();
dst.joinedSlotMask = r.joined_slot_mask();
dst.publicSlots = static_cast<u16>(r.public_slots());
dst.privateSlots = static_cast<u16>(r.private_slots());
dst.openPublicSlots = static_cast<u16>(r.open_public_slots());
dst.openPrivateSlots = static_cast<u16>(r.open_private_slots());
if (!r.owner_npid().empty()) {
auto& npid = p.ext_owner_npids.emplace_back();
npid = Libraries::Np::OrbisNpId{};
std::strncpy(npid.handle.data, r.owner_npid().c_str(), sizeof(npid.handle.data) - 1);
dst.ownerNpId = &npid;
}
if (r.groups_size() > 0) {
OrbisNpMatching2RoomGroupInfo* first = nullptr;
for (int g = 0; g < r.groups_size(); ++g) {
const auto& src = r.groups(g);
auto& gi = p.ext_room_groups.emplace_back();
gi = OrbisNpMatching2RoomGroupInfo{};
gi.id = static_cast<OrbisNpMatching2RoomGroupId>(src.group_id());
gi.hasPasswd = src.has_passwd();
gi.slots = src.slot_count();
gi.groupMembers = src.num_members();
if (!first) {
first = &gi;
}
}
dst.roomGroup = first;
dst.roomGroups = static_cast<u64>(r.groups_size());
}
if (r.external_search_int_attrs_size() > 0) {
OrbisNpMatching2IntAttr* first = nullptr;
for (int a = 0; a < r.external_search_int_attrs_size(); ++a) {
const auto& src = r.external_search_int_attrs(a);
auto& ia = p.ext_int_attrs.emplace_back();
ia = OrbisNpMatching2IntAttr{};
ia.id = static_cast<OrbisNpMatching2AttributeId>(src.attr_id());
ia.num = src.attr_value();
if (!first) {
first = &ia;
}
}
dst.externalSearchIntAttr = first;
dst.externalSearchIntAttrs = static_cast<u64>(r.external_search_int_attrs_size());
}
auto build_bin = [&](const auto& src_attrs, OrbisNpMatching2BinAttr*& out_ptr,
u64& out_num) {
if (src_attrs.empty()) {
return;
}
OrbisNpMatching2BinAttr* first = nullptr;
for (const auto& src : src_attrs) {
p.bin_buffers.emplace_back(src.data().begin(), src.data().end());
auto& buf = p.bin_buffers.back();
auto& ba = p.ext_bin_attrs.emplace_back();
ba = OrbisNpMatching2BinAttr{};
ba.id = static_cast<OrbisNpMatching2AttributeId>(src.attr_id());
ba.data = buf.empty() ? nullptr : buf.data();
ba.dataSize = buf.size();
if (!first) {
first = &ba;
}
}
out_ptr = first;
out_num = static_cast<u64>(src_attrs.size());
};
build_bin(r.external_search_bin_attrs(), dst.externalSearchBinAttr,
dst.externalSearchBinAttrs);
build_bin(r.external_bin_attrs(), dst.externalBinAttr, dst.externalBinAttrs);
}
p.search_room_response = std::make_unique<OrbisNpMatching2SearchRoomResponse>();
auto& out = *p.search_room_response;
out = OrbisNpMatching2SearchRoomResponse{};
out.range.start = resp.range_start();
out.range.total = resp.range_total();
out.range.results = resp.range_result();
out.roomDataExt = p.room_data_external.empty() ? nullptr : p.room_data_external.data();
p.request_data = p.search_room_response.get();
return p.request_data;
}
ContextManager& ContextManager::Instance() {
static ContextManager instance;
return instance;
@ -52,18 +303,15 @@ s32 ContextManager::CreateContext(const OrbisNpId* owner_np_id, OrbisNpServiceLa
ctx.service_label = service_label;
if (owner_np_id) {
ctx.owner_np_id = *owner_np_id;
ctx.online_id =
std::string(owner_np_id->handle.data,
strnlen(owner_np_id->handle.data, sizeof(owner_np_id->handle.data)));
}
if (ctx.online_id.empty()) {
ctx.online_id = "shadPS4_player";
ctx.online_id = owner_np_id->handle;
}
ctx.context_callback = m_pending_context_callback;
ctx.context_callback_arg = m_pending_context_callback_arg;
m_used[id] = true;
*out_ctx_id = id;
LOG_INFO(Lib_NpMatching2, "context created: id={} online_id={} serviceLabel={:#x}", id,
ctx.online_id, service_label);
LOG_DEBUG(Lib_NpMatching2, "context created: id={} online_id={} serviceLabel={:#x}", id,
ctx.online_id.data, service_label);
return ORBIS_OK;
}
@ -84,7 +332,7 @@ bool ContextManager::Destroy(OrbisNpMatching2ContextId ctx_id) {
}
m_contexts[ctx_id] = ContextObject{};
m_used[ctx_id] = false;
LOG_INFO(Lib_NpMatching2, "context destroyed: id={}", ctx_id);
LOG_DEBUG(Lib_NpMatching2, "context destroyed: id={}", ctx_id);
return true;
}
@ -98,7 +346,7 @@ s32 ContextManager::Start(OrbisNpMatching2ContextId ctx_id) {
return ORBIS_NP_MATCHING2_ERROR_CONTEXT_ALREADY_STARTED;
}
ctx->started = true;
LOG_INFO(Lib_NpMatching2, "context started: id={}", ctx_id);
LOG_DEBUG(Lib_NpMatching2, "context started: id={}", ctx_id);
return ORBIS_OK;
}
@ -112,15 +360,29 @@ s32 ContextManager::Stop(OrbisNpMatching2ContextId ctx_id) {
return ORBIS_NP_MATCHING2_ERROR_CONTEXT_NOT_STARTED;
}
ctx->started = false;
LOG_INFO(Lib_NpMatching2, "context stopped: id={}", ctx_id);
LOG_DEBUG(Lib_NpMatching2, "context stopped: id={}", ctx_id);
return ORBIS_OK;
}
void ContextManager::ApplyContextCallback(OrbisNpMatching2ContextCallback callback, void* arg) {
std::lock_guard lock(m_mutex);
m_pending_context_callback = callback;
m_pending_context_callback_arg = arg;
for (u32 id = 1; id <= kMaxContexts; ++id) {
if (m_used[id]) {
m_contexts[id].context_callback = callback;
m_contexts[id].context_callback_arg = arg;
}
}
}
void ContextManager::Reset() {
std::lock_guard lock(m_mutex);
m_contexts = {};
m_used = {};
m_next_id = 1;
m_pending_context_callback = nullptr;
m_pending_context_callback_arg = nullptr;
}
OrbisNpMatching2RequestId AllocRequestId() {
@ -137,49 +399,99 @@ namespace {
void FireEvent(const PendingEvent& ev) {
ContextObject* ctx = ContextManager::Instance().Get(ev.ctx_id);
if (!ctx) {
LOG_WARNING(Lib_NpMatching2, "FireEvent type={} dropped: ctx={} not found",
static_cast<int>(ev.type), ev.ctx_id);
return;
}
switch (ev.type) {
case PendingEvent::CONTEXT_CB:
if (ctx->context_callback) {
LOG_DEBUG(Lib_NpMatching2, "callback CONTEXT ctx={} event={:#x} cause={} err={:#x}",
ev.ctx_id, static_cast<u16>(ev.ctx_event),
static_cast<u8>(ev.ctx_event_cause), ev.error_code);
ctx->context_callback(ev.ctx_id, ev.ctx_event, ev.ctx_event_cause, ev.error_code,
ctx->context_callback_arg);
} else {
LOG_WARNING(Lib_NpMatching2, "callback CONTEXT ctx={} event={:#x} SKIPPED: no callback",
ev.ctx_id, static_cast<u16>(ev.ctx_event));
}
break;
case PendingEvent::REQUEST_CB:
if (ev.request_cb) {
LOG_DEBUG(Lib_NpMatching2,
"callback REQUEST ctx={} reqId={} event={:#x} err={:#x} data={}", ev.ctx_id,
ev.req_id, static_cast<u16>(ev.req_event), ev.error_code,
fmt::ptr(ev.request_data));
ev.request_cb(ev.ctx_id, ev.req_id, ev.req_event, ev.error_code, ev.request_data,
ev.request_cb_arg);
} else {
LOG_WARNING(Lib_NpMatching2,
"callback REQUEST ctx={} reqId={} event={:#x} SKIPPED: no callback",
ev.ctx_id, ev.req_id, static_cast<u16>(ev.req_event));
}
break;
case PendingEvent::SIGNALING_CB:
if (ctx->signaling_callback) {
LOG_DEBUG(Lib_NpMatching2,
"callback SIGNALING ctx={} room={} member={} event={:#x} err={:#x}",
ev.ctx_id, ev.room_id, ev.member_id, static_cast<u16>(ev.sig_event),
ev.error_code);
ctx->signaling_callback(ev.ctx_id, ev.room_id, ev.member_id, ev.sig_event,
ev.error_code, ctx->signaling_callback_arg);
} else {
LOG_WARNING(Lib_NpMatching2,
"callback SIGNALING ctx={} room={} event={:#x} SKIPPED: no callback",
ev.ctx_id, ev.room_id, static_cast<u16>(ev.sig_event));
}
break;
case PendingEvent::ROOM_EVENT_CB:
if (ctx->room_event_callback) {
LOG_DEBUG(Lib_NpMatching2, "callback ROOM_EVENT ctx={} room={} event={:#x} data={}",
ev.ctx_id, ev.room_id, static_cast<u16>(ev.room_event),
fmt::ptr(ev.room_event_data));
ctx->room_event_callback(ev.ctx_id, ev.room_id, ev.room_event, ev.room_event_data,
ctx->room_event_callback_arg);
} else {
LOG_WARNING(Lib_NpMatching2,
"callback ROOM_EVENT ctx={} room={} event={:#x} SKIPPED: no callback",
ev.ctx_id, ev.room_id, static_cast<u16>(ev.room_event));
}
break;
case PendingEvent::LOBBY_EVENT_CB:
if (ctx->lobby_event_callback) {
LOG_DEBUG(Lib_NpMatching2, "callback LOBBY_EVENT ctx={} lobby={} event={:#x} data={}",
ev.ctx_id, ev.lobby_id, static_cast<u16>(ev.lobby_event),
fmt::ptr(ev.lobby_event_data));
ctx->lobby_event_callback(ev.ctx_id, ev.lobby_id, ev.lobby_event, ev.lobby_event_data,
ctx->lobby_event_callback_arg);
} else {
LOG_WARNING(Lib_NpMatching2,
"callback LOBBY_EVENT ctx={} lobby={} event={:#x} SKIPPED: no callback",
ev.ctx_id, ev.lobby_id, static_cast<u16>(ev.lobby_event));
}
break;
case PendingEvent::LOBBY_MESSAGE_CB:
if (ctx->lobby_message_callback) {
LOG_DEBUG(Lib_NpMatching2, "callback LOBBY_MESSAGE ctx={} lobby={} src={} event={:#x}",
ev.ctx_id, ev.lobby_id, ev.src_member_id, static_cast<u16>(ev.msg_event));
ctx->lobby_message_callback(ev.ctx_id, ev.lobby_id, ev.src_member_id, ev.msg_event,
ev.message_data, ctx->lobby_message_callback_arg);
} else {
LOG_WARNING(Lib_NpMatching2,
"callback LOBBY_MESSAGE ctx={} lobby={} SKIPPED: no callback", ev.ctx_id,
ev.lobby_id);
}
break;
case PendingEvent::ROOM_MESSAGE_CB:
if (ctx->room_message_callback) {
LOG_DEBUG(Lib_NpMatching2, "callback ROOM_MESSAGE ctx={} room={} src={} event={:#x}",
ev.ctx_id, ev.room_id, ev.src_member_id, static_cast<u16>(ev.msg_event));
ctx->room_message_callback(ev.ctx_id, ev.room_id, ev.src_member_id, ev.msg_event,
ev.message_data, ctx->room_message_callback_arg);
} else {
LOG_WARNING(Lib_NpMatching2,
"callback ROOM_MESSAGE ctx={} room={} SKIPPED: no callback", ev.ctx_id,
ev.room_id);
}
break;
}
@ -187,7 +499,8 @@ void FireEvent(const PendingEvent& ev) {
} // namespace
PS4_SYSV_ABI void* EventDispatcherThreadMain(void* /*arg*/) {
PS4_SYSV_ABI void* EventDispatcherThreadMain(void*) {
Common::SetCurrentThreadName("Matching2:Dispatch");
std::unique_lock lock(g_state.queue_mutex);
while (g_state.dispatch_running.load()) {
const auto now = std::chrono::steady_clock::now();
@ -236,7 +549,7 @@ void InitEventDispatcher() {
g_state.dispatch_thread = nullptr;
return;
}
LOG_INFO(Lib_NpMatching2, "event dispatcher thread created");
LOG_DEBUG(Lib_NpMatching2, "event dispatcher thread created");
}
void TermEventDispatcher() {

View File

@ -7,7 +7,9 @@
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
@ -18,6 +20,12 @@
#include "core/libraries/np/np_matching2/np_matching2_types.h"
#include "core/libraries/np/np_types.h"
namespace shadnet {
class CreateJoinRoomResponse;
class GetWorldInfoListReply;
class SearchRoomReply;
} // namespace shadnet
namespace Libraries::Np::NpMatching2 {
struct PeerInfo {
@ -26,7 +34,62 @@ struct PeerInfo {
OrbisNpMatching2RoomMemberId member_id = 0;
s32 conn_id = 0;
s32 status = 0;
std::string online_id;
Libraries::Np::OrbisNpOnlineId online_id{};
};
struct MemberBinCache {
OrbisNpMatching2AttributeId id = 0;
u64 update_date = 0;
std::vector<u8> data;
};
struct MemberCache {
Libraries::Np::OrbisNpId np_id{};
u64 join_date = 0;
OrbisNpMatching2RoomMemberId member_id = 0;
OrbisNpMatching2TeamId team_id = 0;
OrbisNpMatching2RoomGroupId group_id = 0;
OrbisNpMatching2NatType nat_type = 0;
OrbisNpMatching2Flags flag_attr = 0;
u32 addr = 0;
u16 port = 0;
std::map<OrbisNpMatching2AttributeId, MemberBinCache> bins;
};
struct RoomCache {
u32 num_slots = 0;
u64 mask_password = 0;
std::map<OrbisNpMatching2RoomGroupId, OrbisNpMatching2RoomGroup> groups;
std::map<OrbisNpMatching2RoomMemberId, MemberCache> members;
bool owner = false;
};
struct CallbackPayload {
std::unique_ptr<OrbisNpMatching2RoomDataInternal> room_data;
std::unique_ptr<OrbisNpMatching2CreateJoinRoomResponse> create_join_response;
std::unique_ptr<OrbisNpMatching2SearchRoomResponse> search_room_response;
std::unique_ptr<OrbisNpMatching2GetWorldInfoListResponse> world_info_response;
std::vector<OrbisNpMatching2World> world_list;
std::vector<OrbisNpMatching2RoomMemberDataInternal> member_data;
std::vector<OrbisNpMatching2RoomGroup> room_groups;
std::vector<OrbisNpMatching2RoomBinAttrInternal> room_bin_attrs;
std::vector<OrbisNpMatching2RoomMemberBinAttrInternal> member_bin_attrs;
std::vector<OrbisNpMatching2RoomDataExternal> room_data_external;
std::vector<std::vector<u8>> bin_buffers;
std::deque<OrbisNpMatching2IntAttr> ext_int_attrs;
std::deque<OrbisNpMatching2BinAttr> ext_bin_attrs;
std::deque<OrbisNpMatching2RoomGroupInfo> ext_room_groups;
std::deque<Libraries::Np::OrbisNpId> ext_owner_npids;
void* request_data = nullptr;
std::unique_ptr<OrbisNpMatching2RoomMemberUpdate> room_member_update;
std::unique_ptr<OrbisNpMatching2RoomUpdate> room_update;
std::unique_ptr<OrbisNpMatching2RoomMemberDataInternal> event_member;
void* room_event_data = nullptr;
void Reset() {
*this = CallbackPayload{};
}
};
struct ContextObject {
@ -36,7 +99,7 @@ struct ContextObject {
OrbisNpMatching2ServerId server_id = 1;
OrbisNpServiceLabel service_label = 0;
Libraries::Np::OrbisNpId owner_np_id{};
std::string online_id;
Libraries::Np::OrbisNpOnlineId online_id{};
std::string signaling_addr;
u16 signaling_port = 0;
@ -53,6 +116,11 @@ struct ContextObject {
std::map<OrbisNpMatching2RoomMemberId, PeerInfo> peers;
std::map<OrbisNpMatching2RoomId, RoomCache> room_cache;
CallbackPayload request_payload;
CallbackPayload room_event_payload;
OrbisNpMatching2ContextCallback context_callback = nullptr;
void* context_callback_arg = nullptr;
OrbisNpMatching2RequestCallback default_request_callback = nullptr;
@ -87,6 +155,8 @@ public:
s32 Start(OrbisNpMatching2ContextId ctx_id);
s32 Stop(OrbisNpMatching2ContextId ctx_id);
void ApplyContextCallback(OrbisNpMatching2ContextCallback callback, void* arg);
void Reset();
private:
@ -97,6 +167,9 @@ private:
std::array<ContextObject, kMaxContexts + 1> m_contexts{};
std::array<bool, kMaxContexts + 1> m_used{};
OrbisNpMatching2ContextId m_next_id = 1;
OrbisNpMatching2ContextCallback m_pending_context_callback = nullptr;
void* m_pending_context_callback_arg = nullptr;
};
struct PendingEvent {
@ -157,6 +230,10 @@ extern NpMatching2State g_state;
OrbisNpMatching2RequestId AllocRequestId();
void* BuildCreateJoinRoomPayload(ContextObject& ctx, const shadnet::CreateJoinRoomResponse& resp);
void* BuildGetWorldInfoListPayload(ContextObject& ctx, const shadnet::GetWorldInfoListReply& resp);
void* BuildSearchRoomPayload(ContextObject& ctx, const shadnet::SearchRoomReply& resp);
void InitEventDispatcher();
void TermEventDispatcher();

View File

@ -0,0 +1,564 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include "common/logging/log.h"
#include "core/libraries/network/net.h"
#include "core/libraries/np/np_error.h"
#include "core/libraries/np/np_matching2/np_matching2_internal.h"
#include "core/libraries/np/np_matching2/np_matching2_mm.h"
#include "shadnet.pb.h"
#include "shadnet/client.h"
namespace Libraries::Np::NpMatching2 {
namespace {
struct PendingRequest {
OrbisNpMatching2ContextId ctx_id = 0;
OrbisNpMatching2RequestId req_id = 0;
OrbisNpMatching2Event req_event{};
};
struct MmClientState {
std::shared_ptr<ShadNet::ShadNetClient> client;
std::mutex mutex;
u32 server_addr = 0;
u16 server_udp_port = 0;
std::mutex pending_mutex;
std::map<u64, PendingRequest> pending;
std::mutex sig_mutex;
std::condition_variable sig_cv;
std::map<u64, std::pair<ShadNet::ErrorType, std::vector<u8>>> sig_replies;
};
MmClientState g_mm;
u32 IpStringToAddr(std::string_view ip) {
u32 a, b, c, d;
if (std::sscanf(std::string(ip).c_str(), "%u.%u.%u.%u", &a, &b, &c, &d) != 4) {
return 0;
}
return a | (b << 8) | (c << 16) | (d << 24);
}
template <typename T>
std::vector<u8> MakeProtoPayload(const T& msg) {
std::string bytes;
(void)msg.SerializeToString(&bytes);
const u32 len = static_cast<u32>(bytes.size());
std::vector<u8> out;
out.reserve(4 + bytes.size());
out.push_back(static_cast<u8>(len));
out.push_back(static_cast<u8>(len >> 8));
out.push_back(static_cast<u8>(len >> 16));
out.push_back(static_cast<u8>(len >> 24));
out.insert(out.end(), bytes.begin(), bytes.end());
return out;
}
void AppendBinAttr(shadnet::MatchingBinAttr* dst, const OrbisNpMatching2BinAttr& src) {
dst->set_attr_id(src.id);
if (src.data && src.dataSize > 0) {
dst->set_data(src.data, src.dataSize);
}
}
void AppendIntAttr(shadnet::MatchingIntAttr* dst, const OrbisNpMatching2IntAttr& src) {
dst->set_attr_id(src.id);
dst->set_attr_value(src.num);
}
std::string ExtractProtoBytes(const std::vector<u8>& payload, size_t offset = 0) {
if (payload.size() < offset + 4) {
return {};
}
const u32 len = static_cast<u32>(payload[offset]) |
(static_cast<u32>(payload[offset + 1]) << 8) |
(static_cast<u32>(payload[offset + 2]) << 16) |
(static_cast<u32>(payload[offset + 3]) << 24);
if (payload.size() < offset + 4 + len) {
return {};
}
return std::string(reinterpret_cast<const char*>(payload.data() + offset + 4), len);
}
void DispatchRequestComplete(const PendingRequest& pr, ShadNet::ErrorType error,
const std::vector<u8>& body) {
ContextObject* ctx = ContextManager::Instance().Get(pr.ctx_id);
if (!ctx) {
return;
}
const s32 error_code = (error == ShadNet::ErrorType::NoError) ? 0 : static_cast<s32>(error);
if (pr.req_event == ORBIS_NP_MATCHING2_CONTEXT_EVENT_STARTED ||
pr.req_event == ORBIS_NP_MATCHING2_CONTEXT_EVENT_STOPPED) {
PendingEvent ev{};
ev.type = PendingEvent::CONTEXT_CB;
ev.ctx_id = pr.ctx_id;
ev.fire_at = std::chrono::steady_clock::now();
ev.ctx_event = pr.req_event;
ev.ctx_event_cause = ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ACTION;
ev.error_code = error_code;
ScheduleEvent(std::move(ev));
return;
}
void* request_data = nullptr;
if (error_code == 0) {
const std::string proto = ExtractProtoBytes(body);
if (pr.req_event == ORBIS_NP_MATCHING2_REQUEST_EVENT_CREATE_JOIN_ROOM ||
pr.req_event == ORBIS_NP_MATCHING2_REQUEST_EVENT_JOIN_ROOM) {
shadnet::CreateRoomReply reply;
if (reply.ParseFromString(proto) && reply.has_details()) {
request_data = BuildCreateJoinRoomPayload(*ctx, reply.details());
}
} else if (pr.req_event == ORBIS_NP_MATCHING2_REQUEST_EVENT_GET_WORLD_INFO_LIST) {
shadnet::GetWorldInfoListReply reply;
if (reply.ParseFromString(proto)) {
request_data = BuildGetWorldInfoListPayload(*ctx, reply);
}
} else if (pr.req_event == ORBIS_NP_MATCHING2_REQUEST_EVENT_SEARCH_ROOM) {
shadnet::SearchRoomReply reply;
if (reply.ParseFromString(proto)) {
request_data = BuildSearchRoomPayload(*ctx, reply);
}
}
}
PendingEvent ev{};
ev.type = PendingEvent::REQUEST_CB;
ev.ctx_id = pr.ctx_id;
ev.fire_at = std::chrono::steady_clock::now();
ev.req_id = pr.req_id;
ev.req_event = pr.req_event;
ev.error_code = error_code;
ev.request_cb = ctx->default_request_callback;
ev.request_cb_arg = ctx->default_request_callback_arg;
ev.request_data = request_data;
ScheduleEvent(std::move(ev));
}
void BuildMemberUpdate(CallbackPayload& p, const RoomCache& rc, const MemberCache& mc,
OrbisNpMatching2EventCause cause) {
p.event_member = std::make_unique<OrbisNpMatching2RoomMemberDataInternal>();
OrbisNpMatching2RoomMemberDataInternal& m = *p.event_member;
m = OrbisNpMatching2RoomMemberDataInternal{};
m.memberId = mc.member_id;
m.teamId = mc.team_id;
m.natType = mc.nat_type;
m.flagAttr = mc.flag_attr;
m.joinDate = mc.join_date;
m.npId = mc.np_id;
if (mc.group_id != 0) {
auto grp_it = rc.groups.find(mc.group_id);
if (grp_it != rc.groups.end()) {
p.room_groups.resize(1);
p.room_groups[0] = grp_it->second;
m.roomGroup = p.room_groups.data();
}
}
p.member_bin_attrs.resize(mc.bins.size());
p.bin_buffers.reserve(mc.bins.size());
size_t i = 0;
for (const auto& [attr_id, bin] : mc.bins) {
p.bin_buffers.emplace_back(bin.data.begin(), bin.data.end());
auto& buf = p.bin_buffers.back();
auto& dst = p.member_bin_attrs[i++];
dst = OrbisNpMatching2RoomMemberBinAttrInternal{};
dst.binAttr.id = bin.id;
dst.binAttr.data = buf.empty() ? nullptr : buf.data();
dst.binAttr.dataSize = buf.size();
}
m.roomMemberBinAttrInternal = p.member_bin_attrs.empty() ? nullptr : p.member_bin_attrs.data();
m.roomMemberBinAttrInternalNum = p.member_bin_attrs.size();
p.room_member_update = std::make_unique<OrbisNpMatching2RoomMemberUpdate>();
OrbisNpMatching2RoomMemberUpdate& u = *p.room_member_update;
u = OrbisNpMatching2RoomMemberUpdate{};
u.roomMemberDataInternal = p.event_member.get();
u.eventCause = cause;
p.room_event_data = p.room_member_update.get();
}
void HandleRoomEvent(const ShadNet::NotifyRoomEvent& n) {
ContextObject* ctx =
ContextManager::Instance().Get(static_cast<OrbisNpMatching2ContextId>(n.ctx_id));
if (!ctx) {
return;
}
const auto event = static_cast<OrbisNpMatching2Event>(n.event);
const auto cause = static_cast<OrbisNpMatching2EventCause>(n.event_cause);
const auto room_id = static_cast<OrbisNpMatching2RoomId>(n.room_id);
const auto member_id = static_cast<OrbisNpMatching2RoomMemberId>(n.member_id);
CallbackPayload& p = ctx->room_event_payload;
p.Reset();
switch (event) {
case ORBIS_NP_MATCHING2_ROOM_EVENT_MEMBER_JOINED: {
auto room_it = ctx->room_cache.find(room_id);
if (room_it == ctx->room_cache.end()) {
return;
}
MemberCache& mc = room_it->second.members[member_id];
mc = MemberCache{};
mc.member_id = member_id;
mc.team_id = static_cast<OrbisNpMatching2TeamId>(n.member_team_id);
mc.nat_type = static_cast<OrbisNpMatching2NatType>(n.member_nat_type);
mc.flag_attr = n.member_flag_attr;
mc.group_id = static_cast<OrbisNpMatching2RoomGroupId>(n.member_group_id);
mc.join_date = n.member_join_date;
mc.addr = IpStringToAddr(n.member_addr);
mc.port = Libraries::Net::sceNetHtons(static_cast<u16>(n.member_port));
std::strncpy(mc.np_id.handle.data, n.member_npid.c_str(), sizeof(mc.np_id.handle.data) - 1);
for (const auto& a : n.member_bin_attrs) {
MemberBinCache& b = mc.bins[a.attr_id];
b.id = static_cast<OrbisNpMatching2AttributeId>(a.attr_id);
b.data = a.data;
}
PeerInfo pi{};
pi.member_id = member_id;
pi.addr = mc.addr;
pi.port = mc.port;
std::strncpy(pi.online_id.data, n.member_npid.c_str(), sizeof(pi.online_id.data) - 1);
ctx->peers[member_id] = pi;
BuildMemberUpdate(p, room_it->second, mc, cause);
break;
}
case ORBIS_NP_MATCHING2_ROOM_EVENT_MEMBER_LEFT: {
auto room_it = ctx->room_cache.find(room_id);
if (room_it == ctx->room_cache.end()) {
return;
}
auto mem_it = room_it->second.members.find(member_id);
if (mem_it == room_it->second.members.end()) {
return;
}
BuildMemberUpdate(p, room_it->second, mem_it->second, cause);
room_it->second.members.erase(mem_it);
ctx->peers.erase(member_id);
break;
}
case ORBIS_NP_MATCHING2_ROOM_EVENT_KICKEDOUT: {
p.room_update = std::make_unique<OrbisNpMatching2RoomUpdate>();
OrbisNpMatching2RoomUpdate& u = *p.room_update;
u = OrbisNpMatching2RoomUpdate{};
u.eventCause = cause;
u.errorCode = n.error_code;
p.room_event_data = p.room_update.get();
break;
}
default:
LOG_WARNING(Lib_NpMatching2, "unhandled room event {:#x}", n.event);
return;
}
LOG_DEBUG(Lib_NpMatching2, "RoomEvent ctx={} room={} event={:#x} cause={}", n.ctx_id, n.room_id,
n.event, n.event_cause);
PendingEvent ev{};
ev.type = PendingEvent::ROOM_EVENT_CB;
ev.ctx_id = ctx->ctx_id;
ev.fire_at = std::chrono::steady_clock::now();
ev.room_id = room_id;
ev.room_event = event;
ev.room_event_data = p.room_event_data;
ScheduleEvent(std::move(ev));
}
} // namespace
void OnMatchingReply(ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
const std::vector<u8>& body) {
if (cmd == ShadNet::CommandType::RequestSignalingInfos) {
std::lock_guard lock(g_mm.sig_mutex);
g_mm.sig_replies[pkt_id] = {error, body};
g_mm.sig_cv.notify_all();
return;
}
PendingRequest pr;
{
std::lock_guard lock(g_mm.pending_mutex);
auto it = g_mm.pending.find(pkt_id);
if (it == g_mm.pending.end()) {
LOG_WARNING(Lib_NpMatching2, "reply pkt_id={} cmd={} has no pending entry (have {})",
pkt_id, static_cast<u16>(cmd), g_mm.pending.size());
return;
}
pr = it->second;
g_mm.pending.erase(it);
}
LOG_DEBUG(Lib_NpMatching2, "reply pkt_id={} cmd={} -> ctx={} reqId={} event={:#x}", pkt_id,
static_cast<u16>(cmd), pr.ctx_id, pr.req_id, static_cast<u16>(pr.req_event));
DispatchRequestComplete(pr, error, body);
}
void SetMmShadNetClient(std::shared_ptr<ShadNet::ShadNetClient> client,
std::string_view server_host, u16 tcp_port) {
{
std::lock_guard lock(g_mm.mutex);
g_mm.client = client;
g_mm.server_addr = client ? client->GetAddrServer() : 0;
if (g_mm.server_addr == 0) {
g_mm.server_addr = IpStringToAddr(server_host);
}
g_mm.server_udp_port = Libraries::Net::sceNetHtons(static_cast<u16>(tcp_port + 1));
}
if (!client) {
return;
}
client->onRoomEvent = [](const ShadNet::NotifyRoomEvent& n) { HandleRoomEvent(n); };
}
void ClearMmShadNetClient() {
std::shared_ptr<ShadNet::ShadNetClient> old_client;
{
std::lock_guard lock(g_mm.mutex);
old_client = std::move(g_mm.client);
g_mm.server_addr = 0;
g_mm.server_udp_port = 0;
}
if (old_client) {
old_client->onRoomEvent = nullptr;
}
{
std::lock_guard lock(g_mm.pending_mutex);
g_mm.pending.clear();
}
g_mm.sig_cv.notify_all();
}
bool IsMmClientRunning() {
std::lock_guard lock(g_mm.mutex);
return g_mm.client && g_mm.client->IsAuthenticated();
}
void MmContextStart(OrbisNpMatching2ContextId ctx_id) {
shadnet::ContextStartRequest req;
req.set_ctx_id(ctx_id);
MmSubmitRequest(ctx_id, 0, ORBIS_NP_MATCHING2_CONTEXT_EVENT_STARTED, MmCommand::ContextStart,
MakeProtoPayload(req));
}
void MmContextStop(OrbisNpMatching2ContextId ctx_id) {
shadnet::ContextStopRequest req;
req.set_ctx_id(ctx_id);
MmSubmitRequest(ctx_id, 0, ORBIS_NP_MATCHING2_CONTEXT_EVENT_STOPPED, MmCommand::ContextStop,
MakeProtoPayload(req));
}
s32 MmSubmitRequest(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
OrbisNpMatching2Event req_event, MmCommand cmd,
const std::vector<u8>& payload) {
std::shared_ptr<ShadNet::ShadNetClient> client;
{
std::lock_guard lock(g_mm.mutex);
client = g_mm.client;
}
if (!client || !client->IsAuthenticated()) {
LOG_ERROR(Lib_NpMatching2, "MmSubmitRequest({}): not connected", static_cast<u16>(cmd));
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
const u64 pkt_id = client->SubmitRequest(static_cast<ShadNet::CommandType>(cmd), payload);
{
std::lock_guard lock(g_mm.pending_mutex);
g_mm.pending[pkt_id] = {ctx_id, req_id, req_event};
}
LOG_DEBUG(Lib_NpMatching2, "submit cmd={} pkt_id={} ctx={} reqId={}", static_cast<u16>(cmd),
pkt_id, ctx_id, req_id);
return ORBIS_OK;
}
s32 MmCreateJoinRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2CreateJoinRoomRequest& request) {
shadnet::CreateRoomRequest req;
req.set_req_id(req_id);
req.set_max_slots(request.maxSlot);
req.set_team_id(request.teamId);
req.set_world_id(request.worldId);
req.set_lobby_id(static_cast<u32>(request.lobbyId));
req.set_flags(request.flags);
req.set_group_config_count(static_cast<u32>(request.groupConfigs));
req.set_allowed_user_count(static_cast<u32>(request.allowedUsers));
req.set_blocked_user_count(static_cast<u32>(request.blockedUsers));
req.set_internal_bin_attr_count(static_cast<u32>(request.internalBinAttrs));
for (u64 i = 0; i < request.externalSearchIntAttrs; ++i) {
AppendIntAttr(req.add_external_search_int_attrs(), request.externalSearchIntAttr[i]);
}
for (u64 i = 0; i < request.externalSearchBinAttrs; ++i) {
AppendBinAttr(req.add_external_search_bin_attrs(), request.externalSearchBinAttr[i]);
}
for (u64 i = 0; i < request.externalBinAttrs; ++i) {
AppendBinAttr(req.add_external_bin_attrs(), request.externalBinAttr[i]);
}
for (u64 i = 0; i < request.memberInternalBinAttrs; ++i) {
AppendBinAttr(req.add_member_bin_attrs(), request.memberInternalBinAttr[i]);
}
req.set_join_group_label_present(request.joinGroupLabel != nullptr);
req.set_room_password_present(request.roomPasswd != nullptr);
if (request.signalingParam) {
req.set_sig_type(request.signalingParam->type);
req.set_sig_flag(request.signalingParam->flag);
req.set_sig_main_member(request.signalingParam->memberId);
}
return MmSubmitRequest(ctx_id, req_id, ORBIS_NP_MATCHING2_REQUEST_EVENT_CREATE_JOIN_ROOM,
MmCommand::CreateRoom, MakeProtoPayload(req));
}
s32 MmJoinRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2JoinRoomRequest& request) {
shadnet::JoinRoomRequest req;
req.set_room_id(request.roomId);
req.set_req_id(req_id);
req.set_team_id(request.teamId);
req.set_join_flags(request.flags);
req.set_blocked_user_count(static_cast<u32>(request.blockedUsers));
for (u64 i = 0; i < request.roomMemberBinInternalAttrNum; ++i) {
AppendBinAttr(req.add_member_bin_attrs(), request.roomMemberBinInternalAttr[i]);
}
req.set_room_password_present(request.roomPasswd != nullptr);
req.set_join_group_label_present(request.joinGroupLabel != nullptr);
return MmSubmitRequest(ctx_id, req_id, ORBIS_NP_MATCHING2_REQUEST_EVENT_JOIN_ROOM,
MmCommand::JoinRoom, MakeProtoPayload(req));
}
s32 MmLeaveRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2LeaveRoomRequest& request) {
shadnet::LeaveRoomRequest req;
req.set_room_id(request.roomId);
req.set_req_id(req_id);
return MmSubmitRequest(ctx_id, req_id, ORBIS_NP_MATCHING2_REQUEST_EVENT_LEAVE_ROOM,
MmCommand::LeaveRoom, MakeProtoPayload(req));
}
s32 MmGetWorldInfoList(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2GetWorldInfoListRequest& request) {
shadnet::GetWorldInfoListRequest req;
req.set_server_id(request.serverId);
return MmSubmitRequest(ctx_id, req_id, ORBIS_NP_MATCHING2_REQUEST_EVENT_GET_WORLD_INFO_LIST,
MmCommand::GetWorldInfoList, MakeProtoPayload(req));
}
s32 MmSearchRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SearchRoomRequest& request) {
shadnet::SearchRoomRequest req;
req.set_world_id(request.worldId);
req.set_lobby_id(request.lobbyId);
req.set_range_filter_start(request.rangeFilter.start);
req.set_range_filter_max(request.rangeFilter.max);
req.set_flag_filter(request.flags1);
req.set_flag_attr(request.flags2);
LOG_DEBUG(Lib_NpMatching2,
"search world={} lobby={} range={}+{} flags={:#x}/{:#x} intN={} binN={} attrN={}",
request.worldId, request.lobbyId, request.rangeFilter.start, request.rangeFilter.max,
request.flags1, request.flags2, request.intFilters, request.binFilters,
request.attrs);
for (u64 i = 0; i < request.intFilters; ++i) {
const auto& f = request.intFilter[i];
LOG_DEBUG(Lib_NpMatching2, " intFilter[{}] op={} id={:#x} num={}", i, f.searchOperator,
f.attr.id, f.attr.num);
auto* dst = req.add_int_filters();
dst->set_op(f.searchOperator);
dst->set_attr_id(f.attr.id);
dst->set_attr_value(f.attr.num);
}
for (u64 i = 0; i < request.binFilters; ++i) {
const auto& f = request.binFilter[i];
auto* dst = req.add_bin_filters();
dst->set_op(f.searchOperator);
dst->set_attr_id(f.attr.id);
if (f.attr.data && f.attr.dataSize > 0) {
dst->set_data(f.attr.data, f.attr.dataSize);
}
}
for (u64 i = 0; i < request.attrs; ++i) {
req.add_attr_ids(request.attr[i]);
}
return MmSubmitRequest(ctx_id, req_id, ORBIS_NP_MATCHING2_REQUEST_EVENT_SEARCH_ROOM,
MmCommand::SearchRoom, MakeProtoPayload(req));
}
u32 GetMmServerAddr() {
std::lock_guard lock(g_mm.mutex);
return g_mm.server_addr;
}
u16 GetMmServerUdpPort() {
std::lock_guard lock(g_mm.mutex);
return g_mm.server_udp_port;
}
bool RequestSignalingInfos(std::string_view target_online_id, u32* out_addr, u16* out_port) {
if (!out_addr || !out_port) {
return false;
}
std::shared_ptr<ShadNet::ShadNetClient> client;
{
std::lock_guard lock(g_mm.mutex);
client = g_mm.client;
}
if (!client || !client->IsAuthenticated()) {
return false;
}
shadnet::RequestSignalingInfosRequest req;
req.set_target_npid(std::string(target_online_id));
const u64 pkt_id =
client->SubmitRequest(ShadNet::CommandType::RequestSignalingInfos, MakeProtoPayload(req));
std::pair<ShadNet::ErrorType, std::vector<u8>> reply;
{
std::unique_lock lock(g_mm.sig_mutex);
if (!g_mm.sig_cv.wait_for(lock, std::chrono::seconds(5),
[&] { return g_mm.sig_replies.count(pkt_id) > 0; })) {
LOG_WARNING(Lib_NpMatching2, "timed out for '{}'", target_online_id);
return false;
}
reply = std::move(g_mm.sig_replies[pkt_id]);
g_mm.sig_replies.erase(pkt_id);
}
if (reply.first != ShadNet::ErrorType::NoError) {
return false;
}
shadnet::RequestSignalingInfosReply rep;
const std::string proto = ExtractProtoBytes(reply.second);
if (proto.empty() || !rep.ParseFromString(proto)) {
return false;
}
const std::string addr_str = rep.target_ip();
const u16 port_host = static_cast<u16>(rep.target_port());
if (addr_str.empty() || port_host == 0) {
return false;
}
u32 addr_nbo = 0;
if (Libraries::Net::sceNetInetPton(Net::ORBIS_NET_AF_INET, addr_str.c_str(), &addr_nbo) <= 0) {
return false;
}
*out_addr = addr_nbo;
*out_port = Libraries::Net::sceNetHtons(port_host);
LOG_DEBUG(Lib_NpMatching2, "'{}' -> {}:{}", target_online_id, addr_str, port_host);
return true;
}
} // namespace Libraries::Np::NpMatching2

View File

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <string_view>
#include <vector>
#include "common/types.h"
#include "core/libraries/np/np_matching2/np_matching2.h"
#include "core/libraries/np/np_matching2/np_matching2_types.h"
namespace ShadNet {
class ShadNetClient;
enum class CommandType : u16;
enum class ErrorType : u8;
} // namespace ShadNet
namespace Libraries::Np::NpMatching2 {
enum class MmCommand : u16 {
ContextStart = 12,
CreateRoom = 13,
JoinRoom = 14,
LeaveRoom = 15,
SearchRoom = 16,
RequestSignalingInfos = 17,
ContextStop = 18,
SetRoomDataInternal = 20,
SetRoomDataExternal = 21,
KickoutRoomMember = 22,
GetWorldInfoList = 23,
};
void SetMmShadNetClient(std::shared_ptr<ShadNet::ShadNetClient> client,
std::string_view server_host, u16 tcp_port);
void ClearMmShadNetClient();
bool IsMmClientRunning();
void OnMatchingReply(ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
const std::vector<u8>& body);
void MmContextStart(OrbisNpMatching2ContextId ctx_id);
void MmContextStop(OrbisNpMatching2ContextId ctx_id);
s32 MmSubmitRequest(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
OrbisNpMatching2Event req_event, MmCommand cmd, const std::vector<u8>& payload);
s32 MmCreateJoinRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2CreateJoinRoomRequest& request);
s32 MmJoinRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2JoinRoomRequest& request);
s32 MmLeaveRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2LeaveRoomRequest& request);
s32 MmGetWorldInfoList(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2GetWorldInfoListRequest& request);
s32 MmSearchRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SearchRoomRequest& request);
u32 GetMmServerAddr();
u16 GetMmServerUdpPort();
bool RequestSignalingInfos(std::string_view target_online_id, u32* out_addr, u16* out_port);
} // namespace Libraries::Np::NpMatching2

View File

@ -261,6 +261,20 @@ struct OrbisNpMatching2PresenceOptionData {
u64 len;
};
struct OrbisNpMatching2RoomMemberUpdateA {
OrbisNpMatching2RoomMemberDataInternalA* roomMemberDataInternal;
OrbisNpMatching2EventCause eventCause;
u8 padding[7];
OrbisNpMatching2PresenceOptionData optData;
};
struct OrbisNpMatching2RoomUpdate {
OrbisNpMatching2EventCause eventCause;
u8 padding[3];
s32 errorCode;
OrbisNpMatching2PresenceOptionData optData;
};
struct OrbisNpMatching2JoinRoomRequest {
OrbisNpMatching2RoomId roomId;
OrbisNpMatching2SessionPassword* roomPasswd;
@ -315,6 +329,20 @@ struct OrbisNpMatching2RangeFilter {
u32 max;
};
using OrbisNpMatching2Operator = u8;
struct OrbisNpMatching2IntSearchFilter {
OrbisNpMatching2Operator searchOperator;
u8 padding[7];
OrbisNpMatching2IntAttr attr;
};
struct OrbisNpMatching2BinSearchFilter {
OrbisNpMatching2Operator searchOperator;
u8 padding[7];
OrbisNpMatching2BinAttr attr;
};
struct OrbisNpMatching2RequestOptParam {
OrbisNpMatching2RequestCallback callback;
void* arg;
@ -404,6 +432,13 @@ struct OrbisNpMatching2RoomMemberDataInternal {
};
static_assert(sizeof(OrbisNpMatching2RoomMemberDataInternal) == 0x58);
struct OrbisNpMatching2RoomMemberUpdate {
OrbisNpMatching2RoomMemberDataInternal* roomMemberDataInternal;
OrbisNpMatching2EventCause eventCause;
u8 padding[7];
OrbisNpMatching2PresenceOptionData optData;
};
struct OrbisNpMatching2RoomMemberDataInternalList {
OrbisNpMatching2RoomMemberDataInternal* members;
u64 membersNum;
@ -450,9 +485,9 @@ struct OrbisNpMatching2SearchRoomRequest {
OrbisNpMatching2RangeFilter rangeFilter;
OrbisNpMatching2Flags flags1;
OrbisNpMatching2Flags flags2;
void* intFilter;
OrbisNpMatching2IntSearchFilter* intFilter;
u64 intFilters;
void* binFilter;
OrbisNpMatching2BinSearchFilter* binFilter;
u64 binFilters;
OrbisNpMatching2AttributeId* attr;
u64 attrs;

View File

@ -7,7 +7,9 @@
#include <thread>
#include "client.h"
#include "common/elf_info.h"
#include "common/logging/log.h"
#include "common/thread.h"
#include "shadnet.pb.h"
#ifdef _WIN32
@ -132,6 +134,9 @@ u64 ShadNetClient::GetUserId() const {
u32 ShadNetClient::GetAddrLocal() const {
return m_addr_local.load();
}
u32 ShadNetClient::GetAddrServer() const {
return m_addr_server.load();
}
u32 ShadNetClient::GetNumFriends() const {
std::lock_guard lock(m_mutex_friends);
@ -153,6 +158,7 @@ std::string ShadNetClient::GetBearerToken() const {
// Threading
void ShadNetClient::ConnectThread() {
Common::SetCurrentThreadName("ShadNet:Connect");
bool connected = false;
u32 backoff_ms = SHAD_CONNECT_RETRY_BACKOFF_MS;
for (u32 attempt = 1; attempt <= SHAD_CONNECT_MAX_ATTEMPTS && !m_terminate; ++attempt) {
@ -187,6 +193,7 @@ void ShadNetClient::ConnectThread() {
req.set_password(m_password);
if (!m_token.empty())
req.set_token(m_token);
req.set_title_id(std::string(Common::ElfInfo::Instance().GameSerial()));
const u64 id = m_pkt_counter.fetch_add(1);
if (!SendAll(BuildPacket(CommandType::Login, id, MakeProtoPayload(req)))) {
@ -198,6 +205,7 @@ void ShadNetClient::ConnectThread() {
}
void ShadNetClient::ReaderThread() {
Common::SetCurrentThreadName("ShadNet:Reader");
while (!m_terminate) {
u8 hdr[SHAD_HEADER_SIZE];
if (!RecvN(hdr, SHAD_HEADER_SIZE)) {
@ -239,6 +247,7 @@ void ShadNetClient::ReaderThread() {
}
void ShadNetClient::WriterThread() {
Common::SetCurrentThreadName("ShadNet:Writer");
while (!m_terminate) {
std::unique_lock lock(m_mutex_send_queue);
m_cv_send_queue.wait(lock, [&] { return m_terminate.load() || !m_send_queue.empty(); });
@ -342,6 +351,11 @@ bool ShadNetClient::DoConnect() {
if (::getsockname(m_sock, reinterpret_cast<struct sockaddr*>(&local), &alen) == 0)
m_addr_local.store(local.sin_addr.s_addr);
struct sockaddr_in peer{};
socklen_t plen = sizeof(peer);
if (::getpeername(m_sock, reinterpret_cast<struct sockaddr*>(&peer), &plen) == 0)
m_addr_server.store(peer.sin_addr.s_addr);
LOG_INFO(ShadNet, "TCP connected to {}:{}", m_host, m_port);
// Apply receive timeout for the ServerInfo handshake.
@ -717,6 +731,50 @@ void ShadNetClient::HandleNotification(u16 cmd_raw, const std::vector<u8>& paylo
onFriendStatus(n);
break;
}
case NotificationType::RoomEvent: {
shadnet::NotifyRoomEvent pb;
if (!pb.ParseFromString(blob)) {
LOG_WARNING(ShadNet, "RoomEvent parse error");
break;
}
NotifyRoomEvent n;
n.ctx_id = pb.ctx_id();
n.room_id = pb.room_id();
n.event = pb.event();
n.event_cause = pb.event_cause();
n.error_code = pb.error_code();
n.flags = pb.flags();
if (pb.has_member()) {
const auto& m = pb.member();
n.member_npid = m.npid();
n.member_id = m.member_id();
n.member_team_id = m.team_id();
n.member_is_owner = m.is_owner();
n.member_join_date = m.join_date();
n.member_nat_type = m.nat_type();
n.member_flag_attr = m.flag_attr();
n.member_group_id = m.group_id();
n.member_addr = m.addr();
n.member_port = m.port();
for (const auto& a : m.bin_attrs_internal()) {
MatchingBinAttr ba;
ba.attr_id = a.attr_id();
ba.data.assign(a.data().begin(), a.data().end());
n.member_bin_attrs.push_back(std::move(ba));
}
}
for (const auto& a : pb.bin_attrs()) {
MatchingBinAttr ba;
ba.attr_id = a.attr_id();
ba.data.assign(a.data().begin(), a.data().end());
n.bin_attrs.push_back(std::move(ba));
}
LOG_DEBUG(ShadNet, "RoomEvent room_id={} event={:#x} cause={}", n.room_id, n.event,
n.event_cause);
if (onRoomEvent)
onRoomEvent(n);
break;
}
case NotificationType::WebApiPushEvent: {
// Length-prefixed fields: npServiceName, npServiceLabel(u32 LE), dataType,
// data, fromNpid, toNpid. ExtractBlob returns {} past the end, so a short

View File

@ -66,16 +66,18 @@ enum class CommandType : u16 {
AddBlock = 10,
RemoveBlock = 11,
// Matchmaking
RegisterHandlers = 12,
ContextStart = 12,
CreateRoom = 13,
JoinRoom = 14,
LeaveRoom = 15,
GetRoomList = 16,
SearchRoom = 16,
RequestSignalingInfos = 17,
ContextStop = 18,
SetRoomDataInternal = 20,
SetRoomDataExternal = 21,
KickoutRoomMember = 22,
// 23-29 reserved for future matchmaking commands
GetWorldInfoList = 23,
// 24-29 reserved for future matchmaking commands
GetBoardInfos = 30,
RecordScore = 31,
RecordScoreData = 32,
@ -93,6 +95,7 @@ enum class NotificationType : u16 {
FriendNew = 6,
FriendLost = 7,
FriendStatus = 8,
RoomEvent = 10,
WebApiPushEvent = 17, // Generic NP WebApi push event
};
@ -189,6 +192,34 @@ struct NotifyWebApiPushEvent {
std::string toNpid; // may be empty
};
struct MatchingBinAttr {
u32 attr_id = 0;
std::vector<u8> data;
};
struct NotifyRoomEvent {
u32 ctx_id = 0;
u64 room_id = 0;
u32 event = 0;
u32 event_cause = 0;
s32 error_code = 0;
u32 flags = 0;
std::string member_npid;
u32 member_id = 0;
u32 member_team_id = 0;
bool member_is_owner = false;
u64 member_join_date = 0;
u32 member_nat_type = 0;
u32 member_flag_attr = 0;
u32 member_group_id = 0;
std::string member_addr;
u32 member_port = 0;
std::vector<MatchingBinAttr> member_bin_attrs;
std::vector<MatchingBinAttr> bin_attrs;
};
// ShadNetClient
class ShadNetClient {
@ -212,6 +243,7 @@ public:
const std::string& GetAvatarUrl() const;
u64 GetUserId() const;
u32 GetAddrLocal() const;
u32 GetAddrServer() const;
u32 GetNumFriends() const;
std::optional<std::string> GetFriendNpid(u32 index) const;
@ -223,6 +255,7 @@ public:
std::function<void(const NotifyFriendNew&)> onFriendNew;
std::function<void(const NotifyFriendLost&)> onFriendLost;
std::function<void(const NotifyFriendStatus&)> onFriendStatus;
std::function<void(const NotifyRoomEvent&)> onRoomEvent;
std::function<void(const NotifyWebApiPushEvent&)> onWebApiPushEvent;
// Async reply callback.
// cmd —command this reply is for (matches the request's cmd)
@ -298,6 +331,7 @@ private:
mutable std::mutex m_mutex_bearer;
std::string m_bearer_token;
std::atomic<u32> m_addr_local{0};
std::atomic<u32> m_addr_server{0};
mutable std::mutex m_mutex_friends;
std::vector<FriendEntry> m_friends;

View File

@ -13,7 +13,8 @@ option optimize_for = LITE_RUNTIME;
message LoginRequest {
string npid = 1;
string password = 2;
string token = 3;
string token = 3;
string title_id = 4;
}
message FriendEntry {
@ -202,12 +203,6 @@ message MatchingRoomGroup {
uint32 num_members = 6;
}
message MatchingCallbackEntry {
bool enabled = 1;
uint64 callback_addr = 2;
uint64 callback_arg = 3;
}
// Room data structures
message MatchingRoomDataInternal {
@ -234,6 +229,12 @@ message MatchingRoomMemberData {
bool is_owner = 4;
MatchingRoomGroup group = 5;
repeated MatchingBinAttr bin_attrs_internal = 6;
uint64 join_date = 7;
uint32 nat_type = 8;
uint32 flag_attr = 9;
string addr = 10;
uint32 port = 11;
uint32 group_id = 12;
}
message MatchingRoomDataExternal {
@ -266,12 +267,12 @@ message CreateJoinRoomResponse {
// Command requests
message RegisterHandlersRequest {
string addr = 1;
uint32 port = 2;
uint32 ctx_id = 3;
uint32 service_label = 4;
repeated MatchingCallbackEntry callbacks = 5;
message ContextStartRequest {
uint32 ctx_id = 1;
}
message ContextStopRequest {
uint32 ctx_id = 1;
}
message CreateRoomRequest {
@ -364,8 +365,52 @@ message JoinRoomReply {
CreateJoinRoomResponse details = 6;
}
message GetRoomListReply {
message MatchingIntSearchFilter {
uint32 op = 1; // 1=Eq 2=Ne 3=Lt 4=Le 5=Gt 6=Ge
uint32 attr_id = 2;
uint32 attr_value = 3;
}
message MatchingBinSearchFilter {
uint32 op = 1; // 1=Eq 2=Ne
uint32 attr_id = 2;
bytes data = 3;
}
message SearchRoomRequest {
uint32 world_id = 1;
uint64 lobby_id = 2;
uint32 range_filter_start = 3;
uint32 range_filter_max = 4;
uint32 flag_filter = 5;
uint32 flag_attr = 6;
repeated MatchingIntSearchFilter int_filters = 7;
repeated MatchingBinSearchFilter bin_filters = 8;
repeated uint32 attr_ids = 9;
}
message SearchRoomReply {
repeated MatchingRoomDataExternal rooms = 1;
uint32 range_start = 2;
uint32 range_total = 3;
uint32 range_result = 4;
}
message MatchingWorld {
uint32 world_id = 1;
uint32 lobbies_num = 2;
uint32 max_lobby_members = 3;
uint32 lobby_members_num = 4;
uint32 rooms_num = 5;
uint32 room_members_num = 6;
}
message GetWorldInfoListRequest {
uint32 server_id = 1;
}
message GetWorldInfoListReply {
repeated MatchingWorld worlds = 1;
}
message RequestSignalingInfosReply {
@ -382,74 +427,19 @@ message KickoutRoomMemberReply { uint64 room_id = 1; }
// Notification payloads
// Every notification carries ctx_id so the client can route it to the owning
// context (the server tracks ctx_id per connection from RegisterHandlersRequest).
// context (the server tracks ctx_id per connection from ContextStartRequest).
// NotificationType::RequestEvent (9)
message NotifyRequestEvent {
uint32 ctx_id = 1;
uint32 server_id = 2;
uint32 world_id = 3;
uint32 lobby_id = 4;
uint32 req_event = 5;
uint32 req_id = 6;
uint32 error_code = 7;
uint64 room_id = 8;
uint32 member_id = 9;
uint32 max_slots = 10;
uint32 flags = 11;
bool is_owner = 12;
bytes response_blob = 13;
}
// NotificationType::MemberJoined (10)
message NotifyMemberJoined {
uint64 room_id = 1;
uint32 member_id = 2;
string npid = 3;
string addr = 4;
uint32 port = 5;
repeated MatchingBinAttr bin_attrs = 6;
uint32 ctx_id = 7;
}
// NotificationType::MemberLeft (11)
message NotifyMemberLeft {
uint64 room_id = 1;
uint32 member_id = 2;
string npid = 3;
uint32 ctx_id = 4;
}
// NotificationType::SignalingHelper (12)
message NotifySignalingHelper {
string npid = 1;
uint32 member_id = 2;
string addr = 3;
uint32 port = 4;
uint32 ctx_id = 5;
}
// NotificationType::SignalingEvent (13)
message NotifySignalingEvent {
uint32 event_type = 1;
uint64 room_id = 2;
uint32 member_id = 3;
uint32 conn_id = 4;
uint32 ctx_id = 5;
}
// NotificationType::RoomDataInternalUpdated (15)
message NotifyRoomDataInternalUpdated {
uint64 room_id = 1;
uint32 flags = 2;
repeated MatchingBinAttr bin_attrs = 3;
uint32 ctx_id = 4;
}
// NotificationType::KickedOut (16)
message NotifyKickedOut {
uint64 room_id = 1;
int32 status_code = 2;
uint32 guard_value = 3;
uint32 ctx_id = 4;
// NotificationType::RoomEvent (10) every room event maps onto
// OrbisNpMatching2RoomEventCallback(ctxId, roomId, event, data). The Orbis
// event id (0x11xx) is carried in `event`; the data shape depends on it:
// member events fill `member`; room-data-updated fills bin_attrs + flags.
message NotifyRoomEvent {
uint32 ctx_id = 1;
uint64 room_id = 2;
uint32 event = 3;
uint32 event_cause = 4;
int32 error_code = 5;
MatchingRoomMemberData member = 6;
repeated MatchingBinAttr bin_attrs = 7;
uint32 flags = 8;
}