MatchingP11 (#4683)

* MatchingP11

Added room messages, made multiple corrections to events, searches, attrs, and fixed a few titles.  Added handling for shadnet matching.

* clang fix
This commit is contained in:
metr1k-91 2026-07-08 09:29:55 -05:00 committed by GitHub
parent 16d5f9c54c
commit 48f4bf889e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 504 additions and 71 deletions

View File

@ -30,6 +30,11 @@ UPnPClient::~UPnPClient() {
}
void UPnPClient::Start() {
if (!m_p2p_features_enabled.load()) {
LOG_INFO(Lib_Net, "UPNP: skipped because Matching2 is disabled");
return;
}
bool expected = false;
if (!m_started.compare_exchange_strong(expected, true))
return;
@ -37,6 +42,10 @@ void UPnPClient::Start() {
m_thread = std::thread(&UPnPClient::DiscoverThread, this);
}
void UPnPClient::SetP2PFeaturesEnabled(bool enabled) {
m_p2p_features_enabled.store(enabled);
}
bool UPnPClient::WaitReady(int timeout_ms) {
std::unique_lock lock(m_mutex);
m_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), [this] { return m_done.load(); });
@ -106,6 +115,9 @@ void UPnPClient::DiscoverThread() {
}
void UPnPClient::AddMapping(u16 port) {
if (!m_p2p_features_enabled.load())
return;
if (!m_available.load())
return;

View File

@ -21,6 +21,7 @@ public:
// Kick off async IGD discovery. Safe to call multiple times — only the first call has effect.
void Start();
void SetP2PFeaturesEnabled(bool enabled);
// Block until discovery completes or timeout_ms elapses. Returns true if IGD was found.
bool WaitReady(int timeout_ms);
@ -48,6 +49,7 @@ private:
void DiscoverThread();
std::atomic<bool> m_started{false};
std::atomic<bool> m_p2p_features_enabled{true};
std::atomic<bool> m_done{false};
std::atomic<bool> m_available{false};
std::atomic<u32> m_external_ip_nbo{0};

View File

@ -182,9 +182,6 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons
// Seed the current Appear-Offline preference so the login packet carries it (the send
// is suppressed pre-auth; it just caches on the client).
client->SetAppearOffline(m_appear_offline.load());
if (EmulatorSettings.IsUPnPEnabled()) {
Net::UPnPClient::Instance().Start();
}
client->Start(host, port, npid, password, token);
const ShadNet::ShadNetState conn_state = client->WaitForConnection();
@ -206,6 +203,11 @@ 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());
Net::UPnPClient::Instance().SetP2PFeaturesEnabled(client->IsMatching2Enabled());
if (client->IsMatching2Enabled() && EmulatorSettings.IsUPnPEnabled()) {
Net::UPnPClient::Instance().Start();
}
NpMatching2::SetMmShadNetClient(client, host, port);
// Build OrbisNpId
@ -1883,7 +1885,7 @@ static u32 FillRankArrayFromProto(const shadnet::GetScoreResponse& resp,
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 <= 26) {
if (cmd_val >= 100 && cmd_val <= 200) {
NpMatching2::OnMatchingReply(cmd, pkt_id, error, body);
} else {
OnScoreReply(user_id, cmd, pkt_id, error, body);

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
#include <cstdint>
#include <cstring>
#include <memory>
@ -317,11 +316,6 @@ int PS4_SYSV_ABI sceNpMatching2SetDefaultRequestOptParam(
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
LOG_INFO(Lib_NpMatching2,
"SetDefaultRequestOptParam: ctx={} opt={} callback={:#x} arg={} timeout={} appId={}",
ctxId, fmt::ptr(requestOpt), reinterpret_cast<std::uintptr_t>(requestOpt->callback),
fmt::ptr(requestOpt->arg), requestOpt->timeout, requestOpt->appId);
ctx->default_request_callback = requestOpt->callback;
ctx->default_request_callback_arg = requestOpt->arg;
@ -479,9 +473,10 @@ int PS4_SYSV_ABI sceNpMatching2SendRoomMessage(OrbisNpMatching2ContextId ctxId,
}
StoreRequestCallback(ctx, requestOpt);
*requestId = AllocRequestId();
LOG_WARNING(Lib_NpMatching2, "not implemented");
return ORBIS_OK;
const OrbisNpMatching2RequestId reqId = AllocRequestId();
*requestId = reqId;
return MmSendRoomMessage(ctxId, reqId,
*static_cast<OrbisNpMatching2SendRoomMessageRequest*>(request));
}
int PS4_SYSV_ABI sceNpMatching2SetRoomDataExternal(

View File

@ -400,7 +400,6 @@ void* BuildSearchRoomPayload(ContextObject& ctx, const shadnet::SearchRoomReply&
if (!r.owner_npid().empty()) {
auto& npid = p.ext_owner_npids.emplace_back();
npid = Libraries::Np::OrbisNpId{};
SetNpId(npid, r.owner_npid());
dst.ownerNpId = &npid;
}
@ -610,7 +609,6 @@ void* BuildGetRoomDataExternalListPayload(ContextObject& ctx,
if (!r.owner_npid().empty()) {
auto& npid = p.ext_owner_npids.emplace_back();
npid = Libraries::Np::OrbisNpId{};
SetNpId(npid, r.owner_npid());
dst.ownerNpId = &npid;
}
@ -1043,6 +1041,68 @@ void* BuildGetRoomDataInternalPayload(ContextObject& ctx, OrbisNpMatching2RoomId
return p.request_data;
}
void* BuildRoomMessagePayload(CallbackPayload& p, bool a_variant, OrbisNpMatching2CastType castType,
const std::vector<OrbisNpMatching2RoomMemberId>& dstMembers,
const MemberCache* srcMember, const std::vector<u8>& msg) {
p.Reset();
p.room_message_dst = std::make_unique<OrbisNpMatching2RoomMessageDestination>();
*p.room_message_dst = {};
if (castType == ORBIS_NP_MATCHING2_CASTTYPE_UNICAST && !dstMembers.empty()) {
p.room_message_dst->unicastTarget = dstMembers.front();
} else if (castType == ORBIS_NP_MATCHING2_CASTTYPE_MULTICAST && !dstMembers.empty()) {
p.room_message_multicast_members = dstMembers;
p.room_message_dst->multicastTarget.memberId = p.room_message_multicast_members.data();
p.room_message_dst->multicastTarget.memberIdNum = p.room_message_multicast_members.size();
}
p.room_message_data = msg;
void* msg_ptr = p.room_message_data.empty() ? nullptr : p.room_message_data.data();
if (a_variant) {
p.room_message_src_addr = std::make_unique<Libraries::Np::OrbisNpPeerAddressA>();
p.room_message_src_online_id = std::make_unique<Libraries::Np::OrbisNpOnlineId>();
*p.room_message_src_addr = {};
*p.room_message_src_online_id = {};
if (srcMember) {
p.room_message_src_addr->accountId = srcMember->account_id;
p.room_message_src_addr->platform = srcMember->platform;
*p.room_message_src_online_id = srcMember->np_id.handle;
}
p.room_message_info_a = std::make_unique<OrbisNpMatching2RoomMessageInfoA>();
auto& info = *p.room_message_info_a;
info = {};
info.filtered = false;
info.castType = castType;
info.dst = p.room_message_dst.get();
info.srcMember = p.room_message_src_addr.get();
info.srcOnlineId = p.room_message_src_online_id.get();
info.msg = msg_ptr;
info.msgLen = static_cast<u32>(p.room_message_data.size());
p.room_message_callback_data = p.room_message_info_a.get();
return p.room_message_callback_data;
}
p.room_message_src_npid = std::make_unique<Libraries::Np::OrbisNpId>();
*p.room_message_src_npid = {};
if (srcMember) {
*p.room_message_src_npid = srcMember->np_id;
}
p.room_message_info = std::make_unique<OrbisNpMatching2RoomMessageInfo>();
auto& info = *p.room_message_info;
info = {};
info.filtered = false;
info.castType = castType;
info.dst = p.room_message_dst.get();
info.srcMember = p.room_message_src_npid.get();
info.msg = msg_ptr;
info.msgLen = static_cast<u32>(p.room_message_data.size());
p.room_message_callback_data = p.room_message_info.get();
return p.room_message_callback_data;
}
ContextManager& ContextManager::Instance() {
static ContextManager instance;
return instance;

View File

@ -132,6 +132,16 @@ struct CallbackPayload {
std::vector<Libraries::Np::OrbisNpId> ext_owner_npids;
void* request_data = nullptr;
std::unique_ptr<OrbisNpMatching2RoomMessageInfo> room_message_info;
std::unique_ptr<OrbisNpMatching2RoomMessageInfoA> room_message_info_a;
std::unique_ptr<OrbisNpMatching2RoomMessageDestination> room_message_dst;
std::unique_ptr<Libraries::Np::OrbisNpId> room_message_src_npid;
std::unique_ptr<Libraries::Np::OrbisNpPeerAddressA> room_message_src_addr;
std::unique_ptr<Libraries::Np::OrbisNpOnlineId> room_message_src_online_id;
std::vector<OrbisNpMatching2RoomMemberId> room_message_multicast_members;
std::vector<u8> room_message_data;
void* room_message_callback_data = nullptr;
std::unique_ptr<OrbisNpMatching2RoomMemberUpdate> room_member_update;
std::unique_ptr<OrbisNpMatching2RoomMemberUpdateA> room_member_update_a;
std::unique_ptr<OrbisNpMatching2RoomUpdate> room_update;
@ -186,6 +196,15 @@ struct CallbackPayload {
ext_room_groups.clear();
ext_owner_npids.clear();
request_data = nullptr;
room_message_info.reset();
room_message_info_a.reset();
room_message_dst.reset();
room_message_src_npid.reset();
room_message_src_addr.reset();
room_message_src_online_id.reset();
room_message_multicast_members.clear();
room_message_data.clear();
room_message_callback_data = nullptr;
room_member_update.reset();
room_member_update_a.reset();
room_update.reset();
@ -212,9 +231,6 @@ struct ContextObject {
Libraries::Np::OrbisNpId owner_np_id{};
Libraries::Np::OrbisNpOnlineId online_id{};
std::string signaling_addr;
u16 signaling_port = 0;
u64 handler_registration_generation = 1;
OrbisNpMatching2WorldId world_id = 0;
@ -260,8 +276,6 @@ struct ContextObject {
service_label = 0;
owner_np_id = {};
online_id = {};
signaling_addr.clear();
signaling_port = 0;
handler_registration_generation = 1;
world_id = 0;
lobby_id = 0;
@ -414,6 +428,9 @@ void* BuildGetRoomMemberDataExternalListPayloadA(
void* BuildGetUserInfoListPayload(ContextObject& ctx, const shadnet::GetUserInfoListReply& resp);
void* BuildGetUserInfoListPayloadA(ContextObject& ctx, const shadnet::GetUserInfoListReply& resp);
void* BuildGetRoomDataInternalPayload(ContextObject& ctx, OrbisNpMatching2RoomId room_id);
void* BuildRoomMessagePayload(CallbackPayload& p, bool a_variant, OrbisNpMatching2CastType castType,
const std::vector<OrbisNpMatching2RoomMemberId>& dstMembers,
const MemberCache* srcMember, const std::vector<u8>& msg);
void InitEventDispatcher();
void TermEventDispatcher();

View File

@ -13,6 +13,7 @@
#include "common/logging/log.h"
#include "core/libraries/network/net.h"
#include "core/libraries/network/net_upnp.h"
#include "core/libraries/network/sockets.h"
#include "core/libraries/np/np_error.h"
#include "core/libraries/np/np_matching2/np_matching2_internal.h"
@ -40,6 +41,7 @@ struct MmClientState {
std::mutex mutex;
u32 server_addr = 0;
u16 server_udp_port = 0;
bool matching2_enabled = false;
std::mutex pending_mutex;
std::map<u64, PendingRequest> pending;
@ -59,6 +61,16 @@ u32 IpStringToAddr(std::string_view ip) {
return a | (b << 8) | (c << 16) | (d << 24);
}
bool IsMatching2BackendDisabled() {
std::lock_guard lock(g_mm.mutex);
return !g_mm.matching2_enabled;
}
bool IsContextLifecycleEvent(OrbisNpMatching2Event event) {
return event == ORBIS_NP_MATCHING2_CONTEXT_EVENT_STARTED ||
event == ORBIS_NP_MATCHING2_CONTEXT_EVENT_STOPPED;
}
template <typename T>
std::vector<u8> MakeProtoPayload(const T& msg) {
std::string bytes;
@ -339,8 +351,8 @@ void HandleRoomEvent(const ShadNet::NotifyRoomEvent& n) {
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();
auto payload_owner = std::make_shared<CallbackPayload>();
CallbackPayload& p = *payload_owner;
switch (event) {
case ORBIS_NP_MATCHING2_ROOM_EVENT_MEMBER_JOINED: {
@ -553,6 +565,7 @@ void HandleRoomEvent(const ShadNet::NotifyRoomEvent& n) {
ev.room_id = room_id;
ev.room_event = fired_event;
ev.room_event_data = p.room_event_data;
ev.request_payload_owner = std::move(payload_owner);
ScheduleEvent(std::move(ev));
if (event == ORBIS_NP_MATCHING2_ROOM_EVENT_MEMBER_JOINED) {
@ -560,6 +573,57 @@ void HandleRoomEvent(const ShadNet::NotifyRoomEvent& n) {
}
}
void HandleRoomMessage(const ShadNet::NotifyRoomMessage& n) {
ContextObject* ctx =
ContextManager::Instance().Get(static_cast<OrbisNpMatching2ContextId>(n.ctx_id));
if (!ctx) {
return;
}
const auto room_id = static_cast<OrbisNpMatching2RoomId>(n.room_id);
const auto src_member_id = static_cast<OrbisNpMatching2RoomMemberId>(n.src_member_id);
const MemberCache* src_member = nullptr;
const auto room_it = ctx->room_cache.find(room_id);
if (room_it != ctx->room_cache.end()) {
const auto member_it = room_it->second.members.find(src_member_id);
if (member_it != room_it->second.members.end()) {
src_member = &member_it->second;
}
}
MemberCache fallback_src{};
if (!src_member) {
fallback_src.member_id = src_member_id;
SetNpId(fallback_src.np_id, n.src_npid);
fallback_src.account_id = static_cast<Libraries::Np::OrbisNpAccountId>(n.src_account_id);
fallback_src.platform = static_cast<Libraries::Np::OrbisNpPlatformType>(n.src_platform);
src_member = &fallback_src;
}
std::vector<OrbisNpMatching2RoomMemberId> dst_members;
dst_members.reserve(n.dst_member_ids.size());
for (const u32 member_id : n.dst_member_ids) {
dst_members.push_back(static_cast<OrbisNpMatching2RoomMemberId>(member_id));
}
auto payload_owner = std::make_shared<CallbackPayload>();
void* data = BuildRoomMessagePayload(*payload_owner, ctx->a_variant,
static_cast<OrbisNpMatching2CastType>(n.cast_type),
dst_members, src_member, n.msg);
PendingEvent ev{};
ev.type = PendingEvent::ROOM_MESSAGE_CB;
ev.ctx_id = ctx->ctx_id;
ev.fire_at = std::chrono::steady_clock::now();
ev.room_id = room_id;
ev.src_member_id = src_member_id;
ev.msg_event = ctx->a_variant ? ORBIS_NP_MATCHING2_ROOM_MSG_EVENT_MESSAGE_A
: ORBIS_NP_MATCHING2_ROOM_MSG_EVENT_MESSAGE;
ev.message_data = data;
ev.request_payload_owner = std::move(payload_owner);
ScheduleEvent(std::move(ev));
}
} // namespace
void OnMatchingReply(ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
@ -592,21 +656,28 @@ void SetMmShadNetClient(std::shared_ptr<ShadNet::ShadNetClient> client,
std::string_view server_host, u16 tcp_port) {
u32 server_addr = 0;
u16 server_udp_port = 0;
bool matching2_enabled = false;
{
std::lock_guard lock(g_mm.mutex);
g_mm.client = client;
g_mm.matching2_enabled = client ? client->IsMatching2Enabled() : false;
matching2_enabled = g_mm.matching2_enabled;
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));
g_mm.server_udp_port =
matching2_enabled ? Libraries::Net::sceNetHtons(static_cast<u16>(tcp_port + 1)) : 0;
server_addr = g_mm.server_addr;
server_udp_port = g_mm.server_udp_port;
}
LOG_INFO(Lib_NpMatching2, "ShadNet features: matching2_enabled={}", matching2_enabled);
Net::UPnPClient::Instance().SetP2PFeaturesEnabled(matching2_enabled);
if (!client) {
NpSignaling::Stubs::SetTransportHooks({});
NpSignaling::Stubs::SetPeerResolver(nullptr);
NpSignaling::Stubs::SetMatching2Enabled(false);
NpSignaling::Stubs::SetMmServerEndpoint(0, 0);
StopMatching2HandshakeThread();
return;
@ -622,10 +693,16 @@ void SetMmShadNetClient(std::shared_ptr<ShadNet::ShadNetClient> client,
.advertised_addr = Net::GetP2PAdvertisedAddr,
.ensure_transport = Net::EnsureP2PTransport,
});
NpSignaling::Stubs::SetPeerResolver(RequestSignalingInfos);
NpSignaling::Stubs::SetPeerResolver(matching2_enabled ? RequestSignalingInfos : nullptr);
NpSignaling::Stubs::SetMatching2Enabled(matching2_enabled);
NpSignaling::Stubs::SetMmServerEndpoint(server_addr, server_udp_port);
StartMatching2HandshakeThread();
if (matching2_enabled) {
StartMatching2HandshakeThread();
} else {
StopMatching2HandshakeThread();
}
client->onRoomEvent = [](const ShadNet::NotifyRoomEvent& n) { HandleRoomEvent(n); };
client->onRoomMessage = [](const ShadNet::NotifyRoomMessage& n) { HandleRoomMessage(n); };
}
void ClearMmShadNetClient() {
@ -635,12 +712,16 @@ void ClearMmShadNetClient() {
old_client = std::move(g_mm.client);
g_mm.server_addr = 0;
g_mm.server_udp_port = 0;
g_mm.matching2_enabled = false;
}
Net::UPnPClient::Instance().SetP2PFeaturesEnabled(false);
if (old_client) {
old_client->onRoomEvent = nullptr;
old_client->onRoomMessage = nullptr;
}
NpSignaling::Stubs::SetTransportHooks({});
NpSignaling::Stubs::SetPeerResolver(nullptr);
NpSignaling::Stubs::SetMatching2Enabled(false);
NpSignaling::Stubs::SetMmServerEndpoint(0, 0);
StopMatching2HandshakeThread();
{
@ -692,6 +773,48 @@ s32 MmSubmitRequest(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId
return ORBIS_NP_MATCHING2_ERROR_INVALID_CONTEXT_ID;
}
const RequestCallbackInfo request_cb = ConsumeRequestCallback(ctx);
if (IsMatching2BackendDisabled() && !IsContextLifecycleEvent(req_event)) {
LOG_INFO(Lib_NpMatching2,
"MmSubmitRequest: matching2 backend disabled; failing ctx={} reqId={} "
"event={:#x} cmd={}",
ctx_id, req_id, static_cast<u16>(req_event), static_cast<u16>(cmd));
s32 error_code = ORBIS_NP_MATCHING2_ERROR_SERVER_NOT_AVAILABLE;
void* request_data = nullptr;
std::shared_ptr<CallbackPayload> request_payload_owner;
if (req_event == ORBIS_NP_MATCHING2_REQUEST_EVENT_GET_WORLD_INFO_LIST) {
LOG_INFO(Lib_NpMatching2,
"MmSubmitRequest: matching2 backend disabled; returning empty world");
shadnet::GetWorldInfoListReply reply;
auto* world = reply.add_worlds();
world->set_world_id(1);
world->set_lobbies_num(0);
world->set_max_lobby_members(0);
world->set_lobby_members_num(0);
world->set_rooms_num(0);
world->set_room_members_num(0);
request_payload_owner = std::make_shared<CallbackPayload>();
ctx->request_payload_override = request_payload_owner.get();
request_data = BuildGetWorldInfoListPayload(*ctx, reply);
ctx->request_payload_override = nullptr;
error_code = ORBIS_OK;
}
PendingEvent ev{};
ev.type = PendingEvent::REQUEST_CB;
ev.ctx_id = ctx_id;
ev.fire_at = std::chrono::steady_clock::now();
ev.req_id = req_id;
ev.req_event = req_event;
ev.error_code = error_code;
ev.request_cb = request_cb.callback;
ev.request_cb_arg = request_cb.arg;
ev.request_data = request_data;
ev.request_payload_owner = std::move(request_payload_owner);
ScheduleEvent(std::move(ev));
return ORBIS_OK;
}
LOG_INFO(Lib_NpMatching2,
"MmSubmitRequest: ctx={} reqId={} event={:#x} cmd={} aVariant={} callback={:#x} "
"arg={}",
@ -978,6 +1101,30 @@ s32 MmSetUserInfo(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId re
MmCommand::SetUserInfo, MakeProtoPayload(req));
}
s32 MmSendRoomMessage(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SendRoomMessageRequest& request) {
shadnet::SendRoomMessageRequest req;
req.set_room_id(request.roomId);
req.set_cast_type(request.castType);
req.set_option(request.option);
if (request.castType == ORBIS_NP_MATCHING2_CASTTYPE_UNICAST) {
req.add_dst_member_ids(request.dst.unicastTarget);
} else if (request.castType == ORBIS_NP_MATCHING2_CASTTYPE_MULTICAST &&
request.dst.multicastTarget.memberId) {
for (u64 i = 0; i < request.dst.multicastTarget.memberIdNum; ++i) {
req.add_dst_member_ids(request.dst.multicastTarget.memberId[i]);
}
}
if (request.msg && request.msgLen > 0) {
req.set_msg(request.msg, request.msgLen);
}
return MmSubmitRequest(ctx_id, req_id, ORBIS_NP_MATCHING2_REQUEST_EVENT_SEND_ROOM_MESSAGE,
MmCommand::SendRoomMessage, MakeProtoPayload(req));
}
s32 MmSetRoomDataInternal(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SetRoomDataInternalRequest& request) {
shadnet::SetRoomDataInternalRequest req;
@ -1039,6 +1186,9 @@ bool RequestSignalingInfos(std::string_view target_online_id, u32* out_addr, u16
if (!out_addr || !out_port) {
return false;
}
if (IsMatching2BackendDisabled()) {
return false;
}
std::shared_ptr<ShadNet::ShadNetClient> client;
{
std::lock_guard lock(g_mm.mutex);

View File

@ -19,21 +19,22 @@ enum class ErrorType : u8;
namespace Libraries::Np::NpMatching2 {
enum class MmCommand : u16 {
ContextStart = 12,
CreateRoom = 13,
JoinRoom = 14,
LeaveRoom = 15,
SearchRoom = 16,
RequestSignalingInfos = 17,
ContextStop = 18,
SetUserInfo = 19,
SetRoomDataInternal = 20,
SetRoomDataExternal = 21,
KickoutRoomMember = 22,
GetWorldInfoList = 23,
GetRoomDataExternalList = 24,
GetUserInfoList = 25,
GetRoomMemberDataExternalList = 26,
ContextStart = 100,
CreateRoom = 101,
JoinRoom = 102,
LeaveRoom = 103,
SearchRoom = 104,
RequestSignalingInfos = 105,
ContextStop = 106,
SetUserInfo = 107,
SetRoomDataInternal = 108,
SetRoomDataExternal = 109,
KickoutRoomMember = 110,
GetWorldInfoList = 111,
GetRoomDataExternalList = 112,
GetUserInfoList = 113,
GetRoomMemberDataExternalList = 114,
SendRoomMessage = 115,
};
void SetMmShadNetClient(std::shared_ptr<ShadNet::ShadNetClient> client,
@ -76,6 +77,8 @@ s32 MmGetUserInfoList(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestI
bool a_variant = false);
s32 MmSetUserInfo(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SetUserInfoRequest& request);
s32 MmSendRoomMessage(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SendRoomMessageRequest& request);
s32 MmSetRoomDataInternal(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SetRoomDataInternalRequest& request);
s32 MmSetRoomDataExternal(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,

View File

@ -311,7 +311,7 @@ void Matching2HandshakeThreadMain() {
now - peer.last_send > kMatching2HandshakeTimeout) {
peer.status = kMatching2ConnInactive;
QueueMatching2SignalingEvent(*ctx, ctx->room_id, member_id,
ORBIS_NP_MATCHING2_SIGNALING_EVENT_NETINFO_ERROR,
ORBIS_NP_MATCHING2_SIGNALING_EVENT_DEAD,
ORBIS_NP_MATCHING2_SIGNALING_ERROR_TIMEOUT);
continue;
}
@ -335,6 +335,9 @@ void Matching2HandshakeThreadMain() {
} // namespace
bool SendMatching2StunPing(const ContextObject& ctx) {
if (!NpSignaling::Stubs::Matching2Enabled()) {
return false;
}
if (!ctx.started || ctx.online_id.data[0] == '\0') {
return false;
}

View File

@ -390,19 +390,21 @@ static void PingThreadMain() {
}
}
const u32 server_addr = Stubs::MmServerAddr();
const u16 server_port = Stubs::MmServerUdpPort();
if (Stubs::Matching2Enabled()) {
const u32 server_addr = Stubs::MmServerAddr();
const u16 server_port = Stubs::MmServerUdpPort();
for (const auto& cs : contexts) {
if (server_addr == 0 || server_port == 0) {
break;
for (const auto& cs : contexts) {
if (server_addr == 0 || server_port == 0) {
break;
}
StunPing ping{};
ping.cmd = 0x01;
std::memcpy(ping.online_id, cs.online_id.data, ORBIS_NP_ONLINEID_MAX_LENGTH);
ping.local_ip = Stubs::AdvertisedAddr();
Stubs::SignalingSendTo(&ping, sizeof(ping), server_addr, server_port);
}
StunPing ping{};
ping.cmd = 0x01;
std::memcpy(ping.online_id, cs.online_id.data, ORBIS_NP_ONLINEID_MAX_LENGTH);
ping.local_ip = Stubs::AdvertisedAddr();
Stubs::SignalingSendTo(&ping, sizeof(ping), server_addr, server_port);
}
SendEchoPings();

View File

@ -1304,6 +1304,10 @@ void HandleControlPacket(u32 from_addr, u16 from_port, const SignalingControl& p
}
void SendStunPing(s32 ctx_id) {
if (!Stubs::Matching2Enabled()) {
return;
}
OrbisNpOnlineId online_id{};
{

View File

@ -8,6 +8,7 @@ namespace Libraries::Np::NpSignaling::Stubs {
namespace {
TransportHooks g_transport{};
PeerResolver g_peer_resolver = nullptr;
bool g_matching2_enabled = false;
u32 g_mm_server_addr = 0;
u16 g_mm_server_udp_port = 0;
} // namespace
@ -55,6 +56,10 @@ void SetPeerResolver(PeerResolver fn) {
g_peer_resolver = fn;
}
void SetMatching2Enabled(bool enabled) {
g_matching2_enabled = enabled;
}
void SetMmServerEndpoint(u32 addr, u16 udp_port) {
g_mm_server_addr = addr;
g_mm_server_udp_port = udp_port;
@ -64,6 +69,10 @@ bool ResolvePeer(std::string_view online_id, u32* out_addr, u16* out_port) {
return g_peer_resolver ? g_peer_resolver(online_id, out_addr, out_port) : false;
}
bool Matching2Enabled() {
return g_matching2_enabled;
}
u32 MmServerAddr() {
return g_mm_server_addr;
}

View File

@ -31,9 +31,11 @@ bool EnsureTransport();
using PeerResolver = bool (*)(std::string_view online_id, u32* out_addr, u16* out_port);
void SetPeerResolver(PeerResolver fn);
void SetMatching2Enabled(bool enabled);
void SetMmServerEndpoint(u32 addr, u16 udp_port);
bool ResolvePeer(std::string_view online_id, u32* out_addr, u16* out_port);
bool Matching2Enabled();
u32 MmServerAddr();
u16 MmServerUdpPort();

View File

@ -42,6 +42,14 @@ using OrbisNpMatching2SignalingRequestId = u32;
using OrbisNpMatching2TeamId = u8;
using OrbisNpMatching2WorldId = u32;
using OrbisNpMatching2CastType = u8;
enum : OrbisNpMatching2CastType {
ORBIS_NP_MATCHING2_CASTTYPE_BROADCAST = 1,
ORBIS_NP_MATCHING2_CASTTYPE_UNICAST = 2,
ORBIS_NP_MATCHING2_CASTTYPE_MULTICAST = 3,
};
// Event of request/room/signaling/context functions
enum OrbisNpMatching2Event : u16 {
ORBIS_NP_MATCHING2_REQUEST_EVENT_GET_WORLD_INFO_LIST = 0x0002,
@ -83,6 +91,7 @@ enum OrbisNpMatching2Event : u16 {
ORBIS_NP_MATCHING2_ROOM_EVENT_MEMBER_JOINED_A = 0x8101,
ORBIS_NP_MATCHING2_ROOM_EVENT_MEMBER_LEFT_A = 0x8102,
ORBIS_NP_MATCHING2_ROOM_EVENT_UPDATED_ROOM_MEMBER_DATA_INTERNAL_A = 0x8107,
ORBIS_NP_MATCHING2_ROOM_MSG_EVENT_MESSAGE = 0x2102,
ORBIS_NP_MATCHING2_ROOM_MSG_EVENT_MESSAGE_A = 0x9102,
};
@ -308,6 +317,48 @@ struct OrbisNpMatching2RoomGroupInfo {
u32 groupMembers;
};
union OrbisNpMatching2RoomMessageDestination {
OrbisNpMatching2RoomMemberId unicastTarget;
struct {
OrbisNpMatching2RoomMemberId* memberId;
u64 memberIdNum;
} multicastTarget;
};
static_assert(sizeof(OrbisNpMatching2RoomMessageDestination) == 0x10);
struct OrbisNpMatching2SendRoomMessageRequest {
OrbisNpMatching2RoomId roomId;
OrbisNpMatching2CastType castType;
u8 padding[3];
OrbisNpMatching2RoomMessageDestination dst;
const void* msg;
u32 msgLen;
s32 option;
};
static_assert(sizeof(OrbisNpMatching2SendRoomMessageRequest) == 0x30);
struct OrbisNpMatching2RoomMessageInfo {
bool filtered;
OrbisNpMatching2CastType castType;
u8 padding[2];
OrbisNpMatching2RoomMessageDestination* dst;
Libraries::Np::OrbisNpId* srcMember;
void* msg;
u32 msgLen;
};
struct OrbisNpMatching2RoomMessageInfoA {
bool filtered;
OrbisNpMatching2CastType castType;
u8 padding[2];
OrbisNpMatching2RoomMessageDestination* dst;
Libraries::Np::OrbisNpPeerAddressA* srcMember;
Libraries::Np::OrbisNpOnlineId* srcOnlineId;
void* msg;
u32 msgLen;
};
// Room-internal binary attribute
struct OrbisNpMatching2RoomBinAttrInternal {
Libraries::Rtc::OrbisRtcTick lastUpdate;
@ -781,6 +832,7 @@ struct OrbisNpMatching2JoinedSessionInfo {
};
// User information
// from PS3 this was NpUserInfo2; setting addresses like our other A and nonA types TODO: confirm
struct OrbisNpMatching2UserInfo {
OrbisNpMatching2UserInfo* next;
Libraries::Np::OrbisNpId npId;
@ -791,6 +843,7 @@ struct OrbisNpMatching2UserInfo {
};
// User information (account-id variant)
// from PS3 this was NpUserInfo2; setting addresses like our other A and nonA types TODO: confirm
struct OrbisNpMatching2UserInfoA {
OrbisNpMatching2UserInfoA* next;
Libraries::Np::OrbisNpPeerAddressA user;

View File

@ -137,6 +137,9 @@ u32 ShadNetClient::GetAddrLocal() const {
u32 ShadNetClient::GetAddrServer() const {
return m_addr_server.load();
}
bool ShadNetClient::IsMatching2Enabled() const {
return m_matching2_enabled.load();
}
u32 ShadNetClient::GetNumFriends() const {
std::lock_guard lock(m_mutex_friends);
@ -518,6 +521,19 @@ u64 ShadNetClient::SetAppearOffline(bool enable) {
return SubmitRequest(CommandType::SetAppearOffline, MakeProtoPayload(req));
}
bool ShadNetClient::RequestServerFeatures() {
const u64 pkt_id = m_pkt_counter.fetch_add(1);
std::vector<u8> empty_payload;
std::vector<u8> pkt = BuildPacket(CommandType::GetServerFeatures, pkt_id, empty_payload);
{
std::lock_guard lock(m_mutex_send_queue);
m_send_queue.push_back(std::move(pkt));
}
m_cv_send_queue.notify_one();
LOG_DEBUG(ShadNet, "GetServerFeatures request fired pkt_id={}", pkt_id);
return true;
}
// Packet dispatch
void ShadNetClient::DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id,
@ -531,6 +547,9 @@ void ShadNetClient::DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id,
case CommandType::GetToken:
HandleGetTokenReply(payload);
break;
case CommandType::GetServerFeatures:
HandleServerFeaturesReply(payload);
break;
default:
if (onAsyncReply) {
// Every reply body starts with an ErrorType byte.
@ -648,21 +667,21 @@ void ShadNetClient::HandleLoginReply(const std::vector<u8>& payload) {
void ShadNetClient::HandleGetTokenReply(const std::vector<u8>& payload) {
if (payload.empty()) {
LOG_ERROR(ShadNet, "Empty GetToken reply");
m_sem_authenticated.release();
RequestServerFeatures();
return;
}
const ErrorType err = static_cast<ErrorType>(payload[0]);
if (err != ErrorType::NoError) {
LOG_WARNING(ShadNet, "GetToken returned error={} — WebAPI calls will be unauthenticated",
static_cast<int>(err));
m_sem_authenticated.release();
RequestServerFeatures();
return;
}
shadnet::GetTokenReply pb;
const std::string blob = ExtractBlob(payload, 1);
if (blob.empty() || !pb.ParseFromString(blob)) {
LOG_ERROR(ShadNet, "Failed to parse GetTokenReply proto");
m_sem_authenticated.release();
RequestServerFeatures();
return;
}
{
@ -671,6 +690,33 @@ void ShadNetClient::HandleGetTokenReply(const std::vector<u8>& payload) {
}
LOG_INFO(ShadNet, "Bearer token captured ({} chars) for accountID={} canonical npid='{}'",
pb.token().size(), pb.user_id(), pb.npid());
RequestServerFeatures();
}
void ShadNetClient::HandleServerFeaturesReply(const std::vector<u8>& payload) {
bool matching2_enabled = false;
bool parsed = false;
if (!payload.empty()) {
const ErrorType err = static_cast<ErrorType>(payload[0]);
if (err == ErrorType::NoError) {
shadnet::ServerFeaturesReply pb;
const std::string blob = ExtractBlob(payload, 1);
if (!blob.empty() && pb.ParseFromString(blob)) {
matching2_enabled = pb.matching2_enabled();
parsed = true;
}
} else {
LOG_WARNING(ShadNet,
"GetServerFeatures returned error={} - assuming Matching2 disabled",
static_cast<int>(err));
}
}
m_matching2_enabled.store(matching2_enabled);
m_server_features_received.store(parsed);
LOG_INFO(ShadNet, "Server features: matching2_enabled={}{}",
matching2_enabled ? "true" : "false", parsed ? "" : " (defaulted)");
m_sem_authenticated.release();
}
@ -795,6 +841,28 @@ void ShadNetClient::HandleNotification(u16 cmd_raw, const std::vector<u8>& paylo
onRoomEvent(n);
break;
}
case NotificationType::RoomMessage: {
shadnet::NotifyRoomMessage pb;
if (!pb.ParseFromString(blob)) {
break;
}
NotifyRoomMessage n;
n.ctx_id = pb.ctx_id();
n.room_id = pb.room_id();
n.src_member_id = pb.src_member_id();
n.event = pb.event();
n.cast_type = pb.cast_type();
for (const u32 member_id : pb.dst_member_ids()) {
n.dst_member_ids.push_back(member_id);
}
n.src_npid = pb.src_npid();
n.src_account_id = pb.src_account_id();
n.src_platform = pb.src_platform();
n.msg.assign(pb.msg().begin(), pb.msg().end());
if (onRoomMessage)
onRoomMessage(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,22 +66,7 @@ enum class CommandType : u16 {
RemoveFriend = 9,
AddBlock = 10,
RemoveBlock = 11,
// Matchmaking
ContextStart = 12,
CreateRoom = 13,
JoinRoom = 14,
LeaveRoom = 15,
SearchRoom = 16,
RequestSignalingInfos = 17,
ContextStop = 18,
SetUserInfo = 19,
SetRoomDataInternal = 20,
SetRoomDataExternal = 21,
KickoutRoomMember = 22,
GetWorldInfoList = 23,
GetRoomDataExternalList = 24,
GetUserInfoList = 25,
GetRoomMemberDataExternalList = 26,
GetServerFeatures = 12,
GetBoardInfos = 30,
RecordScore = 31,
RecordScoreData = 32,
@ -93,6 +78,23 @@ enum class CommandType : u16 {
GetScoreGameDataByAccId = 38,
GetToken = 39,
SetAppearOffline = 40,
// Matchmaking
ContextStart = 100,
CreateRoom = 101,
JoinRoom = 102,
LeaveRoom = 103,
SearchRoom = 104,
RequestSignalingInfos = 105,
ContextStop = 106,
SetUserInfo = 107,
SetRoomDataInternal = 108,
SetRoomDataExternal = 109,
KickoutRoomMember = 110,
GetWorldInfoList = 111,
GetRoomDataExternalList = 112,
GetUserInfoList = 113,
GetRoomMemberDataExternalList = 114,
SendRoomMessage = 115,
};
enum class NotificationType : u16 {
@ -101,6 +103,7 @@ enum class NotificationType : u16 {
FriendLost = 7,
FriendStatus = 8,
RoomEvent = 10,
RoomMessage = 11,
WebApiPushEvent = 17, // Generic NP WebApi push event
};
@ -234,6 +237,19 @@ struct NotifyRoomEvent {
std::vector<MatchingBinAttr> bin_attrs;
};
struct NotifyRoomMessage {
u32 ctx_id = 0;
u64 room_id = 0;
u32 src_member_id = 0;
u32 event = 0;
u32 cast_type = 0;
std::vector<u32> dst_member_ids;
std::string src_npid;
u64 src_account_id = 0;
u32 src_platform = 0;
std::vector<u8> msg;
};
// ShadNetClient
class ShadNetClient {
@ -258,6 +274,7 @@ public:
u64 GetUserId() const;
u32 GetAddrLocal() const;
u32 GetAddrServer() const;
bool IsMatching2Enabled() const;
u32 GetNumFriends() const;
std::optional<std::string> GetFriendNpid(u32 index) const;
@ -270,6 +287,7 @@ public:
std::function<void(const NotifyFriendLost&)> onFriendLost;
std::function<void(const NotifyFriendStatus&)> onFriendStatus;
std::function<void(const NotifyRoomEvent&)> onRoomEvent;
std::function<void(const NotifyRoomMessage&)> onRoomMessage;
std::function<void(const NotifyWebApiPushEvent&)> onWebApiPushEvent;
// Async reply callback.
// cmd —command this reply is for (matches the request's cmd)
@ -304,7 +322,9 @@ private:
void DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id, const std::vector<u8>& payload);
void HandleLoginReply(const std::vector<u8>& payload);
void HandleGetTokenReply(const std::vector<u8>& payload);
void HandleServerFeaturesReply(const std::vector<u8>& payload);
void HandleNotification(u16 cmd_raw, const std::vector<u8>& payload);
bool RequestServerFeatures();
// Helper: read a u32-LE-prefixed proto blob from a byte vector at pos.
static std::string ExtractBlob(const std::vector<u8>& p, int pos);
@ -349,6 +369,8 @@ private:
std::string m_bearer_token;
std::atomic<u32> m_addr_local{0};
std::atomic<u32> m_addr_server{0};
std::atomic<bool> m_matching2_enabled{false};
std::atomic<bool> m_server_features_received{false};
mutable std::mutex m_mutex_friends;
std::vector<FriendEntry> m_friends;

View File

@ -29,6 +29,10 @@ message SetAppearOfflineRequest {
bool appear_offline = 1;
}
message ServerFeaturesReply {
bool matching2_enabled = 1;
}
message FriendEntry {
string npid = 1;
bool online = 2;
@ -514,6 +518,16 @@ message SetRoomDataInternalReply { uint64 room_id = 1; }
message SetRoomDataExternalReply { uint64 room_id = 1; }
message KickoutRoomMemberReply { uint64 room_id = 1; }
message SendRoomMessageRequest {
uint64 room_id = 1;
uint32 cast_type = 2;
repeated uint32 dst_member_ids = 3;
bytes msg = 4;
int32 option = 5;
}
message SendRoomMessageReply { 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 ContextStartRequest).
@ -534,3 +548,18 @@ message NotifyRoomEvent {
bool has_passwd_mask = 9;
uint64 passwd_slot_mask = 10;
}
// NotificationType::RoomMessage (11) maps onto
// OrbisNpMatching2RoomMessageCallback(ctxId, roomId, srcMemberId, event, data).
message NotifyRoomMessage {
uint32 ctx_id = 1;
uint64 room_id = 2;
uint32 src_member_id = 3;
uint32 event = 4;
uint32 cast_type = 5;
repeated uint32 dst_member_ids = 6;
string src_npid = 7;
uint64 src_account_id = 8;
uint32 src_platform = 9;
bytes msg = 10;
}