diff --git a/src/core/libraries/network/net_upnp.cpp b/src/core/libraries/network/net_upnp.cpp index de54cc830..a014737b4 100644 --- a/src/core/libraries/network/net_upnp.cpp +++ b/src/core/libraries/network/net_upnp.cpp @@ -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; diff --git a/src/core/libraries/network/net_upnp.h b/src/core/libraries/network/net_upnp.h index ffc2c6c4c..6fdc92409 100644 --- a/src/core/libraries/network/net_upnp.h +++ b/src/core/libraries/network/net_upnp.h @@ -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 m_started{false}; + std::atomic m_p2p_features_enabled{true}; std::atomic m_done{false}; std::atomic m_available{false}; std::atomic m_external_ip_nbo{0}; diff --git a/src/core/libraries/np/np_handler.cpp b/src/core/libraries/np/np_handler.cpp index 3fc848d77..ba218ae15 100644 --- a/src/core/libraries/np/np_handler.cpp +++ b/src/core/libraries/np/np_handler.cpp @@ -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& body) { const auto cmd_val = static_cast(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); diff --git a/src/core/libraries/np/np_matching2/np_matching2.cpp b/src/core/libraries/np/np_matching2/np_matching2.cpp index 109ddfc31..7e99f3cda 100644 --- a/src/core/libraries/np/np_matching2/np_matching2.cpp +++ b/src/core/libraries/np/np_matching2/np_matching2.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include #include #include @@ -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(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(request)); } int PS4_SYSV_ABI sceNpMatching2SetRoomDataExternal( diff --git a/src/core/libraries/np/np_matching2/np_matching2_internal.cpp b/src/core/libraries/np/np_matching2/np_matching2_internal.cpp index 3fd58cd93..34f20a244 100644 --- a/src/core/libraries/np/np_matching2/np_matching2_internal.cpp +++ b/src/core/libraries/np/np_matching2/np_matching2_internal.cpp @@ -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& dstMembers, + const MemberCache* srcMember, const std::vector& msg) { + p.Reset(); + + p.room_message_dst = std::make_unique(); + *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(); + p.room_message_src_online_id = std::make_unique(); + *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(); + 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(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(); + *p.room_message_src_npid = {}; + if (srcMember) { + *p.room_message_src_npid = srcMember->np_id; + } + + p.room_message_info = std::make_unique(); + 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(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; diff --git a/src/core/libraries/np/np_matching2/np_matching2_internal.h b/src/core/libraries/np/np_matching2/np_matching2_internal.h index 3c76ed4a0..3e25b9437 100644 --- a/src/core/libraries/np/np_matching2/np_matching2_internal.h +++ b/src/core/libraries/np/np_matching2/np_matching2_internal.h @@ -132,6 +132,16 @@ struct CallbackPayload { std::vector ext_owner_npids; void* request_data = nullptr; + std::unique_ptr room_message_info; + std::unique_ptr room_message_info_a; + std::unique_ptr room_message_dst; + std::unique_ptr room_message_src_npid; + std::unique_ptr room_message_src_addr; + std::unique_ptr room_message_src_online_id; + std::vector room_message_multicast_members; + std::vector room_message_data; + void* room_message_callback_data = nullptr; + std::unique_ptr room_member_update; std::unique_ptr room_member_update_a; std::unique_ptr 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& dstMembers, + const MemberCache* srcMember, const std::vector& msg); void InitEventDispatcher(); void TermEventDispatcher(); diff --git a/src/core/libraries/np/np_matching2/np_matching2_mm.cpp b/src/core/libraries/np/np_matching2/np_matching2_mm.cpp index db936cd34..94882fde4 100644 --- a/src/core/libraries/np/np_matching2/np_matching2_mm.cpp +++ b/src/core/libraries/np/np_matching2/np_matching2_mm.cpp @@ -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 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 std::vector MakeProtoPayload(const T& msg) { std::string bytes; @@ -339,8 +351,8 @@ void HandleRoomEvent(const ShadNet::NotifyRoomEvent& n) { const auto room_id = static_cast(n.room_id); const auto member_id = static_cast(n.member_id); - CallbackPayload& p = ctx->room_event_payload; - p.Reset(); + auto payload_owner = std::make_shared(); + 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(n.ctx_id)); + if (!ctx) { + return; + } + + const auto room_id = static_cast(n.room_id); + const auto src_member_id = static_cast(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(n.src_account_id); + fallback_src.platform = static_cast(n.src_platform); + src_member = &fallback_src; + } + + std::vector 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(member_id)); + } + + auto payload_owner = std::make_shared(); + void* data = BuildRoomMessagePayload(*payload_owner, ctx->a_variant, + static_cast(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 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(tcp_port + 1)); + g_mm.server_udp_port = + matching2_enabled ? Libraries::Net::sceNetHtons(static_cast(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 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(req_event), static_cast(cmd)); + + s32 error_code = ORBIS_NP_MATCHING2_ERROR_SERVER_NOT_AVAILABLE; + void* request_data = nullptr; + std::shared_ptr 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(); + 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 client; { std::lock_guard lock(g_mm.mutex); diff --git a/src/core/libraries/np/np_matching2/np_matching2_mm.h b/src/core/libraries/np/np_matching2/np_matching2_mm.h index 10e6ebdab..b74707102 100644 --- a/src/core/libraries/np/np_matching2/np_matching2_mm.h +++ b/src/core/libraries/np/np_matching2/np_matching2_mm.h @@ -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 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, diff --git a/src/core/libraries/np/np_matching2/np_matching2_signaling.cpp b/src/core/libraries/np/np_matching2/np_matching2_signaling.cpp index 73cea0506..ee8f33886 100644 --- a/src/core/libraries/np/np_matching2/np_matching2_signaling.cpp +++ b/src/core/libraries/np/np_matching2/np_matching2_signaling.cpp @@ -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; } diff --git a/src/core/libraries/np/np_signaling/np_signaling_helpers.cpp b/src/core/libraries/np/np_signaling/np_signaling_helpers.cpp index 651d357db..59a284942 100644 --- a/src/core/libraries/np/np_signaling/np_signaling_helpers.cpp +++ b/src/core/libraries/np/np_signaling/np_signaling_helpers.cpp @@ -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(); diff --git a/src/core/libraries/np/np_signaling/np_signaling_state.cpp b/src/core/libraries/np/np_signaling/np_signaling_state.cpp index 96cbaf5e1..948e602c4 100644 --- a/src/core/libraries/np/np_signaling/np_signaling_state.cpp +++ b/src/core/libraries/np/np_signaling/np_signaling_state.cpp @@ -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{}; { diff --git a/src/core/libraries/np/np_signaling/np_signaling_stubs.cpp b/src/core/libraries/np/np_signaling/np_signaling_stubs.cpp index 981b97ec6..bb2ce65f7 100644 --- a/src/core/libraries/np/np_signaling/np_signaling_stubs.cpp +++ b/src/core/libraries/np/np_signaling/np_signaling_stubs.cpp @@ -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; } diff --git a/src/core/libraries/np/np_signaling/np_signaling_stubs.h b/src/core/libraries/np/np_signaling/np_signaling_stubs.h index 63067abc6..a724b29af 100644 --- a/src/core/libraries/np/np_signaling/np_signaling_stubs.h +++ b/src/core/libraries/np/np_signaling/np_signaling_stubs.h @@ -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(); diff --git a/src/core/libraries/np/np_types2.h b/src/core/libraries/np/np_types2.h index 314da4573..cece77d3d 100644 --- a/src/core/libraries/np/np_types2.h +++ b/src/core/libraries/np/np_types2.h @@ -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; diff --git a/src/shadnet/client.cpp b/src/shadnet/client.cpp index 5a73af63d..4075f9a30 100644 --- a/src/shadnet/client.cpp +++ b/src/shadnet/client.cpp @@ -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 empty_payload; + std::vector 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& payload) { void ShadNetClient::HandleGetTokenReply(const std::vector& payload) { if (payload.empty()) { LOG_ERROR(ShadNet, "Empty GetToken reply"); - m_sem_authenticated.release(); + RequestServerFeatures(); return; } const ErrorType err = static_cast(payload[0]); if (err != ErrorType::NoError) { LOG_WARNING(ShadNet, "GetToken returned error={} — WebAPI calls will be unauthenticated", static_cast(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& 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& payload) { + bool matching2_enabled = false; + bool parsed = false; + + if (!payload.empty()) { + const ErrorType err = static_cast(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(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& 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 diff --git a/src/shadnet/client.h b/src/shadnet/client.h index 8d537ea76..fc438ce66 100644 --- a/src/shadnet/client.h +++ b/src/shadnet/client.h @@ -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 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 dst_member_ids; + std::string src_npid; + u64 src_account_id = 0; + u32 src_platform = 0; + std::vector 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 GetFriendNpid(u32 index) const; @@ -270,6 +287,7 @@ public: std::function onFriendLost; std::function onFriendStatus; std::function onRoomEvent; + std::function onRoomMessage; std::function 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& payload); void HandleLoginReply(const std::vector& payload); void HandleGetTokenReply(const std::vector& payload); + void HandleServerFeaturesReply(const std::vector& payload); void HandleNotification(u16 cmd_raw, const std::vector& payload); + bool RequestServerFeatures(); // Helper: read a u32-LE-prefixed proto blob from a byte vector at pos. static std::string ExtractBlob(const std::vector& p, int pos); @@ -349,6 +369,8 @@ private: std::string m_bearer_token; std::atomic m_addr_local{0}; std::atomic m_addr_server{0}; + std::atomic m_matching2_enabled{false}; + std::atomic m_server_features_received{false}; mutable std::mutex m_mutex_friends; std::vector m_friends; diff --git a/src/shadnet/shadnet.proto b/src/shadnet/shadnet.proto index 5ee0621ba..48725fd07 100644 --- a/src/shadnet/shadnet.proto +++ b/src/shadnet/shadnet.proto @@ -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; +}