From 81be949167d0060b08f26dc3e055a0b640d129c8 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 10 Oct 2026 00:51:03 +0300 Subject: [PATCH] spilt np_handler to multiple files based on it purpose. (#5333) * spilt np_handler to multiple files based on it purpose . No functionality added * qos --- CMakeLists.txt | 10 +- .../invitation_dialog/invitation_dialog.cpp | 2 +- .../invitation_dialog_ui.cpp | 2 +- .../invitation_dialog/invitation_dialog_ui.h | 2 +- src/core/libraries/np/np_handler.cpp | 3619 ----------------- .../libraries/np/np_handler/np_handler.cpp | 754 ++++ .../np/{ => np_handler}/np_handler.h | 2 + .../np/np_handler/np_handler_friends.cpp | 232 ++ .../np/np_handler/np_handler_invitation.cpp | 221 + .../np/np_handler/np_handler_score.cpp | 1202 ++++++ .../np/np_handler/np_handler_trophy.cpp | 149 + .../np/np_handler/np_handler_tus.cpp | 1022 +++++ .../np/np_handler/np_handler_web_api.cpp | 87 + src/core/libraries/np/np_manager.cpp | 2 +- .../np/np_matching2/np_matching2.cpp | 2 +- .../np/np_matching2/np_matching2_internal.cpp | 2 +- .../np/np_matching2/np_matching2_mm.cpp | 2 +- .../np_matching2/np_matching2_signaling.cpp | 2 +- src/core/libraries/np/np_score/np_score.cpp | 2 +- .../np/np_signaling/np_signaling.cpp | 2 +- .../np/np_signaling/np_signaling_helpers.cpp | 2 +- .../np/np_signaling/np_signaling_state.cpp | 2 +- src/core/libraries/np/np_trophy.cpp | 2 +- src/core/libraries/np/np_tus/np_tus.cpp | 2 +- .../libraries/np/np_utility/np_utility.cpp | 2 +- .../np/np_web_api/np_web_api_internal.cpp | 2 +- .../np/np_web_api2/np_web_api2_context.cpp | 2 +- src/core/libraries/np/signaling_handler.cpp | 2 +- src/core/libraries/system/userservice.cpp | 2 +- src/emulator.cpp | 2 +- src/imgui/friends_layer.cpp | 2 +- src/imgui/invitation_prompt_layer.cpp | 2 +- 32 files changed, 3699 insertions(+), 3643 deletions(-) delete mode 100644 src/core/libraries/np/np_handler.cpp create mode 100644 src/core/libraries/np/np_handler/np_handler.cpp rename src/core/libraries/np/{ => np_handler}/np_handler.h (99%) create mode 100644 src/core/libraries/np/np_handler/np_handler_friends.cpp create mode 100644 src/core/libraries/np/np_handler/np_handler_invitation.cpp create mode 100644 src/core/libraries/np/np_handler/np_handler_score.cpp create mode 100644 src/core/libraries/np/np_handler/np_handler_trophy.cpp create mode 100644 src/core/libraries/np/np_handler/np_handler_tus.cpp create mode 100644 src/core/libraries/np/np_handler/np_handler_web_api.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 67a5c634b..476069198 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -647,8 +647,14 @@ set(NP_LIBS src/core/libraries/np/np_error.h src/core/libraries/np/np_partner.cpp src/core/libraries/np/np_partner.h src/core/libraries/np/object_manager.h - src/core/libraries/np/np_handler.cpp - src/core/libraries/np/np_handler.h + src/core/libraries/np/np_handler/np_handler.cpp + src/core/libraries/np/np_handler/np_handler.h + src/core/libraries/np/np_handler/np_handler_friends.cpp + src/core/libraries/np/np_handler/np_handler_invitation.cpp + src/core/libraries/np/np_handler/np_handler_score.cpp + src/core/libraries/np/np_handler/np_handler_trophy.cpp + src/core/libraries/np/np_handler/np_handler_tus.cpp + src/core/libraries/np/np_handler/np_handler_web_api.cpp src/core/libraries/np/signaling_handler.cpp src/core/libraries/np/signaling_handler.h src/core/libraries/np/np_commerce/np_commerce.cpp diff --git a/src/core/libraries/invitation_dialog/invitation_dialog.cpp b/src/core/libraries/invitation_dialog/invitation_dialog.cpp index 29c9fa32b..0425fd6cd 100644 --- a/src/core/libraries/invitation_dialog/invitation_dialog.cpp +++ b/src/core/libraries/invitation_dialog/invitation_dialog.cpp @@ -9,7 +9,7 @@ #include "core/libraries/error_codes.h" #include "core/libraries/invitation_dialog/invitation_dialog.h" #include "core/libraries/libs.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "invitation_dialog_ui.h" namespace Libraries::InvitationDialog { diff --git a/src/core/libraries/invitation_dialog/invitation_dialog_ui.cpp b/src/core/libraries/invitation_dialog/invitation_dialog_ui.cpp index 32b33834e..6cf7de456 100644 --- a/src/core/libraries/invitation_dialog/invitation_dialog_ui.cpp +++ b/src/core/libraries/invitation_dialog/invitation_dialog_ui.cpp @@ -14,7 +14,7 @@ #include "core/libraries/net/net.h" #include "core/libraries/network/http.h" #include "core/libraries/network/ssl.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_web_api/np_web_api.h" #include "core/libraries/system/commondialog.h" #include "imgui/imgui_std.h" diff --git a/src/core/libraries/invitation_dialog/invitation_dialog_ui.h b/src/core/libraries/invitation_dialog/invitation_dialog_ui.h index f5f3ba992..fd3c3a33c 100644 --- a/src/core/libraries/invitation_dialog/invitation_dialog_ui.h +++ b/src/core/libraries/invitation_dialog/invitation_dialog_ui.h @@ -9,7 +9,7 @@ #include #include "core/libraries/invitation_dialog/invitation_dialog.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/system/commondialog.h" #include "imgui/imgui_layer.h" diff --git a/src/core/libraries/np/np_handler.cpp b/src/core/libraries/np/np_handler.cpp deleted file mode 100644 index 976786d9b..000000000 --- a/src/core/libraries/np/np_handler.cpp +++ /dev/null @@ -1,3619 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project -// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include -#include -#include -#include "common/elf_info.h" -#include "common/logging/log.h" -#include "common/string_util.h" -#include "core/emulator_settings.h" -#include "core/libraries/invitation_dialog/invitation_dialog.h" -#include "core/libraries/net/net.h" -#include "core/libraries/net/net_p2p.h" -#include "core/libraries/net/net_upnp.h" -#include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_manager.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_score/np_score.h" -#include "core/libraries/np/np_web_api/np_web_api.h" -#include "core/libraries/system/systemservice.h" -#include "core/user_settings.h" -#include "imgui/invitation_prompt_layer.h" -#include "imgui/shadnet_notifications_layer.h" -#include "np_handler.h" -#include "shadnet.pb.h" -#include "shadnet/server_probe.h" - -namespace Libraries::Np { - -NpHandler::NpHandler() - : m_matching2_contexts( - std::unique_ptr(new NpMatching2::ContextManager())), - m_matching2_state(std::make_unique()) {} - -NpHandler::~NpHandler() = default; - -NpHandler& NpHandler::GetInstance() { - static NpHandler s_instance; - return s_instance; -} - -NpMatching2::ContextManager& NpHandler::GetMatching2ContextManager() { - return *m_matching2_contexts; -} - -NpMatching2::NpMatching2State& NpHandler::GetMatching2State() { - return *m_matching2_state; -} - -NpHandler::Matching2CacheGuard NpHandler::LockMatching2Cache( - NpMatching2::OrbisNpMatching2ContextId ctx_id) { - std::shared_ptr cache; - { - std::lock_guard lock(m_mutex_matching2_cache); - auto& entry = m_matching2_cache[ctx_id]; - if (!entry) { - entry = std::make_shared(); - } - cache = entry; - } - return Matching2CacheGuard(std::move(cache)); -} - -void NpHandler::ResetMatching2Cache(NpMatching2::OrbisNpMatching2ContextId ctx_id) { - std::lock_guard lock(m_mutex_matching2_cache); - m_matching2_cache.erase(ctx_id); -} - -void NpHandler::ResetMatching2Caches() { - std::lock_guard lock(m_mutex_matching2_cache); - m_matching2_cache.clear(); -} - -std::pair NpHandler::ParseServerAddress() const { - const std::string server_str = EmulatorSettings.GetShadNetServer(); - u16 port = 31313; // default port - if (server_str.empty()) { - LOG_ERROR(NpHandler, - "shadNet server address is empty,set the shadNet server field in settings. " - "Connection will fail."); - return {std::string{}, port}; - } - std::string host = server_str; - const auto colon = server_str.rfind(':'); - if (colon != std::string::npos) { - host = server_str.substr(0, colon); - const std::string port_str = server_str.substr(colon + 1); - try { - port = static_cast(std::stoi(port_str)); - } catch (const std::exception&) { - LOG_WARNING(NpHandler, - "shadNet server port '{}' is not a valid number; using default {}", - port_str, port); - } - } - return {host, port}; -} - -bool NpHandler::ConnectUserById(s32 user_id) { - if (!EmulatorSettings.IsShadNetEnabled()) - return false; - - { - std::lock_guard lock(m_mutex_clients); - if (m_clients.find(user_id) != m_clients.end()) - return false; // already connected - } - - const User* u = UserManagement.GetUserByID(user_id); - if (!u) - return false; - if (!u->shadnet_enabled) { - LOG_DEBUG(NpHandler, "user_id={} ('{}') shadNet disabled,skipping", u->user_id, - u->user_name); - return false; - } - if (u->shadnet_npid.empty() || u->shadnet_password.empty()) { - LOG_WARNING(NpHandler, "user_id={} ('{}') shadNet enabled but credentials missing,skipping", - u->user_id, u->user_name); - return false; - } - - const auto [host, port] = ParseServerAddress(); - return ConnectUser(u->user_id, host, port, u->shadnet_npid, u->shadnet_password, - u->shadnet_token); -} - -void NpHandler::StartWorker() { - bool expected = false; - if (m_worker_running.compare_exchange_strong(expected, true)) { - m_worker_thread = std::thread(&NpHandler::WorkerThread, this); - } -} - -void NpHandler::Initialize() { - if (m_initialized.exchange(true)) { - LOG_WARNING(NpHandler, "Initialize called more than once"); - return; - } - - if (!EmulatorSettings.IsShadNetEnabled()) { - LOG_INFO(NpHandler, "shadNet disabled globally we are in offline mode"); - return; - } - - { - const auto [host, port] = ParseServerAddress(); - const ShadNet::ProbeInfo probe = ShadNet::ProbeServer(host, port); - if (probe.result != ShadNet::ProbeResult::Ok) { - EmulatorSettings.SetShadNetSessionDisabled(true); - switch (probe.result) { - case ShadNet::ProbeResult::VersionMismatch: - LOG_WARNING(NpHandler, - "shadNet server {}:{} protocol version mismatch (server v{}, emulator " - "v{}); disabling shadNet for this run (saved setting unchanged)", - host, port, probe.server_version, ShadNet::SHAD_PROTOCOL_VERSION); - ImGui::ShadNetNotify::Push( - ImGui::ShadNetNotify::Kind::Info, - fmt::format("shadNet protocol version mismatch (server v{}, emulator v{}). " - "Please update shadPS4. Online features are disabled for this " - "session.", - probe.server_version, ShadNet::SHAD_PROTOCOL_VERSION)); - break; - case ShadNet::ProbeResult::ProtocolError: - LOG_WARNING(NpHandler, - "shadNet server {}:{} sent an invalid ServerInfo handshake; disabling " - "shadNet for this run (saved setting unchanged)", - host, port); - ImGui::ShadNetNotify::Push( - ImGui::ShadNetNotify::Kind::Info, - fmt::format("shadNet server ({}:{}) uses an incompatible protocol. Online " - "features are disabled for this session.", - host, port)); - break; - default: // Unreachable - LOG_WARNING(NpHandler, - "shadNet server {}:{} is offline/unreachable; disabling shadNet for " - "this run (saved setting unchanged)", - host, port); - ImGui::ShadNetNotify::Push( - ImGui::ShadNetNotify::Kind::Info, - fmt::format("shadNet server ({}:{}) is offline. Online features are disabled " - "for this session.", - host, port)); - break; - } - return; - } - } - - const auto logged_in = UserManagement.GetLoggedInUsers(); // get all login users - int connected_count = 0; - for (int i = 0; i < Libraries::UserService::ORBIS_USER_SERVICE_MAX_LOGIN_USERS; ++i) { - const User* u = logged_in[i]; - if (!u) - continue; - if (ConnectUserById(u->user_id)) - ++connected_count; - } - - if (connected_count == 0) { - LOG_WARNING(NpHandler, "no users connected to shadNet"); - return; - } - - StartWorker(); -} - -void NpHandler::Shutdown() { - if (!m_initialized.exchange(false)) - return; - - m_worker_running = false; - - // Stop any pending reconnect retries. - { - std::lock_guard lock(m_mutex_clients); - m_reconnect.clear(); - } - - // Collect user IDs to disconnect (avoid holding m_mutex_clients during Stop) - std::vector ids; - { - std::lock_guard lock(m_mutex_clients); - for (auto& [uid, _] : m_clients) - ids.push_back(uid); - } - for (s32 uid : ids) - DisconnectUser(uid); - - if (m_worker_thread.joinable()) - m_worker_thread.join(); - - // P2P goes with NP: closes the UDP port and removes its UPnP forwarding. - Net::StopP2P(); - - LOG_INFO(NpHandler, "Shutdown complete"); -} - -bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, const std::string& npid, - const std::string& password, const std::string& token) { - LOG_INFO(NpHandler, "Connecting user_id={} npid='{}' to {}:{} (timeout {}s)", user_id, npid, - host, port, ShadNet::SHAD_CONNECT_TIMEOUT_MS / 1000); - - auto client = std::make_shared(); - - // Wire per-user notification callbacks - client->onFriendQuery = [this, user_id](const ShadNet::NotifyFriendQuery& n) { - OnFriendQuery(user_id, n); - }; - client->onFriendNew = [this, user_id](const ShadNet::NotifyFriendNew& n) { - OnFriendNew(user_id, n); - }; - client->onFriendLost = [this, user_id](const ShadNet::NotifyFriendLost& n) { - OnFriendLost(user_id, n); - }; - client->onFriendStatus = [this, user_id](const ShadNet::NotifyFriendStatus& n) { - OnFriendStatus(user_id, n); - }; - client->onWebApiPushEvent = [this, user_id](const ShadNet::NotifyWebApiPushEvent& n) { - OnWebApiPushEvent(user_id, n); - }; - client->onAsyncReply = [this, user_id](ShadNet::CommandType cmd, u64 pkt_id, - ShadNet::ErrorType err, const std::vector& body) { - OnAsyncReply(user_id, cmd, pkt_id, err, body); - }; - client->onLoginResult = [this, user_id](const ShadNet::LoginResult& res) { - OnLoginResult(user_id, res); - }; - - // 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()); - client->Start(host, port, npid, password, token); - - // Shared handling for an incompatible-protocol failure (version mismatch or - // corrupt/unparseable stream): tell the user and disable shadNet for this run, - // since reconnect attempts against an incompatible server are pointless. - const auto handle_protocol_mismatch = [&client](ShadNet::ShadNetState st) { - if (st != ShadNet::ShadNetState::FailureProtocol) - return; - const u32 server_ver = client->GetServerProtocolVersion(); - EmulatorSettings.SetShadNetSessionDisabled(true); - if (server_ver != 0) { - LOG_ERROR(NpHandler, - "shadNet protocol version mismatch (server v{}, emulator v{}); disabling " - "shadNet for this run (saved setting unchanged)", - server_ver, ShadNet::SHAD_PROTOCOL_VERSION); - ImGui::ShadNetNotify::Push( - ImGui::ShadNetNotify::Kind::Info, - fmt::format("shadNet protocol version mismatch (server v{}, emulator v{}). " - "Please update shadPS4. Online features are disabled for this " - "session.", - server_ver, ShadNet::SHAD_PROTOCOL_VERSION)); - } else { - LOG_ERROR(NpHandler, "shadNet protocol error during handshake; disabling shadNet for " - "this run (saved setting unchanged)"); - ImGui::ShadNetNotify::Push( - ImGui::ShadNetNotify::Kind::Info, - "shadNet server uses an incompatible protocol. Online features are disabled " - "for this session."); - } - }; - - const ShadNet::ShadNetState conn_state = client->WaitForConnection(); - if (conn_state != ShadNet::ShadNetState::Ok) { - LOG_ERROR(NpHandler, "user_id={} connection failed (state={})", user_id, - static_cast(conn_state)); - handle_protocol_mismatch(conn_state); - client->Stop(); - return false; - } - - const ShadNet::ShadNetState auth_state = client->WaitForAuthenticated(); - if (auth_state != ShadNet::ShadNetState::Ok) { - LOG_ERROR(NpHandler, "user_id={} authentication failed (state={})", user_id, - static_cast(auth_state)); - handle_protocol_mismatch(auth_state); - client->Stop(); - return false; - } - - 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 - { - OrbisNpId np_id{}; - SetNpId(np_id, npid); - std::lock_guard lock(m_mutex_clients); - m_np_ids[user_id] = np_id; - m_clients[user_id] = std::move(client); - } - - FireStateCallback(user_id, NpManager::OrbisNpState::SignedIn); - return true; -} - -void NpHandler::SetAppearOffline(bool enable) { - m_appear_offline = enable; - std::lock_guard lock(m_mutex_clients); - for (auto& [uid, client] : m_clients) { - if (client) - client->SetAppearOffline(enable); // sends the toggle to connected sessions - } -} - -void NpHandler::FailPendingRequests(s32 user_id, s32 error_code) { - size_t failed = 0; - { - std::lock_guard lock(m_mutex_pending_tus); - for (auto it = m_pending_tus.begin(); it != m_pending_tus.end();) { - if (it->second.user_id == user_id) { - if (it->second.req) { - it->second.req->SetResult(error_code); - } - it = m_pending_tus.erase(it); - ++failed; - } else { - ++it; - } - } - } - { - std::lock_guard lock(m_mutex_pending_score); - for (auto it = m_pending_score.begin(); it != m_pending_score.end();) { - if (it->second.user_id == user_id) { - if (it->second.req) { - it->second.req->SetResult(error_code); - } - it = m_pending_score.erase(it); - ++failed; - } else { - ++it; - } - } - } - { - std::lock_guard lock(m_mutex_pending_lookup); - for (auto it = m_pending_lookup.begin(); it != m_pending_lookup.end();) { - if (it->second.user_id == user_id) { - if (it->second.on_result) { - it->second.on_result(error_code, 0, {}); - } - it = m_pending_lookup.erase(it); - ++failed; - } else { - ++it; - } - } - } - if (failed > 0) { - LOG_WARNING(NpHandler, "user_id={} failed {} pending request(s) with {:#x}", user_id, - failed, static_cast(error_code)); - } -} - -void NpHandler::DisconnectUser(s32 user_id) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) - return; - client = std::move(it->second); - m_clients.erase(it); - } - { - std::lock_guard lock(m_mutex_friend_state); - m_friend_state.erase(user_id); - } - client->Stop(); - // The reader thread is joined, so no reply can race us: complete every - // in-flight request now, or PollAsync spins and WaitAsync blocks forever. - FailPendingRequests(user_id, ORBIS_NP_ERROR_SIGNED_OUT); - FireStateCallback(user_id, NpManager::OrbisNpState::SignedOut); - LOG_INFO(NpHandler, "user_id={} disconnected", user_id); -} - -void NpHandler::OnUserLoggedIn(s32 user_id) { - if (ConnectUserById(user_id)) - StartWorker(); -} - -void NpHandler::OnUserLoggedOut(s32 user_id) { - { - std::lock_guard lock(m_mutex_clients); - m_reconnect.erase(user_id); // do not auto-reconnect - } - DisconnectUser(user_id); -} - -void NpHandler::WorkerThread() { - constexpr auto INTERVAL = std::chrono::milliseconds(500); - - while (m_worker_running) { - std::this_thread::sleep_for(INTERVAL); - - // Collect ids of dropped clients - std::vector dropped; - { - std::lock_guard lock(m_mutex_clients); - for (auto& [uid, client] : m_clients) { - if (!client->IsConnected()) - dropped.push_back(uid); - } - } - - for (s32 uid : dropped) { - LOG_WARNING(NpHandler, "user_id={} connection dropped (network); will retry", uid); - DisconnectUser(uid); // reports SignedOut + stops/removes the dead client - MarkForReconnect(uid); // schedule transparent reconnect (network drop, not logout) - } - - TryReconnect(); - } -} - -void NpHandler::MarkForReconnect(s32 user_id) { - if (!EmulatorSettings.IsShadNetEnabled()) - return; // offline mode: nothing to reconnect to - constexpr auto kInitialBackoff = std::chrono::milliseconds(2000); - std::lock_guard lock(m_mutex_clients); - auto& st = m_reconnect[user_id]; - st.backoff = kInitialBackoff; - st.next_attempt = std::chrono::steady_clock::now() + st.backoff; -} - -void NpHandler::TryReconnect() { - constexpr auto kMaxBackoff = std::chrono::milliseconds(30000); - const auto now = std::chrono::steady_clock::now(); - - std::vector due; - { - std::lock_guard lock(m_mutex_clients); - for (auto& [uid, st] : m_reconnect) { - if (m_clients.count(uid) || now >= st.next_attempt) - due.push_back(uid); - } - } - - for (s32 uid : due) { - if (!m_worker_running) - return; - if (!EmulatorSettings.IsShadNetEnabled()) { - std::lock_guard lock(m_mutex_clients); - m_reconnect.erase(uid); - continue; - } - // ConnectUserById locks m_mutex_clients internally; call it unlocked. - const bool ok = ConnectUserById(uid); - std::lock_guard lock(m_mutex_clients); - if (ok || m_clients.count(uid)) { - m_reconnect.erase(uid); - LOG_INFO(NpHandler, "user_id={} reconnected to shadNet", uid); - } else if (auto it = m_reconnect.find(uid); it != m_reconnect.end()) { - it->second.backoff = std::min(it->second.backoff * 2, kMaxBackoff); - it->second.next_attempt = std::chrono::steady_clock::now() + it->second.backoff; - LOG_DEBUG(NpHandler, "user_id={} reconnect failed; next attempt in {}ms", uid, - it->second.backoff.count()); - } - } -} - -bool NpHandler::IsPsnSignedIn(s32 user_id) const { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - return it != m_clients.end() && it->second->IsAuthenticated(); -} - -bool NpHandler::IsAnySignedIn() const { - std::lock_guard lock(m_mutex_clients); - for (auto& [_, client] : m_clients) - if (client->IsAuthenticated()) - return true; - return false; -} - -OrbisNpId NpHandler::GetNpId(s32 user_id) const { - std::lock_guard lock(m_mutex_clients); - auto it = m_np_ids.find(user_id); - return it != m_np_ids.end() ? it->second : OrbisNpId{}; -} - -OrbisNpOnlineId NpHandler::GetOnlineId(s32 user_id) const { - return GetNpId(user_id).handle; -} - -std::string NpHandler::GetAvatarUrl(s32 user_id) const { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - return it != m_clients.end() ? it->second->GetAvatarUrl() : std::string{}; -} - -OrbisNpAccountId NpHandler::GetAccountId(s32 user_id) const { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - return it != m_clients.end() ? static_cast(it->second->GetUserId()) : 0; -} - -std::string NpHandler::GetBearerToken(s32 user_id) const { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - return it != m_clients.end() ? it->second->GetBearerToken() : std::string{}; -} - -// Minimal JSON string escaper for the invitation-request body (npids are safe but the user message -// can contain quotes/backslashes/control chars). -std::string JsonEscape(const std::string& in) { - std::string out; - out.reserve(in.size() + 8); - for (const char c : in) { - switch (c) { - case '"': - out += "\\\""; - break; - case '\\': - out += "\\\\"; - break; - case '\n': - out += "\\n"; - break; - case '\r': - out += "\\r"; - break; - case '\t': - out += "\\t"; - break; - default: - if (static_cast(c) < 0x20) { - char buf[8]; - std::snprintf(buf, sizeof(buf), "\\u%04x", c); - out += buf; - } else { - out += c; - } - } - } - return out; -} - -bool NpHandler::SendSessionInvitation(s32 user_id, const std::string& session_id, - const std::vector& to, - const std::string& message) { - if (session_id.empty() || to.empty()) { - LOG_ERROR(NpHandler, "empty session_id or recipient list"); - return false; - } - const std::string base_url = EmulatorSettings.GetShadNetWebApiServer(); - if (base_url.empty()) { - LOG_ERROR(NpHandler, "WebAPI server address is empty"); - return false; - } - const std::string token = GetBearerToken(user_id); - if (token.empty()) { - LOG_ERROR(NpHandler, "no bearer token for user_id={}", user_id); - return false; - } - - // invitation-request JSON: {"to":[...],"message":"..."} - std::string json = "{\"to\":["; - for (size_t i = 0; i < to.size(); ++i) { - if (i != 0) { - json += ','; - } - json += '"'; - json += JsonEscape(to[i]); - json += '"'; - } - json += ']'; - if (!message.empty()) { - json += ",\"message\":\""; - json += JsonEscape(message); - json += '"'; - } - json += '}'; - - // multipart/mixed with a single JSON part; shadNet keys on the Content-Description. - const std::string boundary = "shadps4invite" + std::to_string(user_id); - std::string body; - body += "--" + boundary + "\r\n"; - body += "Content-Type: application/json; charset=utf-8\r\n"; - body += "Content-Description: invitation-request\r\n\r\n"; - body += json; - body += "\r\n--" + boundary + "--\r\n"; - - const std::string path = "/v1/sessions/" + session_id + "/invitations"; - const std::string content_type = "multipart/mixed; boundary=" + boundary; - - httplib::Client cli(base_url); - cli.set_connection_timeout(5); - cli.set_read_timeout(10); - const httplib::Headers headers = {{"Authorization", "Bearer " + token}}; - const auto res = cli.Post(path.c_str(), headers, body, content_type.c_str()); - if (!res) { - LOG_ERROR(NpHandler, "POST {} failed (no response)", path); - return false; - } - if (res->status != 200 && res->status != 204) { - LOG_ERROR(NpHandler, "POST {} -> HTTP {}", path, res->status); - return false; - } - LOG_INFO(NpHandler, "sent invite to {} recipient(s) for session '{}'", to.size(), session_id); - return true; -} - -void NpHandler::PostSessionInvitationEvent(s32 user_id, const std::string& session_id, - const std::string& invitation_id, - const std::string& accepter_online_id, - const std::string& inviter_online_id, - OrbisNpAccountId inviter_account_id) { - using Libraries::InvitationDialog::ORBIS_NP_SESSION_INVITATION_EVENT_FLAG_INVITATION; - using Libraries::InvitationDialog::OrbisNpSessionInvitationEventParam; - - Libraries::SystemService::OrbisSystemServiceEvent event{}; - event.event_type = Libraries::SystemService::OrbisSystemServiceEventType::SessionInvitation; - - auto* param = reinterpret_cast(event.param); - std::memset(param, 0, sizeof(*param)); - std::strncpy(param->sessionId.data, session_id.c_str(), sizeof(param->sessionId.data) - 1); - if (!invitation_id.empty()) { - std::strncpy(param->invitationId.data, invitation_id.c_str(), - sizeof(param->invitationId.data) - 1); - param->flag = ORBIS_NP_SESSION_INVITATION_EVENT_FLAG_INVITATION; - } else { - param->flag = 0; // join from session info (no invitation id in the push) - } - // data[16] is followed by its own 'term' byte, so a full 16-char online id must not be cut. - std::strncpy(param->onlineId.data, accepter_online_id.c_str(), sizeof(param->onlineId.data)); - param->userId = user_id; - std::strncpy(param->referralOnlineId.data, inviter_online_id.c_str(), - sizeof(param->referralOnlineId.data)); - param->referralAccountId = inviter_account_id; - - Libraries::SystemService::PushSystemServiceEvent(event); - LOG_INFO(NpHandler, - "Posted SESSION_INVITATION user_id={} session='{}' invitation='{}' flag={} " - "onlineId='{}' referral='{}'({})", - user_id, session_id, invitation_id, param->flag, accepter_online_id, inviter_online_id, - inviter_account_id); -} - -std::vector NpHandler::GetPendingInvitations(s32 user_id) const { - std::lock_guard lk(m_mutex_pending_invites); - const auto it = m_pending_invites.find(user_id); - if (it == m_pending_invites.end()) { - return {}; - } - const auto now = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - std::vector out; - out.reserve(it->second.size()); - for (const auto& p : it->second) { - if (p.valid_until == 0 || now <= p.valid_until) { - out.push_back(p); - } - } - return out; -} - -bool NpHandler::AcceptSessionInvitation(s32 user_id, const std::string& invitation_id) { - // Pull the matching stashed invite (and drop it). - PendingInvitation inv; - bool found = false; - { - std::lock_guard lk(m_mutex_pending_invites); - const auto mit = m_pending_invites.find(user_id); - if (mit != m_pending_invites.end()) { - auto& v = mit->second; - for (auto it = v.begin(); it != v.end(); ++it) { - if (it->invitation_id == invitation_id) { - inv = *it; - v.erase(it); - found = true; - break; - } - } - } - } - // Whatever surface handled it (RECV dialog or the emulator prompt), retire the other one. - ImGui::InvitationPrompt::Dismiss(invitation_id); - if (!found) { - LOG_ERROR(NpHandler, "no pending invite '{}' for user_id={}", invitation_id, user_id); - return false; - } - // Raise the join event now that the user has explicitly accepted (via the RECV dialog or the - // emulator's system-UI equivalent). - PostSessionInvitationEvent(user_id, inv.session_id, invitation_id, inv.to_npid, inv.from_npid, - inv.from_account_id); - LOG_INFO(NpHandler, "accepted '{}' session='{}'", invitation_id, inv.session_id); - return true; -} - -void NpHandler::DeclineSessionInvitation(s32 user_id, const std::string& invitation_id) { - ImGui::InvitationPrompt::Dismiss(invitation_id); - std::lock_guard lock(m_mutex_pending_invites); - auto uit = m_pending_invites.find(user_id); - if (uit == m_pending_invites.end()) { - return; - } - auto& v = uit->second; - v.erase(std::remove_if( - v.begin(), v.end(), - [&](const PendingInvitation& p) { return p.invitation_id == invitation_id; }), - v.end()); - LOG_INFO(NpHandler, "dismissed '{}' for user_id={}", invitation_id, user_id); -} - -u32 NpHandler::GetLocalIpAddr(s32 user_id) const { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - return it != m_clients.end() ? it->second->GetAddrLocal() : 0; -} - -s32 NpHandler::GetUserIdByAccountId(u64 account_id) const { - std::lock_guard lock(m_mutex_clients); - for (auto& [uid, client] : m_clients) { - if (static_cast(client->GetUserId()) == account_id) - return uid; - } - return -1; -} - -s32 NpHandler::GetUserIdByOnlineId(const OrbisNpOnlineId& online_id) const { - std::lock_guard lock(m_mutex_clients); - for (auto& [uid, np_id] : m_np_ids) { - if (strncmp(np_id.handle.data, online_id.data, ORBIS_NP_ONLINEID_MAX_LENGTH) == 0) - return uid; - } - return -1; -} - -// friends calls -u32 NpHandler::GetNumFriends(s32 user_id) const { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - return it != m_clients.end() ? it->second->GetNumFriends() : 0; -} - -std::optional NpHandler::GetFriendNpid(s32 user_id, u32 index) const { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - return it != m_clients.end() ? it->second->GetFriendNpid(index) : std::nullopt; -} - -void NpHandler::OnFriendQuery(s32 user_id, const ShadNet::NotifyFriendQuery& n) { - LOG_INFO(NpHandler, "user_id={} FriendQuery from '{}'", user_id, n.fromNpid); - ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendRequest, - "Friend request from " + n.fromNpid); - std::lock_guard lock(m_mutex_friend_state); - auto& st = m_friend_state[user_id]; - if (std::find(st.requests_received.begin(), st.requests_received.end(), n.fromNpid) == - st.requests_received.end()) { - st.requests_received.push_back(n.fromNpid); - } -} - -void NpHandler::OnFriendNew(s32 user_id, const ShadNet::NotifyFriendNew& n) { - LOG_INFO(NpHandler, "user_id={} FriendNew '{}' ({})", user_id, n.npid, - n.online ? "online" : "offline"); - ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendNew, - n.npid + " is now your friend"); - std::lock_guard lock(m_mutex_friend_state); - auto& st = m_friend_state[user_id]; - auto it = std::find_if(st.friends.begin(), st.friends.end(), - [&](const FriendInfo& f) { return f.npid == n.npid; }); - if (it == st.friends.end()) { - st.friends.push_back({n.npid, n.online}); - } else { - it->online = n.online; - } - const auto drop = [&](std::vector& v) { - v.erase(std::remove(v.begin(), v.end(), n.npid), v.end()); - }; - drop(st.requests_received); - drop(st.requests_sent); -} - -void NpHandler::OnFriendLost(s32 user_id, const ShadNet::NotifyFriendLost& n) { - LOG_INFO(NpHandler, "user_id={} FriendLost '{}'", user_id, n.npid); - ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendLost, - n.npid + " removed you as a friend"); - std::lock_guard lock(m_mutex_friend_state); - auto& st = m_friend_state[user_id]; - st.friends.erase(std::remove_if(st.friends.begin(), st.friends.end(), - [&](const FriendInfo& f) { return f.npid == n.npid; }), - st.friends.end()); -} - -void NpHandler::OnFriendStatus(s32 user_id, const ShadNet::NotifyFriendStatus& n) { - LOG_DEBUG(NpHandler, "user_id={} FriendStatus '{}' is {}", user_id, n.npid, - n.online ? "online" : "offline"); - if (n.online) { - ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::Online, n.npid + " is online"); - } - std::lock_guard lock(m_mutex_friend_state); - auto& st = m_friend_state[user_id]; - auto it = std::find_if(st.friends.begin(), st.friends.end(), - [&](const FriendInfo& f) { return f.npid == n.npid; }); - if (it != st.friends.end()) { - it->online = n.online; - } else { - st.friends.push_back({n.npid, n.online}); - } -} - -void NpHandler::OnWebApiPushEvent(s32 user_id, const ShadNet::NotifyWebApiPushEvent& n) { - LOG_INFO(NpHandler, "user_id={} WebApiPushEvent svc='{}' type='{}' bytes={}", user_id, - n.npServiceName, n.dataType, n.data.size()); - // Forward verbatim to the libSceNpWebApi push-event dispatch.it queues the event - // and delivers it on the game's thread during sceNpCheckCallback to any registered - // (and filter-matching) push-event callback. - NpWebApi::PushEventInput ev; - ev.targetUserId = user_id; - ev.npServiceName = n.npServiceName; - ev.npServiceLabel = n.npServiceLabel; - ev.dataType = n.dataType; - ev.data = n.data; - if (!n.fromNpid.empty()) { - ev.hasFrom = true; - SetNpOnlineId(ev.fromOnlineId, n.fromNpid); - } - if (!n.toNpid.empty()) { - ev.hasTo = true; - SetNpOnlineId(ev.toOnlineId, n.toNpid); - } - ev.extdData = n.extdData; // extended-data (key,value) pairs -> dispatched as pExtdData - // Account ids for pFrom/pTo. - ev.toAccountId = n.toAccountId != 0 ? n.toAccountId : GetAccountId(user_id); - ev.fromAccountId = n.fromAccountId; - if (ev.fromAccountId == 0) { - for (const auto& kv : n.extdData) { - if (kv.first == "fromAccountId") { - ev.fromAccountId = std::strtoull(kv.second.c_str(), nullptr, 10); - break; - } - } - } - if (ev.toAccountId != 0) { - ev.hasTo = true; - if (n.toNpid.empty()) { - // e.g. friendlist events carry no toNpid; the recipient is still us. - std::lock_guard lock(m_mutex_clients); - if (const auto it = m_np_ids.find(user_id); it != m_np_ids.end()) { - std::memcpy(ev.toOnlineId.data, it->second.handle.data, sizeof(ev.toOnlineId.data)); - } - } - } - NpWebApi::EnqueuePushEvent(ev); - - // Also surface a SESSION_INVITATION system-service event for titles that watch it instead of - // (or in addition to) the WebAPI push callback - if (n.npServiceName == "sessionInvitation") { - std::string session_id, invitation_id; - int64_t valid_until = 0; - for (const auto& kv : n.extdData) { - if (kv.first == "sessionId") { - session_id = kv.second; - } else if (kv.first == "invitationId") { - invitation_id = kv.second; - } else if (kv.first == "validUntil") { - valid_until = std::strtoll(kv.second.c_str(), nullptr, 10); - } - } - if (!session_id.empty()) { - { - std::lock_guard lk(m_mutex_pending_invites); - auto& v = m_pending_invites[user_id]; - v.erase(std::remove_if(v.begin(), v.end(), - [&](const PendingInvitation& p) { - return p.invitation_id == invitation_id; - }), - v.end()); - PendingInvitation inv{session_id, invitation_id, n.fromNpid, n.toNpid, valid_until}; - inv.from_account_id = ev.fromAccountId; - v.push_back(std::move(inv)); - } - // ORBIS_SYSTEM_SERVICE_EVENT_SESSION_INVITATION is a *join* event: it - // fires only after the user explicitly accepts, via the game-opened invitation dialog - // (RECV) or the system software UI. Posting it here on arrival makes titles silently - // auto-join. The event is raised from AcceptSessionInvitation instead. The WebAPI push - // callback above still fires on arrival for titles that watch invites themselves. - // - // The emulator has no ShellUI, so surface the "system software UI" half here: an - // overlay prompt whose Accept routes through AcceptSessionInvitation. - ImGui::InvitationPrompt::Push(user_id, invitation_id, session_id, n.fromNpid); - } - } -} - -void NpHandler::OnLoginResult(s32 user_id, const ShadNet::LoginResult& res) { - if (res.error != ShadNet::ErrorType::NoError) { - return; - } - FriendListSnapshot snap; - snap.friends.reserve(res.friends.size()); - for (const auto& f : res.friends) { - snap.friends.push_back({f.npid, f.online}); - } - snap.requests_sent = res.requestsSent; - snap.requests_received = res.requestsReceived; - snap.blocked = res.blocked; - { - std::lock_guard lock(m_mutex_friend_state); - m_friend_state[user_id] = std::move(snap); - } - LOG_INFO(NpHandler, - "user_id={} friend state seeded: {} friends, {} received, {} sent, {} blocked", - user_id, res.friends.size(), res.requestsReceived.size(), res.requestsSent.size(), - res.blocked.size()); - - // Surface friend requests that arrived while offline (no live notification fires for these). - if (!res.requestsReceived.empty()) { - ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendRequest, - std::to_string(res.requestsReceived.size()) + - " pending friend request(s)"); - } -} - -std::vector NpHandler::GetConnectedUsers() const { - std::vector out; - std::lock_guard lock(m_mutex_clients); - out.reserve(m_clients.size()); - for (const auto& [uid, c] : m_clients) { - out.push_back(uid); - } - return out; -} - -NpHandler::FriendListSnapshot NpHandler::GetFriendList(s32 user_id) const { - std::lock_guard lock(m_mutex_friend_state); - const auto it = m_friend_state.find(user_id); - return it == m_friend_state.end() ? FriendListSnapshot{} : it->second; -} - -s32 NpHandler::SendFriendRequest(s32 user_id, const std::string& npid) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - const auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client->AddFriend(npid); - LOG_INFO(NpHandler, "user_id={} AddFriend '{}'", user_id, npid); - - std::lock_guard lock(m_mutex_friend_state); - auto& st = m_friend_state[user_id]; - const bool accepting = std::find(st.requests_received.begin(), st.requests_received.end(), - npid) != st.requests_received.end(); - if (!accepting && std::find(st.requests_sent.begin(), st.requests_sent.end(), npid) == - st.requests_sent.end()) { - st.requests_sent.push_back(npid); - } - return ORBIS_OK; -} - -s32 NpHandler::RemoveFriend(s32 user_id, const std::string& npid) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - const auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client->RemoveFriend(npid); - LOG_INFO(NpHandler, "user_id={} RemoveFriend '{}'", user_id, npid); - - std::lock_guard lock(m_mutex_friend_state); - auto& st = m_friend_state[user_id]; - st.friends.erase(std::remove_if(st.friends.begin(), st.friends.end(), - [&](const FriendInfo& f) { return f.npid == npid; }), - st.friends.end()); - const auto drop = [&](std::vector& v) { - v.erase(std::remove(v.begin(), v.end(), npid), v.end()); - }; - drop(st.requests_received); - drop(st.requests_sent); - return ORBIS_OK; -} - -s32 NpHandler::BlockUser(s32 user_id, const std::string& npid) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - const auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client->AddBlock(npid); - LOG_INFO(NpHandler, "user_id={} AddBlock '{}'", user_id, npid); - - std::lock_guard lock(m_mutex_friend_state); - auto& st = m_friend_state[user_id]; - if (std::find(st.blocked.begin(), st.blocked.end(), npid) == st.blocked.end()) { - st.blocked.push_back(npid); - } - st.friends.erase(std::remove_if(st.friends.begin(), st.friends.end(), - [&](const FriendInfo& f) { return f.npid == npid; }), - st.friends.end()); - const auto drop = [&](std::vector& v) { - v.erase(std::remove(v.begin(), v.end(), npid), v.end()); - }; - drop(st.requests_received); - drop(st.requests_sent); - return ORBIS_OK; -} - -s32 NpHandler::UnblockUser(s32 user_id, const std::string& npid) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - const auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client->RemoveBlock(npid); - LOG_INFO(NpHandler, "user_id={} RemoveBlock '{}'", user_id, npid); - - std::lock_guard lock(m_mutex_friend_state); - auto& st = m_friend_state[user_id]; - st.blocked.erase(std::remove(st.blocked.begin(), st.blocked.end(), npid), st.blocked.end()); - return ORBIS_OK; -} - -// state callbacks -s32 NpHandler::RegisterStateCallback(StateCallback cb, void* userdata) { - std::lock_guard lock(m_mutex_cbs); - const s32 h = m_next_handle++; - m_state_cbs.push_back({h, std::move(cb), userdata}); - return h; -} - -void NpHandler::UnregisterStateCallback(s32 handle) { - std::lock_guard lock(m_mutex_cbs); - m_state_cbs.erase(std::remove_if(m_state_cbs.begin(), m_state_cbs.end(), - [handle](const CbEntry& e) { return e.handle == handle; }), - m_state_cbs.end()); -} - -void NpHandler::FireStateCallback(s32 user_id, NpManager::OrbisNpState state) { - std::lock_guard lock(m_mutex_cbs); - for (const auto& e : m_state_cbs) { - if (e.cb) - e.cb(user_id, state); - } -} - -// Score - -// A usable NP Communication ID is the 12-char "NPWRxxxxx_00" form. An empty or -// all-NUL buffer means npbind.dat was missing or unparsed,treat it as invalid -// so callers reject the request instead of sending a blank comm id on the wire. -bool IsValidNpCommId(const std::string& id) { - return !id.empty() && std::any_of(id.begin(), id.end(), [](char c) { return c != '\0'; }); -} - -std::string NpHandler::GetNpCommId(s32 service_label) const { - // TODO complete guess of how commid is mapping to service_label. - constexpr size_t COM_ID_LEN = 12; - const auto& ids = Common::ElfInfo::Instance().GetNpCommIds(); - std::string com_id; - if (!ids.empty()) { - const size_t idx = (service_label >= 0 && static_cast(service_label) < ids.size()) - ? static_cast(service_label) - : 0; - com_id = ids[idx]; - if (static_cast(service_label) >= ids.size()) { - LOG_WARNING(NpHandler, - "service_label={} >= npbind entry count {} — falling back " - "to index 0", - service_label, ids.size()); - } - } - if (com_id.size() < COM_ID_LEN) { - com_id.resize(COM_ID_LEN, '\0'); - } else if (com_id.size() > COM_ID_LEN) { - com_id.resize(COM_ID_LEN); - } - return com_id; -} - -s32 NpHandler::RecordScore(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score, - const char* comment, size_t commentLen, const u8* gameInfoData, - size_t gameInfoSize, std::shared_ptr req) { - // Look up the user's shadNet session. - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - // Build RecordScoreRequest proto. - shadnet::RecordScoreRequest proto; - proto.set_boardid(boardId); - proto.set_pcid(pcId); - proto.set_score(score); - if (comment && commentLen > 0) { - proto.set_comment(std::string(comment, commentLen)); - } - if (gameInfoData && gameInfoSize > 0) { - proto.set_data(std::string(reinterpret_cast(gameInfoData), gameInfoSize)); - } - - const std::string proto_bytes = proto.SerializeAsString(); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size()); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::RecordScore, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::RecordScore; - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, - "user_id={} service_label={} board={} pcId={} score={} commentLen={} " - "gameInfoSize={} pkt_id={} com_id='{}'", - user_id, service_label, boardId, pcId, score, commentLen, gameInfoSize, pkt_id, - com_id); - return ORBIS_OK; -} - -s32 NpHandler::RecordGameData(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score, - const u8* data, size_t size, - std::shared_ptr req) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - shadnet::RecordScoreGameDataRequest proto; - proto.set_boardid(boardId); - proto.set_pcid(pcId); - proto.set_score(score); - - const std::string proto_bytes = proto.SerializeAsString(); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size() + size); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - if (data != nullptr && size > 0) { - payload.insert(payload.end(), data, data + size); - } - - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::RecordScoreData, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::RecordScoreData; - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, - "user_id={} service_label={} board={} pcId={} score={} dataSize={} " - "pkt_id={} com_id='{}'", - user_id, service_label, boardId, pcId, score, size, pkt_id, com_id); - return ORBIS_OK; -} - -s32 NpHandler::GetGameData(s32 user_id, s32 service_label, u32 boardId, const std::string& npId, - s32 pcId, void* dataOut, u64 recvSize, u64* totalSizeOut, - std::shared_ptr req) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - shadnet::GetScoreGameDataRequest proto; - proto.set_boardid(boardId); - proto.set_npid(npId); - proto.set_pcid(pcId); - - const std::string proto_bytes = proto.SerializeAsString(); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size()); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreData, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::GetScoreData; - pending.dataOut = dataOut; - pending.recvSize = recvSize; - pending.totalSizeOut = totalSizeOut; - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, - "user_id={} service_label={} board={} npId='{}' pcId={} recvSize={} " - "pkt_id={} com_id='{}'", - user_id, service_label, boardId, npId, pcId, recvSize, pkt_id, com_id); - return ORBIS_OK; -} - -s32 NpHandler::GetGameDataByAccountId(s32 user_id, s32 service_label, u32 boardId, u64 accountId, - s32 pcId, void* dataOut, u64 recvSize, u64* totalSizeOut, - std::shared_ptr req) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - shadnet::GetScoreGameDataByAccountIdRequest proto; - proto.set_boardid(boardId); - proto.set_accountid(accountId); - proto.set_pcid(pcId); - - const std::string proto_bytes = proto.SerializeAsString(); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size()); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - - const u64 pkt_id = - client->SubmitRequest(ShadNet::CommandType::GetScoreGameDataByAccId, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::GetScoreGameDataByAccId; - pending.dataOut = dataOut; - pending.recvSize = recvSize; - pending.totalSizeOut = totalSizeOut; - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, - "user_id={} service_label={} board={} accountId={} pcId={} " - "recvSize={} pkt_id={} com_id='{}'", - user_id, service_label, boardId, accountId, pcId, recvSize, pkt_id, com_id); - return ORBIS_OK; -} - -s32 NpHandler::GetBoardInfo(s32 user_id, s32 service_label, u32 boardId, - NpScore::OrbisNpScoreBoardInfo* boardInfo, - std::shared_ptr req) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - std::vector payload; - payload.reserve(12 + 4); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - payload.push_back(static_cast(boardId)); - payload.push_back(static_cast(boardId >> 8)); - payload.push_back(static_cast(boardId >> 16)); - payload.push_back(static_cast(boardId >> 24)); - - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetBoardInfos, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::GetBoardInfos; - pending.boardInfo = boardInfo; - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, "user_id={} service_label={} board={} pkt_id={} com_id='{}'", user_id, - service_label, boardId, pkt_id, com_id); - return ORBIS_OK; -} - -s32 NpHandler::GetRankingByNpId(s32 user_id, s32 service_label, u32 boardId, - const std::vector& npIds, - const std::vector& pcIds, - NpScore::OrbisNpScorePlayerRankData* rankArray, - NpScore::OrbisNpScoreComment* commentArray, - NpScore::OrbisNpScoreGameInfo* infoArray, - Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, - std::shared_ptr req) { - // pcIds must either be empty (use 0 for everything) or match npIds in size. - if (!pcIds.empty() && pcIds.size() != npIds.size()) { - LOG_ERROR(NpHandler, "pcIds size {} != npIds size {}", pcIds.size(), npIds.size()); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - // Look up the user's session. - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - shadnet::GetScoreNpIdRequest proto; - proto.set_boardid(boardId); - proto.set_withcomment(commentArray != nullptr); - proto.set_withgameinfo(infoArray != nullptr); - for (size_t i = 0; i < npIds.size(); ++i) { - auto* entry = proto.add_npids(); - entry->set_npid(npIds[i]); - entry->set_pcid(pcIds.empty() ? 0 : pcIds[i]); - } - - const std::string proto_bytes = proto.SerializeAsString(); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size()); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreNpid, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::GetScoreNpid; - pending.requestedNpIds = npIds; - pending.rankArray = rankArray; - pending.commentArray = commentArray; - pending.infoArray = infoArray; - pending.lastSortDate = lastSortDate; - pending.totalRecord = totalRecord; - pending.arrayNum = npIds.size(); - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, - "user_id={} service_label={} board={} npIdCount={} " - "withPcId={} withComment={} withGameInfo={} pkt_id={} com_id='{}'", - user_id, service_label, boardId, npIds.size(), !pcIds.empty(), commentArray != nullptr, - infoArray != nullptr, pkt_id, com_id); - return ORBIS_OK; -} - -s32 NpHandler::GetRankingByRange(s32 user_id, s32 service_label, u32 boardId, u32 startSerialRank, - u32 arrayNum, NpScore::OrbisNpScoreRankData* rankArray, - NpScore::OrbisNpScoreComment* commentArray, - NpScore::OrbisNpScoreGameInfo* infoArray, - Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, - std::shared_ptr req) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - shadnet::GetScoreRangeRequest proto; - proto.set_boardid(boardId); - proto.set_startrank(startSerialRank); - proto.set_numranks(arrayNum); - proto.set_withcomment(commentArray != nullptr); - proto.set_withgameinfo(infoArray != nullptr); - - const std::string proto_bytes = proto.SerializeAsString(); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size()); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreRange, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::GetScoreRange; - pending.plainRankArray = rankArray; - pending.commentArray = commentArray; - pending.infoArray = infoArray; - pending.lastSortDate = lastSortDate; - pending.totalRecord = totalRecord; - pending.arrayNum = arrayNum; - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, - "user_id={} service_label={} board={} startRank={} numRanks={} " - "withComment={} withGameInfo={} pkt_id={} com_id='{}'", - user_id, service_label, boardId, startSerialRank, arrayNum, commentArray != nullptr, - infoArray != nullptr, pkt_id, com_id); - return ORBIS_OK; -} - -s32 NpHandler::GetRankingByRangeA(s32 user_id, s32 service_label, u32 boardId, u32 startSerialRank, - u32 arrayNum, NpScore::OrbisNpScoreRankDataA* rankArray, - NpScore::OrbisNpScoreComment* commentArray, - NpScore::OrbisNpScoreGameInfo* infoArray, - Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, - std::shared_ptr req) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - shadnet::GetScoreRangeRequest proto; - proto.set_boardid(boardId); - proto.set_startrank(startSerialRank); - proto.set_numranks(arrayNum); - proto.set_withcomment(commentArray != nullptr); - proto.set_withgameinfo(infoArray != nullptr); - - const std::string proto_bytes = proto.SerializeAsString(); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size()); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreRange, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::GetScoreRange; - // Note: aRankArray is set, plainRankArray remains null. The shared - // GetScoreRange/GetScoreFriends branch in OnScoreReply dispatches - // on which one is non-null to pick the right fill helper. - pending.aRankArray = rankArray; - pending.commentArray = commentArray; - pending.infoArray = infoArray; - pending.lastSortDate = lastSortDate; - pending.totalRecord = totalRecord; - pending.arrayNum = arrayNum; - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, - "user_id={} service_label={} board={} startRank={} numRanks={} " - "withComment={} withGameInfo={} pkt_id={} com_id='{}'", - user_id, service_label, boardId, startSerialRank, arrayNum, commentArray != nullptr, - infoArray != nullptr, pkt_id, com_id); - return ORBIS_OK; -} - -s32 NpHandler::GetRankingByAccountId(s32 user_id, s32 service_label, u32 boardId, - const std::vector& accountIds, - const std::vector& pcIds, - NpScore::OrbisNpScorePlayerRankDataA* rankArray, - NpScore::OrbisNpScoreComment* commentArray, - NpScore::OrbisNpScoreGameInfo* infoArray, - Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, - std::shared_ptr req) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - shadnet::GetScoreAccountIdRequest proto; - proto.set_boardid(boardId); - for (size_t i = 0; i < accountIds.size(); ++i) { - auto* entry = proto.add_ids(); - entry->set_accountid(static_cast(accountIds[i])); - // If the caller provided per-entry pcIds, use them; otherwise all zero. - entry->set_pcid(i < pcIds.size() ? pcIds[i] : 0); - } - proto.set_withcomment(commentArray != nullptr); - proto.set_withgameinfo(infoArray != nullptr); - - const std::string proto_bytes = proto.SerializeAsString(); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size()); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreAccountId, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::GetScoreAccountId; - pending.aPlayerRankArray = rankArray; - pending.commentArray = commentArray; - pending.infoArray = infoArray; - pending.lastSortDate = lastSortDate; - pending.totalRecord = totalRecord; - pending.arrayNum = accountIds.size(); - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, - "user_id={} service_label={} board={} accountIdCount={} " - "withComment={} withGameInfo={} pkt_id={} com_id='{}'", - user_id, service_label, boardId, accountIds.size(), commentArray != nullptr, - infoArray != nullptr, pkt_id, com_id); - return ORBIS_OK; -} - -s32 NpHandler::GetFriendsRanking(s32 user_id, s32 service_label, u32 boardId, bool includeSelf, - u32 arrayNum, NpScore::OrbisNpScoreRankData* rankArray, - NpScore::OrbisNpScoreComment* commentArray, - NpScore::OrbisNpScoreGameInfo* infoArray, - Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, - std::shared_ptr req) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - shadnet::GetScoreFriendsRequest proto; - proto.set_boardid(boardId); - proto.set_includeself(includeSelf); - proto.set_max(arrayNum); - proto.set_withcomment(commentArray != nullptr); - proto.set_withgameinfo(infoArray != nullptr); - - const std::string proto_bytes = proto.SerializeAsString(); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size()); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreFriends, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::GetScoreFriends; - pending.plainRankArray = rankArray; - pending.commentArray = commentArray; - pending.infoArray = infoArray; - pending.lastSortDate = lastSortDate; - pending.totalRecord = totalRecord; - pending.arrayNum = arrayNum; - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, - "user_id={} service_label={} board={} includeSelf={} " - "arrayNum={} withComment={} withGameInfo={} pkt_id={} com_id='{}'", - user_id, service_label, boardId, includeSelf, arrayNum, commentArray != nullptr, - infoArray != nullptr, pkt_id, com_id); - return ORBIS_OK; -} - -s32 NpHandler::GetFriendsRankingA(s32 user_id, s32 service_label, u32 boardId, bool includeSelf, - u32 arrayNum, NpScore::OrbisNpScoreRankDataA* rankArray, - NpScore::OrbisNpScoreComment* commentArray, - NpScore::OrbisNpScoreGameInfo* infoArray, - Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, - std::shared_ptr req) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "user_id={} not connected", user_id); - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; - } - - shadnet::GetScoreFriendsRequest proto; - proto.set_boardid(boardId); - proto.set_includeself(includeSelf); - proto.set_max(arrayNum); - proto.set_withcomment(commentArray != nullptr); - proto.set_withgameinfo(infoArray != nullptr); - - const std::string proto_bytes = proto.SerializeAsString(); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " - "rejecting request"); - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size()); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreFriends, payload); - { - std::lock_guard lock(m_mutex_pending_score); - PendingScoreRequest pending; - pending.req = std::move(req); - pending.cmd = ShadNet::CommandType::GetScoreFriends; - // Note: aRankArray is set, plainRankArray remains null. OnScoreReply - // dispatches on which one is non-null. - pending.aRankArray = rankArray; - pending.commentArray = commentArray; - pending.infoArray = infoArray; - pending.lastSortDate = lastSortDate; - pending.totalRecord = totalRecord; - pending.arrayNum = arrayNum; - pending.user_id = user_id; - m_pending_score.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, - "user_id={} service_label={} board={} includeSelf={} " - "arrayNum={} withComment={} withGameInfo={} pkt_id={} com_id='{}'", - user_id, service_label, boardId, includeSelf, arrayNum, commentArray != nullptr, - infoArray != nullptr, pkt_id, com_id); - return ORBIS_OK; -} - -static u32 FillPlainRankArrayFromProto(const shadnet::GetScoreResponse& resp, u64 maxSlots, - NpScore::OrbisNpScoreRankData* rankArray, - NpScore::OrbisNpScoreComment* commentArray, - NpScore::OrbisNpScoreGameInfo* infoArray) { - const int n_resp = resp.rankarray_size(); - u32 out_i = 0; - for (int i = 0; i < n_resp && out_i < maxSlots; ++i) { - const auto& r = resp.rankarray(i); - if (r.npid().empty()) { - continue; - } - auto& out = rankArray[out_i]; - // Copy OnlineId (the npId string) into the 16-byte data field. - const std::string& npid = r.npid(); - const size_t cp = std::min(npid.size(), sizeof(out.npId.handle.data)); - std::memcpy(out.npId.handle.data, npid.data(), cp); - out.npId.handle.term = 0; - LOG_INFO(NpHandler, - "out[{}] (resp[{}]) npid='{}' (len={}) rank={} " - "score={}", - out_i, i, npid, npid.size(), r.rank(), r.score()); - out.pcId = r.pcid(); - out.serialRank = r.rank(); - out.rank = r.rank(); - out.highestRank = r.rank(); - out.hasGameData = r.hasgamedata() ? 1 : 0; - out.scoreValue = r.score(); - out.recordDate.tick = r.recorddate(); - - if (commentArray != nullptr && i < resp.commentarray_size()) { - const std::string& cmt = resp.commentarray(i); - const size_t ccp = - std::min(cmt.size(), sizeof(commentArray[out_i].utf8Comment) - 1); - std::memcpy(commentArray[out_i].utf8Comment, cmt.data(), ccp); - } - if (infoArray != nullptr && i < resp.infoarray_size()) { - const std::string& gi = resp.infoarray(i).data(); - const size_t gcp = std::min(gi.size(), sizeof(infoArray[out_i].data)); - infoArray[out_i].infoSize = static_cast(gcp); - std::memcpy(infoArray[out_i].data, gi.data(), gcp); - } - ++out_i; - } - return out_i; -} - -static u32 FillPlainRankArrayAFromProto(const shadnet::GetScoreResponse& resp, u64 maxSlots, - NpScore::OrbisNpScoreRankDataA* rankArray, - NpScore::OrbisNpScoreComment* commentArray, - NpScore::OrbisNpScoreGameInfo* infoArray) { - const int n_resp = resp.rankarray_size(); - u32 out_i = 0; - for (int i = 0; i < n_resp && out_i < maxSlots; ++i) { - const auto& r = resp.rankarray(i); - if (r.npid().empty()) { - continue; - } - auto& out = rankArray[out_i]; - // RankDataA stores OnlineId directly (not wrapped in NpId). - const std::string& npid = r.npid(); - const size_t cp = std::min(npid.size(), sizeof(out.onlineId.data)); - std::memcpy(out.onlineId.data, npid.data(), cp); - out.onlineId.term = 0; - LOG_INFO(NpHandler, - "out[{}] (resp[{}]) npid='{}' (len={}) rank={} " - "score={} accountId={}", - out_i, i, npid, npid.size(), r.rank(), r.score(), r.accountid()); - out.pcId = r.pcid(); - out.serialRank = r.rank(); - out.rank = r.rank(); - out.highestRank = r.rank(); - out.hasGameData = r.hasgamedata() ? 1 : 0; - out.scoreValue = r.score(); - out.recordDate.tick = r.recorddate(); - out.accountId = static_cast(r.accountid()); - - if (commentArray != nullptr && i < resp.commentarray_size()) { - const std::string& cmt = resp.commentarray(i); - const size_t ccp = - std::min(cmt.size(), sizeof(commentArray[out_i].utf8Comment) - 1); - std::memcpy(commentArray[out_i].utf8Comment, cmt.data(), ccp); - } - if (infoArray != nullptr && i < resp.infoarray_size()) { - const std::string& gi = resp.infoarray(i).data(); - const size_t gcp = std::min(gi.size(), sizeof(infoArray[out_i].data)); - infoArray[out_i].infoSize = static_cast(gcp); - std::memcpy(infoArray[out_i].data, gi.data(), gcp); - } - ++out_i; - } - return out_i; -} - -static u32 FillPlayerRankArrayAFromProto(const shadnet::GetScoreResponse& resp, u64 requestedCount, - NpScore::OrbisNpScorePlayerRankDataA* rankArray, - NpScore::OrbisNpScoreComment* commentArray, - NpScore::OrbisNpScoreGameInfo* infoArray) { - const int n_resp = resp.rankarray_size(); - const int n_req = static_cast(requestedCount); - if (n_resp != n_req) { - LOG_WARNING(NpHandler, - "response count {} != request count {} — " - "will fill up to min() and leave extras at hasData=0", - n_resp, n_req); - } - const int n = std::min(n_resp, n_req); - u32 found = 0; - for (int i = 0; i < n; ++i) { - const auto& r = resp.rankarray(i); - if (r.npid().empty()) { - continue; // no score on this board for this input accountId - } - auto& out = rankArray[i]; - out.hasData = 1; - const std::string& npid = r.npid(); - const size_t cp = std::min(npid.size(), sizeof(out.rankData.onlineId.data)); - std::memcpy(out.rankData.onlineId.data, npid.data(), cp); - out.rankData.onlineId.term = 0; - out.rankData.pcId = r.pcid(); - out.rankData.serialRank = r.rank(); - out.rankData.rank = r.rank(); - out.rankData.highestRank = r.rank(); - out.rankData.hasGameData = r.hasgamedata() ? 1 : 0; - out.rankData.scoreValue = r.score(); - out.rankData.recordDate.tick = r.recorddate(); - out.rankData.accountId = static_cast(r.accountid()); - - if (commentArray != nullptr && i < resp.commentarray_size()) { - const std::string& cmt = resp.commentarray(i); - const size_t ccp = - std::min(cmt.size(), sizeof(commentArray[i].utf8Comment) - 1); - std::memcpy(commentArray[i].utf8Comment, cmt.data(), ccp); - } - if (infoArray != nullptr && i < resp.infoarray_size()) { - const std::string& gi = resp.infoarray(i).data(); - const size_t gcp = std::min(gi.size(), sizeof(infoArray[i].data)); - infoArray[i].infoSize = static_cast(gcp); - std::memcpy(infoArray[i].data, gi.data(), gcp); - } - ++found; - } - return found; -} - -static u32 FillRankArrayFromProto(const shadnet::GetScoreResponse& resp, - const std::vector& requestedNpIds, - NpScore::OrbisNpScorePlayerRankData* rankArray, - NpScore::OrbisNpScoreComment* commentArray, - NpScore::OrbisNpScoreGameInfo* infoArray) { - const int n_resp = resp.rankarray_size(); - const int n_req = static_cast(requestedNpIds.size()); - if (n_resp != n_req) { - LOG_WARNING(NpHandler, - "response count {} != request count {} — " - "will fill up to min() and leave extras at hasData=0", - n_resp, n_req); - } - const int n = std::min(n_resp, n_req); - u32 found = 0; - for (int i = 0; i < n; ++i) { - const auto& r = resp.rankarray(i); - if (r.npid().empty()) { - continue; // server signalled "no data for this slot" - } - - auto& out = rankArray[i]; - out.hasData = 1; - const std::string& npid = r.npid(); - const size_t cp = std::min(npid.size(), sizeof(out.rankData.npId.handle.data)); - std::memcpy(out.rankData.npId.handle.data, npid.data(), cp); - out.rankData.npId.handle.term = 0; - out.rankData.pcId = r.pcid(); - out.rankData.serialRank = r.rank(); - out.rankData.rank = r.rank(); - out.rankData.highestRank = r.rank(); - out.rankData.hasGameData = r.hasgamedata() ? 1 : 0; - out.rankData.scoreValue = r.score(); - out.rankData.recordDate.tick = r.recorddate(); - - if (commentArray != nullptr && i < resp.commentarray_size()) { - const std::string& cmt = resp.commentarray(i); - const size_t ccp = - std::min(cmt.size(), sizeof(commentArray[i].utf8Comment) - 1); - std::memcpy(commentArray[i].utf8Comment, cmt.data(), ccp); - } - if (infoArray != nullptr && i < resp.infoarray_size()) { - const std::string& gi = resp.infoarray(i).data(); - const size_t gcp = std::min(gi.size(), sizeof(infoArray[i].data)); - infoArray[i].infoSize = static_cast(gcp); - std::memcpy(infoArray[i].data, gi.data(), gcp); - } - ++found; - } - return found; -} -// TUS -std::vector BuildTusPayload(const std::string& com_id, const std::string& proto_bytes) { - std::vector payload; - payload.reserve(12 + 4 + proto_bytes.size()); - payload.insert(payload.end(), com_id.begin(), com_id.end()); - const u32 sz = static_cast(proto_bytes.size()); - payload.push_back(static_cast(sz)); - payload.push_back(static_cast(sz >> 8)); - payload.push_back(static_cast(sz >> 16)); - payload.push_back(static_cast(sz >> 24)); - payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); - return payload; -} - -void CopyNpHandle(OrbisNpId& dst, const std::string& src) { - dst = OrbisNpId{}; - const size_t cp = std::min(src.size(), sizeof(dst.handle.data) - 1); - std::memcpy(dst.handle.data, src.data(), cp); -} - -s32 NpHandler::TusGetMultiSlotVariable( - s32 user_id, s32 service_label, const std::string& ownerNpId, const std::string& virtualUser, - const std::vector& slotIds, NpTus::OrbisNpTusVariable* variablesOut, u64 arrayNum, - std::shared_ptr ctx, NpTus::OrbisNpTusVariableA* variablesAOut, - s64 ownerAccountId, NpTus::OrbisNpTusVariableForCrossSave* variablesCSOut) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusGetMultiSlotVariableRequest proto; - proto.set_ownernpid(ownerNpId); - if (!virtualUser.empty()) { - proto.set_virtualuser(virtualUser); - } else if (ownerAccountId != 0) { - proto.set_owneraccountid(ownerAccountId); - } else if (ownerNpId.empty() || - Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { - proto.set_owneraccountid(static_cast(client->GetUserId())); - } - for (s32 s : slotIds) { - proto.add_slotids(s); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetMultiSlotVariable, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusGetMultiSlotVariable; - p.variableArray = variablesOut; - p.variableArrayA = variablesAOut; - p.variableArrayCS = variablesCSOut; - p.arrayNum = arrayNum; - p.user_id = user_id; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusSetMultiSlotVariable(s32 user_id, s32 service_label, const std::string& ownerNpId, - const std::string& virtualUser, - const std::vector& slotIds, - const std::vector& values, - std::shared_ptr ctx, - s64 ownerAccountId) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusSetMultiSlotVariableRequest proto; - proto.set_ownernpid(ownerNpId); - if (!virtualUser.empty()) { - proto.set_virtualuser(virtualUser); - } else if (ownerAccountId != 0) { - proto.set_owneraccountid(ownerAccountId); - } else if (ownerNpId.empty() || - Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { - proto.set_owneraccountid(static_cast(client->GetUserId())); - } - - for (s32 s : slotIds) { - proto.add_slotids(s); - } - for (s64 v : values) { - proto.add_values(v); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusSetMultiSlotVariable, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusSetMultiSlotVariable; - p.user_id = user_id; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TssGetData(s32 user_id, s32 service_label, s32 slotId, bool hasOffset, u64 offset, - bool hasLastByte, u64 lastByte, bool hasIfParam, s32 ifType, - u64 ifLastModified, NpTus::OrbisNpTssDataStatus* statusOut, void* dataOut, - u64 dataCap, std::shared_ptr ctx, - u64* contentLengthOut) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TssGetDataRequest proto; - proto.set_slotid(slotId); - if (hasOffset) { - proto.set_hasoffset(true); - proto.set_offset(offset); - } - if (hasLastByte) { - proto.set_haslastbyte(true); - proto.set_lastbyte(lastByte); - } - if (hasIfParam) { - proto.set_hasifparam(true); - proto.set_iftype(ifType); - proto.set_iflastmodified(ifLastModified); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TssGetData, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TssGetData; - p.user_id = user_id; - p.tssStatusOut = statusOut; - p.tssContentLengthOut = contentLengthOut; - p.dataOut = dataOut; - p.dataCap = dataCap; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusGetData(s32 user_id, s32 service_label, const std::string& ownerNpId, - const std::string& virtualUser, s64 ownerAccountId, s32 slotId, - NpTus::OrbisNpTusDataStatusA* statusAOut, u64 statusCap, void* dataOut, - u64 dataCap, u64 dataOffset, std::shared_ptr ctx, - NpTus::OrbisNpTusDataStatus* statusOut, - NpTus::OrbisNpTusDataStatusForCrossSave* statusCSOut) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusGetDataRequest proto; - proto.set_ownernpid(ownerNpId); - proto.set_slotid(slotId); - // Same targeting precedence as the variable paths. - if (!virtualUser.empty()) { - proto.set_virtualuser(virtualUser); - } else if (ownerAccountId != 0) { - proto.set_owneraccountid(ownerAccountId); - } else if (ownerNpId.empty() || - Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { - proto.set_owneraccountid(static_cast(client->GetUserId())); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetData, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusGetData; - p.user_id = user_id; - p.statusArrayA = statusAOut; - p.statusArray = statusOut; - p.statusArrayCS = statusCSOut; - p.statusCap = statusCap; - p.dataOut = dataOut; - p.dataCap = dataCap; - p.dataOffset = dataOffset; - p.arrayNum = 1; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusDeleteMultiSlotData(s32 user_id, s32 service_label, const std::string& ownerNpId, - const std::string& virtualUser, s64 ownerAccountId, - const std::vector& slotIds, - std::shared_ptr ctx) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusDeleteMultiSlotDataRequest proto; - proto.set_ownernpid(ownerNpId); - if (!virtualUser.empty()) { - proto.set_virtualuser(virtualUser); - } else if (ownerAccountId != 0) { - proto.set_owneraccountid(ownerAccountId); - } else if (ownerNpId.empty() || - Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { - proto.set_owneraccountid(static_cast(client->GetUserId())); - } - for (s32 s : slotIds) { - proto.add_slotids(s); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusDeleteMultiSlotData, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusDeleteMultiSlotData; - p.user_id = user_id; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusDeleteMultiSlotVariable(s32 user_id, s32 service_label, - const std::string& ownerNpId, - const std::string& virtualUser, s64 ownerAccountId, - const std::vector& slotIds, - std::shared_ptr ctx) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusDeleteMultiSlotVariableRequest proto; - proto.set_ownernpid(ownerNpId); - if (!virtualUser.empty()) { - proto.set_virtualuser(virtualUser); - } else if (ownerAccountId != 0) { - proto.set_owneraccountid(ownerAccountId); - } else if (ownerNpId.empty() || - Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { - proto.set_owneraccountid(static_cast(client->GetUserId())); - } - for (s32 s : slotIds) { - proto.add_slotids(s); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusDeleteMultiSlotVariable, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusDeleteMultiSlotVariable; - p.user_id = user_id; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusGetMultiUserDataStatus(s32 user_id, s32 service_label, s32 slotId, - const std::vector& ownerNpIds, - const std::vector& virtualUsers, - const std::vector& ownerAccountIds, - NpTus::OrbisNpTusDataStatus* statusOut, - NpTus::OrbisNpTusDataStatusA* statusAOut, u64 arrayNum, - std::shared_ptr ctx, - NpTus::OrbisNpTusDataStatusForCrossSave* statusCSOut) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusGetMultiUserDataStatusRequest proto; - proto.set_slotid(slotId); - for (const auto& vu : virtualUsers) { - proto.add_virtualusers(vu); - } - for (const auto& np : ownerNpIds) { - proto.add_ownernpids(np); - } - for (s64 acc : ownerAccountIds) { - proto.add_owneraccountids(acc); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetMultiUserDataStatus, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusGetMultiUserDataStatus; - p.user_id = user_id; - p.statusArray = statusOut; - p.statusArrayA = statusAOut; - p.statusArrayCS = statusCSOut; - p.arrayNum = arrayNum; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusGetMultiUserVariable(s32 user_id, s32 service_label, s32 slotId, - const std::vector& ownerNpIds, - const std::vector& virtualUsers, - const std::vector& ownerAccountIds, - NpTus::OrbisNpTusVariable* variablesOut, - NpTus::OrbisNpTusVariableA* variablesAOut, u64 arrayNum, - std::shared_ptr ctx, - NpTus::OrbisNpTusVariableForCrossSave* variablesCSOut) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusGetMultiUserVariableRequest proto; - proto.set_slotid(slotId); - for (const auto& vu : virtualUsers) { - proto.add_virtualusers(vu); - } - for (const auto& np : ownerNpIds) { - proto.add_ownernpids(np); - } - for (s64 acc : ownerAccountIds) { - proto.add_owneraccountids(acc); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetMultiUserVariable, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusGetMultiUserVariable; - p.user_id = user_id; - p.variableArray = variablesOut; - p.variableArrayA = variablesAOut; - p.variableArrayCS = variablesCSOut; - p.arrayNum = arrayNum; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusTryAndSetVariable(s32 user_id, s32 service_label, const std::string& ownerNpId, - const std::string& virtualUser, s64 ownerAccountId, s32 slotId, - s32 opeType, s64 value, bool hasCompare, s64 compareValue, - bool hasAuthorCheck, s64 isLastChangedAuthor, - const std::string& isLastChangedAuthorNpId, bool hasDateCheck, - u64 isLastChangedDate, NpTus::OrbisNpTusVariable* variableOut, - NpTus::OrbisNpTusVariableA* variableAOut, - std::shared_ptr ctx, - NpTus::OrbisNpTusVariableForCrossSave* variableCSOut) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusTryAndSetVariableRequest proto; - proto.set_ownernpid(ownerNpId); - proto.set_slotid(slotId); - proto.set_opetype(opeType); - proto.set_value(value); - // compareValue is optional: when absent the server compares against value - if (hasCompare) { - proto.set_hascompare(true); - proto.set_comparevalue(compareValue); - } - if (!virtualUser.empty()) { - proto.set_virtualuser(virtualUser); - } else if (ownerAccountId != 0) { - proto.set_owneraccountid(ownerAccountId); - } else if (ownerNpId.empty() || - Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { - proto.set_owneraccountid(static_cast(client->GetUserId())); - } - if (hasAuthorCheck) { - proto.set_hasauthorcheck(true); - proto.set_islastchangedauthor(isLastChangedAuthor); - if (!isLastChangedAuthorNpId.empty()) { - proto.set_islastchangedauthornpid(isLastChangedAuthorNpId); - } - } - if (hasDateCheck) { - proto.set_hasdatecheck(true); - proto.set_islastchangeddate(isLastChangedDate); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusTryAndSetVariable, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusTryAndSetVariable; - p.user_id = user_id; - p.variableArray = variableOut; - p.variableArrayA = variableAOut; - p.variableArrayCS = variableCSOut; - p.arrayNum = 1; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusAddAndGetVariable(s32 user_id, s32 service_label, const std::string& ownerNpId, - const std::string& virtualUser, s64 ownerAccountId, s32 slotId, - s64 inVariable, bool hasAuthorCheck, s64 isLastChangedAuthor, - const std::string& isLastChangedAuthorNpId, bool hasDateCheck, - u64 isLastChangedDate, NpTus::OrbisNpTusVariable* variableOut, - NpTus::OrbisNpTusVariableA* variableAOut, - std::shared_ptr ctx, - NpTus::OrbisNpTusVariableForCrossSave* variableCSOut) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusAddAndGetVariableRequest proto; - proto.set_ownernpid(ownerNpId); - proto.set_slotid(slotId); - proto.set_invalue(inVariable); - if (!virtualUser.empty()) { - proto.set_virtualuser(virtualUser); - } else if (ownerAccountId != 0) { - proto.set_owneraccountid(ownerAccountId); - } else if (ownerNpId.empty() || - Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { - proto.set_owneraccountid(static_cast(client->GetUserId())); - } - if (hasAuthorCheck) { - proto.set_hasauthorcheck(true); - proto.set_islastchangedauthor(isLastChangedAuthor); - if (!isLastChangedAuthorNpId.empty()) { - proto.set_islastchangedauthornpid(isLastChangedAuthorNpId); - } - } - if (hasDateCheck) { - proto.set_hasdatecheck(true); - proto.set_islastchangeddate(isLastChangedDate); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusAddAndGetVariable, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusAddAndGetVariable; - p.user_id = user_id; - p.variableArray = variableOut; - p.variableArrayA = variableAOut; - p.variableArrayCS = variableCSOut; - p.arrayNum = 1; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusGetFriendsDataStatus(s32 user_id, s32 service_label, s32 slotId, bool includeSelf, - s32 sortType, u32 max, - NpTus::OrbisNpTusDataStatus* statusOut, - NpTus::OrbisNpTusDataStatusA* statusAOut, u64 arrayNum, - std::shared_ptr ctx, u32 startOffset, - u32* hitsOut, - NpTus::OrbisNpTusDataStatusForCrossSave* statusCSOut) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - // Always targets the caller's own friend list, so there is no owner field. - shadnet::TusGetFriendsDataStatusRequest proto; - proto.set_slotid(slotId); - proto.set_includeself(includeSelf); - proto.set_sorttype(sortType); - proto.set_max(max); - proto.set_startoffset(startOffset); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetFriendsDataStatus, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusGetFriendsDataStatus; - p.user_id = user_id; - p.statusArray = statusOut; - p.statusArrayA = statusAOut; - p.statusArrayCS = statusCSOut; - p.arrayNum = arrayNum; - p.totalOut = hitsOut; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusGetFriendsVariable(s32 user_id, s32 service_label, s32 slotId, bool includeSelf, - s32 sortType, u32 max, NpTus::OrbisNpTusVariable* variablesOut, - NpTus::OrbisNpTusVariableA* variablesAOut, u64 arrayNum, - std::shared_ptr ctx, u32 startOffset, - u32* hitsOut, - NpTus::OrbisNpTusVariableForCrossSave* variablesCSOut) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - // Always targets the caller's own friend list, so there is no owner field. - shadnet::TusGetFriendsVariableRequest proto; - proto.set_slotid(slotId); - proto.set_includeself(includeSelf); - proto.set_sorttype(sortType); - proto.set_max(max); - proto.set_startoffset(startOffset); - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetFriendsVariable, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusGetFriendsVariable; - p.user_id = user_id; - p.variableArray = variablesOut; - p.variableArrayA = variablesAOut; - p.variableArrayCS = variablesCSOut; - p.arrayNum = arrayNum; - p.totalOut = hitsOut; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusGetMultiSlotDataStatus(s32 user_id, s32 service_label, - const std::string& ownerNpId, - const std::string& virtualUser, s64 ownerAccountId, - const std::vector& slotIds, - NpTus::OrbisNpTusDataStatus* statusOut, - NpTus::OrbisNpTusDataStatusA* statusAOut, u64 arrayNum, - std::shared_ptr ctx, - NpTus::OrbisNpTusDataStatusForCrossSave* statusCSOut) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusGetMultiSlotDataStatusRequest proto; - proto.set_ownernpid(ownerNpId); - if (!virtualUser.empty()) { - proto.set_virtualuser(virtualUser); - } else if (ownerAccountId != 0) { - proto.set_owneraccountid(ownerAccountId); - } else if (ownerNpId.empty() || - Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { - proto.set_owneraccountid(static_cast(client->GetUserId())); - } - for (s32 s : slotIds) { - proto.add_slotids(s); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetMultiSlotDataStatus, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusGetMultiSlotDataStatus; - p.user_id = user_id; - p.statusArray = statusOut; - p.statusArrayA = statusAOut; - p.statusArrayCS = statusCSOut; - p.arrayNum = arrayNum; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -s32 NpHandler::TusSetData(s32 user_id, s32 service_label, const std::string& ownerNpId, - const std::string& virtualUser, s64 ownerAccountId, s32 slotId, - const std::vector& blob, const std::vector& info, - bool hasAuthorCheck, s64 isLastChangedAuthor, - const std::string& isLastChangedAuthorNpId, bool hasDateCheck, - u64 isLastChangedDate, std::shared_ptr ctx) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - return ORBIS_NP_ERROR_SIGNED_OUT; - } - client = it->second; - } - shadnet::TusSetDataRequest proto; - proto.set_ownernpid(ownerNpId); - proto.set_slotid(slotId); - proto.set_data(blob.data(), blob.size()); - if (!info.empty()) { - proto.set_info(info.data(), info.size()); - } - if (!virtualUser.empty()) { - proto.set_virtualuser(virtualUser); - } else if (ownerAccountId != 0) { - proto.set_owneraccountid(ownerAccountId); - } else if (ownerNpId.empty() || - Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { - proto.set_owneraccountid(static_cast(client->GetUserId())); - } - if (hasAuthorCheck) { - proto.set_hasauthorcheck(true); - proto.set_islastchangedauthor(isLastChangedAuthor); - if (!isLastChangedAuthorNpId.empty()) { - proto.set_islastchangedauthornpid(isLastChangedAuthorNpId); - } - } - if (hasDateCheck) { - proto.set_hasdatecheck(true); - proto.set_islastchangeddate(isLastChangedDate); - } - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - } - const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusSetData, - BuildTusPayload(com_id, proto.SerializeAsString())); - std::lock_guard lock(m_mutex_pending_tus); - PendingTusRequest p; - p.req = std::move(ctx); - p.cmd = ShadNet::CommandType::TusSetData; - p.user_id = user_id; - m_pending_tus.emplace(pkt_id, std::move(p)); - return ORBIS_OK; -} - -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 >= 100 && cmd_val <= 200) { - NpMatching2::OnMatchingReply(cmd, pkt_id, error, body); - } else if (cmd_val >= 201 && cmd_val <= 300) { - OnTusReply(user_id, cmd, pkt_id, error, body); - } else if (cmd_val >= 301 && cmd_val <= 400) { - OnTrophyReply(user_id, cmd, pkt_id, error, body); - } else if (cmd == ShadNet::CommandType::LookupOnlineId) { - OnLookupReply(user_id, cmd, pkt_id, error, body); - } else { - OnScoreReply(user_id, cmd, pkt_id, error, body); - } -} - -void NpHandler::ReportTrophyUnlock(s32 user_id, s32 service_label, s32 trophy_id, u64 timestamp) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_DEBUG(NpHandler, "no shadNet session for user_id={}; skipping", user_id); - return; - } - client = it->second; - } - if (!client) { - LOG_DEBUG(NpHandler, "null client for user_id={}; skipping", user_id); - return; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "not authenticated; trophy {} not mirrored", trophy_id); - return; - } - if (!client->IsTrophiesEnabled()) { - LOG_WARNING(NpHandler, - "server has not advertised trophy support; trophy {} " - "not mirrored", - trophy_id); - return; - } - - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_WARNING(NpHandler, - "no valid NP Communication ID for service_label={}; " - "skipping trophy {}", - service_label, trophy_id); - return; - } - - const u64 pkt_id = client->UnlockTrophy(com_id, trophy_id, timestamp); - LOG_INFO(NpHandler, "user_id={} trophy={} com_id='{}' pkt_id={}", user_id, trophy_id, com_id, - pkt_id); -} - -void NpHandler::SyncTrophies( - s32 user_id, s32 service_label, const std::vector>& local_trophies, - std::function>&)> on_merged) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_DEBUG(NpHandler, "no shadNet session for user_id={}; skipping", user_id); - return; - } - client = it->second; - } - if (!client) { - LOG_DEBUG(NpHandler, "null client for user_id={}; skipping", user_id); - return; - } - if (!client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "not authenticated; skipping sync"); - return; - } - if (!client->IsTrophiesEnabled()) { - LOG_WARNING(NpHandler, "server has not advertised trophy support; " - "skipping sync"); - return; - } - - const std::string com_id = GetNpCommId(service_label); - if (!IsValidNpCommId(com_id)) { - LOG_WARNING(NpHandler, "no valid NP Communication ID for service_label={}; skipping", - service_label); - return; - } - - const u64 pkt_id = client->SyncTrophies(com_id, local_trophies); - if (on_merged) { - std::lock_guard lock(m_mutex_pending_trophy); - m_pending_trophy.emplace(pkt_id, std::move(on_merged)); - } - LOG_INFO(NpHandler, "user_id={} uploading {} com_id='{}' pkt_id={}", user_id, - local_trophies.size(), com_id, pkt_id); -} - -void NpHandler::OnTrophyReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, - ShadNet::ErrorType error, const std::vector& body) { - if (cmd == ShadNet::CommandType::UnlockTrophy) { - if (error != ShadNet::ErrorType::NoError) { - LOG_WARNING(NpHandler, "UnlockTrophy rejected by server: error={}", - static_cast(error)); - } - return; - } - if (cmd != ShadNet::CommandType::SyncTrophies) - return; - - std::function>&)> on_merged; - { - std::lock_guard lock(m_mutex_pending_trophy); - auto it = m_pending_trophy.find(pkt_id); - if (it == m_pending_trophy.end()) - return; - on_merged = std::move(it->second); - m_pending_trophy.erase(it); - } - - if (error != ShadNet::ErrorType::NoError) { - LOG_WARNING(NpHandler, "SyncTrophies failed: error={}", static_cast(error)); - return; // leave local trophies exactly as they were - } - - // Reply body is u32 LE blob size followed by the proto. - if (body.size() < 4) { - LOG_WARNING(NpHandler, "SyncTrophies: truncated reply"); - return; - } - const u32 blob_sz = static_cast(body[0]) | (static_cast(body[1]) << 8) | - (static_cast(body[2]) << 16) | (static_cast(body[3]) << 24); - if (body.size() < 4 + blob_sz) { - LOG_WARNING(NpHandler, "SyncTrophies: reply shorter than declared blob"); - return; - } - - shadnet::SyncTrophiesReply pb; - if (!pb.ParseFromArray(body.data() + 4, static_cast(blob_sz))) { - LOG_WARNING(NpHandler, "could not parse reply"); - return; - } - - std::vector> merged; - merged.reserve(pb.trophies_size()); - for (const auto& t : pb.trophies()) - merged.emplace_back(t.trophyid(), t.timestamp()); - - LOG_INFO(NpHandler, "server holds {} trophies for user_id={}", merged.size(), user_id); - on_merged(merged); -} - -void NpHandler::ResolveOnlineId( - s32 user_id, const std::string& online_id, - std::function - on_result) { - std::shared_ptr client; - { - std::lock_guard lock(m_mutex_clients); - auto it = m_clients.find(user_id); - if (it == m_clients.end()) { - LOG_WARNING(NpHandler, "No shadNet session for user_id={}", user_id); - if (on_result) { - on_result(ORBIS_NP_ERROR_SIGNED_OUT, 0, {}); - } - return; - } - client = it->second; - } - if (!client || !client->IsAuthenticated()) { - LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); - if (on_result) { - on_result(ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN, 0, {}); - } - return; - } - if (online_id.empty()) { - if (on_result) { - on_result(ORBIS_NP_COMMUNITY_SERVER_ERROR_NO_SUCH_USER_NPID, 0, {}); - } - return; - } - - const u64 pkt_id = client->LookupOnlineId(online_id); - if (on_result) { - std::lock_guard lock(m_mutex_pending_lookup); - PendingLookupRequest pending; - pending.user_id = user_id; - pending.on_result = std::move(on_result); - m_pending_lookup.emplace(pkt_id, std::move(pending)); - } - LOG_INFO(NpHandler, "user_id={} onlineId='{}' pkt_id={}", user_id, online_id, pkt_id); -} - -void NpHandler::OnLookupReply(s32 user_id, ShadNet::CommandType /*cmd*/, u64 pkt_id, - ShadNet::ErrorType error, const std::vector& body) { - std::function on_result; - { - std::lock_guard lock(m_mutex_pending_lookup); - auto it = m_pending_lookup.find(pkt_id); - if (it == m_pending_lookup.end()) { - return; // no waiter (already flushed by a disconnect, or a stray reply) - } - on_result = std::move(it->second.on_result); - m_pending_lookup.erase(it); - } - if (!on_result) { - return; - } - - if (error == ShadNet::ErrorType::NotFound) { - LOG_INFO(NpHandler, "LookupOnlineId: user_id={} pkt_id={} -> not found", user_id, pkt_id); - on_result(ORBIS_NP_COMMUNITY_SERVER_ERROR_NO_SUCH_USER_NPID, 0, {}); - return; - } - if (error == ShadNet::ErrorType::InvalidInput) { - on_result(ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT, 0, {}); - return; - } - if (error != ShadNet::ErrorType::NoError) { - LOG_WARNING(NpHandler, "LookupOnlineId failed: error={}", static_cast(error)); - on_result(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE, 0, {}); - return; - } - - // Reply body is u32 LE blob size followed by the proto. - if (body.size() < 4) { - LOG_WARNING(NpHandler, "LookupOnlineId: truncated reply"); - on_result(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE, 0, {}); - return; - } - const u32 blob_sz = static_cast(body[0]) | (static_cast(body[1]) << 8) | - (static_cast(body[2]) << 16) | (static_cast(body[3]) << 24); - if (body.size() < 4 + blob_sz) { - LOG_WARNING(NpHandler, "LookupOnlineId: reply shorter than declared blob"); - on_result(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE, 0, {}); - return; - } - - shadnet::LookupOnlineIdReply pb; - if (!pb.ParseFromArray(body.data() + 4, static_cast(blob_sz))) { - LOG_WARNING(NpHandler, "LookupOnlineId: could not parse reply"); - on_result(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE, 0, {}); - return; - } - - LOG_INFO(NpHandler, "LookupOnlineId: user_id={} pkt_id={} -> accountId={} npid='{}'", user_id, - pkt_id, pb.account_id(), pb.npid()); - on_result(ORBIS_OK, pb.account_id(), pb.npid()); -} - -void NpHandler::OnScoreReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, - ShadNet::ErrorType error, const std::vector& body) { - PendingScoreRequest pending; - { - std::lock_guard lock(m_mutex_pending_score); - auto it = m_pending_score.find(pkt_id); - if (it == m_pending_score.end()) { - LOG_WARNING(NpHandler, "no pending request for pkt_id={} cmd={}", pkt_id, - static_cast(cmd)); - return; - } - pending = std::move(it->second); - m_pending_score.erase(it); - } - auto& req = pending.req; - - // Server-side errors take precedence over success-path parsing. - if (error != ShadNet::ErrorType::NoError) { - s32 orbis_err = ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE; - switch (error) { - case ShadNet::ErrorType::ScoreNotBest: - orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_NOT_BEST_SCORE; - break; - case ShadNet::ErrorType::ScoreInvalid: - orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_INVALID_SCORE; - break; - case ShadNet::ErrorType::NotFound: - orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_RANKING_BOARD_MASTER_NOT_FOUND; - break; - case ShadNet::ErrorType::Unauthorized: - orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_FORBIDDEN; - break; - case ShadNet::ErrorType::DbFail: - orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_INTERNAL_SERVER_ERROR; - break; - case ShadNet::ErrorType::Malformed: - case ShadNet::ErrorType::Invalid: - case ShadNet::ErrorType::InvalidInput: - orbis_err = ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE; - break; - default: - break; - } - LOG_WARNING(NpHandler, "user_id={} pkt_id={} cmd={} server_error={} → orbis_err={:#x}", - user_id, pkt_id, static_cast(cmd), static_cast(error), orbis_err); - req->SetResult(orbis_err); - return; - } - - // Per-command success-path parsing. - switch (cmd) { - case ShadNet::CommandType::RecordScore: { - // Reply body = u32 LE rank. - if (req->tmpRankOut != nullptr && body.size() >= 4) { - const u32 rank = static_cast(body[0]) | (static_cast(body[1]) << 8) | - (static_cast(body[2]) << 16) | (static_cast(body[3]) << 24); - *req->tmpRankOut = rank; - LOG_INFO(NpHandler, "RecordScore user_id={} pkt_id={} rank={}", user_id, pkt_id, rank); - } - req->SetResult(ORBIS_OK); - break; - } - - case ShadNet::CommandType::RecordScoreData: { - // Reply body is empty on success — error byte already consumed by - // the caller. Server-side errors are mapped to ErrorType values - // (NotFound / ScoreInvalid / ScoreHasData) which arrive as the - // packet's error byte; if we got here, error == NoError. - LOG_INFO(NpHandler, "RecordScoreData user_id={} pkt_id={} ok", user_id, pkt_id); - req->SetResult(ORBIS_OK); - break; - } - - case ShadNet::CommandType::GetScoreData: - case ShadNet::CommandType::GetScoreGameDataByAccId: { - const char* cmd_name = (cmd == ShadNet::CommandType::GetScoreData) - ? "GetScoreData" - : "GetScoreGameDataByAccId"; - if (body.size() < 4) { - LOG_ERROR(NpHandler, "{} body too small ({})", cmd_name, body.size()); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - const u32 stored_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | - (static_cast(body[2]) << 16) | - (static_cast(body[3]) << 24); - if (static_cast(4) + stored_size > body.size()) { - LOG_ERROR(NpHandler, "{} blob size {} exceeds body size {}", cmd_name, stored_size, - body.size()); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - - // Always populate *totalSizeOut with the actual stored size on - // the server, even when truncating into a smaller dataOut buffer. - // Lets the caller know if they undersized their buffer and need - // to retry with a larger one. - if (pending.totalSizeOut != nullptr) { - *pending.totalSizeOut = stored_size; - } - if (pending.dataOut != nullptr && pending.recvSize > 0) { - const u64 copy_n = std::min(static_cast(stored_size), pending.recvSize); - std::memcpy(pending.dataOut, body.data() + 4, static_cast(copy_n)); - if (copy_n < stored_size) { - LOG_WARNING(NpHandler, "{} truncated user_id={} pkt_id={} stored={} recv={}", - cmd_name, user_id, pkt_id, stored_size, pending.recvSize); - } - } - LOG_INFO(NpHandler, "{} user_id={} pkt_id={} storedSize={} recvSize={}", cmd_name, user_id, - pkt_id, stored_size, pending.recvSize); - req->SetResult(ORBIS_OK); - break; - } - - case ShadNet::CommandType::GetBoardInfos: { - if (body.size() < 4) { - LOG_ERROR(NpHandler, "GetBoardInfos body too small ({})", body.size()); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - const u32 proto_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | - (static_cast(body[2]) << 16) | - (static_cast(body[3]) << 24); - if (static_cast(4) + proto_size > body.size()) { - LOG_ERROR(NpHandler, "GetBoardInfos proto size {} exceeds body size {}", proto_size, - body.size()); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - shadnet::BoardInfo bi; - if (!bi.ParseFromArray(body.data() + 4, static_cast(proto_size))) { - LOG_ERROR(NpHandler, "GetBoardInfos proto parse failed"); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - if (pending.boardInfo != nullptr) { - pending.boardInfo->rankLimit = bi.ranklimit(); - pending.boardInfo->updateMode = bi.updatemode(); - pending.boardInfo->sortMode = bi.sortmode(); - pending.boardInfo->uploadNumLimit = bi.uploadnumlimit(); - pending.boardInfo->uploadSizeLimit = bi.uploadsizelimit(); - } - LOG_INFO(NpHandler, - "GetBoardInfos user_id={} pkt_id={} rankLimit={} updateMode={} " - "sortMode={} uploadNumLimit={} uploadSizeLimit={}", - user_id, pkt_id, bi.ranklimit(), bi.updatemode(), bi.sortmode(), - bi.uploadnumlimit(), bi.uploadsizelimit()); - req->SetResult(ORBIS_OK); - break; - } - - case ShadNet::CommandType::GetScoreNpid: { - // Reply body = u32 LE proto size + GetScoreResponse proto bytes. - if (body.size() < 4) { - LOG_ERROR(NpHandler, "GetScoreNpid body too small ({})", body.size()); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - const u32 proto_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | - (static_cast(body[2]) << 16) | - (static_cast(body[3]) << 24); - if (static_cast(4) + proto_size > body.size()) { - LOG_ERROR(NpHandler, "GetScoreNpid proto size {} exceeds body size {}", proto_size, - body.size()); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - shadnet::GetScoreResponse resp; - if (!resp.ParseFromArray(body.data() + 4, static_cast(proto_size))) { - LOG_ERROR(NpHandler, "GetScoreNpid proto parse failed"); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - const u32 found = FillRankArrayFromProto(resp, pending.requestedNpIds, pending.rankArray, - pending.commentArray, pending.infoArray); - if (pending.lastSortDate != nullptr) { - pending.lastSortDate->tick = resp.lastsortdate(); - } - if (pending.totalRecord != nullptr) { - *pending.totalRecord = resp.totalrecord(); - } - LOG_INFO(NpHandler, "GetScoreNpid user_id={} pkt_id={} found={}/{} totalRecord={}", user_id, - pkt_id, found, pending.arrayNum, resp.totalrecord()); - req->SetResult(static_cast(found)); - break; - } - case ShadNet::CommandType::GetScoreAccountId: { - // Reply body = u32 LE proto size + GetScoreResponse proto bytes. - // Index-aligned with the request's accountIds[] (empty-npid sentinel - // marks "no score on this board"), same contract as GetScoreNpid. - if (body.size() < 4) { - LOG_ERROR(NpHandler, "GetScoreAccountId body too small ({})", body.size()); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - const u32 proto_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | - (static_cast(body[2]) << 16) | - (static_cast(body[3]) << 24); - if (static_cast(4) + proto_size > body.size()) { - LOG_ERROR(NpHandler, "GetScoreAccountId proto size {} exceeds body size {}", proto_size, - body.size()); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - shadnet::GetScoreResponse resp; - if (!resp.ParseFromArray(body.data() + 4, static_cast(proto_size))) { - LOG_ERROR(NpHandler, "GetScoreAccountId proto parse failed"); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - const u32 found = - FillPlayerRankArrayAFromProto(resp, pending.arrayNum, pending.aPlayerRankArray, - pending.commentArray, pending.infoArray); - if (pending.lastSortDate != nullptr) { - pending.lastSortDate->tick = resp.lastsortdate(); - } - if (pending.totalRecord != nullptr) { - *pending.totalRecord = resp.totalrecord(); - } - LOG_INFO(NpHandler, "GetScoreAccountId user_id={} pkt_id={} found={}/{} totalRecord={}", - user_id, pkt_id, found, pending.arrayNum, resp.totalrecord()); - req->SetResult(static_cast(found)); - break; - } - - case ShadNet::CommandType::GetScoreRange: - case ShadNet::CommandType::GetScoreFriends: { - const char* cmd_name = - (cmd == ShadNet::CommandType::GetScoreRange) ? "GetScoreRange" : "GetScoreFriends"; - if (body.size() < 4) { - LOG_ERROR(NpHandler, "{} body too small ({})", cmd_name, body.size()); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - const u32 proto_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | - (static_cast(body[2]) << 16) | - (static_cast(body[3]) << 24); - if (static_cast(4) + proto_size > body.size()) { - LOG_ERROR(NpHandler, "{} proto size {} exceeds body size {}", cmd_name, proto_size, - body.size()); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - shadnet::GetScoreResponse resp; - if (!resp.ParseFromArray(body.data() + 4, static_cast(proto_size))) { - LOG_ERROR(NpHandler, "{} proto parse failed", cmd_name); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } - u32 found = 0; - if (pending.aRankArray != nullptr) { - found = FillPlainRankArrayAFromProto(resp, pending.arrayNum, pending.aRankArray, - pending.commentArray, pending.infoArray); - } else { - found = FillPlainRankArrayFromProto(resp, pending.arrayNum, pending.plainRankArray, - pending.commentArray, pending.infoArray); - } - if (pending.lastSortDate != nullptr) { - pending.lastSortDate->tick = resp.lastsortdate(); - } - if (pending.totalRecord != nullptr) { - *pending.totalRecord = resp.totalrecord(); - } - LOG_INFO(NpHandler, "{} user_id={} pkt_id={} found={}/{} totalRecord={}{}", cmd_name, - user_id, pkt_id, found, pending.arrayNum, resp.totalrecord(), - pending.aRankArray != nullptr ? " (A-variant)" : ""); - req->SetResult(static_cast(found)); - break; - } - - default: - LOG_WARNING(NpHandler, "unexpected cmd={} pkt_id={}", static_cast(cmd), pkt_id); - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - break; - } -} - -void NpHandler::OnTusReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, - ShadNet::ErrorType error, const std::vector& body) { - PendingTusRequest pending; - { - std::lock_guard lock(m_mutex_pending_tus); - auto it = m_pending_tus.find(pkt_id); - if (it == m_pending_tus.end()) { - LOG_WARNING(NpHandler, "no pending request for pkt_id={} cmd={}", pkt_id, - static_cast(cmd)); - return; - } - pending = std::move(it->second); - m_pending_tus.erase(it); - } - auto& req = pending.req; - - if (error != ShadNet::ErrorType::NoError) { - s32 orbis_err = ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE; - switch (error) { - case ShadNet::ErrorType::Unauthorized: - orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_FORBIDDEN; - break; - case ShadNet::ErrorType::NotFound: - // Owner npid did not resolve to a registered user. - orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_USER_PROFILE_NOT_FOUND; - break; - case ShadNet::ErrorType::DbFail: - orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_INTERNAL_SERVER_ERROR; - break; - case ShadNet::ErrorType::CondFail: - orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_CONDITIONS_NOT_SATISFIED; - break; - default: - break; - } - LOG_WARNING(NpHandler, "user_id={} pkt_id={} cmd={} server_error={} -> orbis {:#x}", - user_id, pkt_id, static_cast(cmd), static_cast(error), - static_cast(orbis_err)); - req->SetResult(orbis_err); - return; - } - - auto parseTusBody = [&](auto& msg) -> bool { - if (body.size() < 4) { - LOG_ERROR(NpHandler, "OnTusReply: cmd={} body too small ({})", static_cast(cmd), - body.size()); - return false; - } - const u32 proto_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | - (static_cast(body[2]) << 16) | - (static_cast(body[3]) << 24); - if (static_cast(4) + proto_size > body.size()) { - LOG_ERROR(NpHandler, "OnTusReply: cmd={} proto size {} exceeds body size {}", - static_cast(cmd), proto_size, body.size()); - return false; - } - if (!msg.ParseFromArray(body.data() + 4, static_cast(proto_size))) { - LOG_ERROR(NpHandler, "OnTusReply: cmd={} proto parse failed", static_cast(cmd)); - return false; - } - return true; - }; - - auto fillVariable = [](NpTus::OrbisNpTusVariable& v, const shadnet::TusVariable& s) { - v = NpTus::OrbisNpTusVariable{}; - CopyNpHandle(v.ownerId, s.ownernpid()); - v.hasData = s.set() ? 1 : 0; - v.lastChangedDate.tick = s.lastchangeddate(); - CopyNpHandle(v.lastChangedAuthorId, s.lastchangedauthornpid()); - v.variable = s.variable(); - v.oldVariable = s.oldvariable(); - v.ownerAccountId = static_cast(s.owneraccountid()); - v.lastChangedAuthorAccountId = - static_cast(s.lastchangedauthoraccountid()); - }; - auto fillVariableA = [](NpTus::OrbisNpTusVariableA& v, const shadnet::TusVariable& s) { - v = NpTus::OrbisNpTusVariableA{}; - std::memcpy(v.ownerId.data, s.ownernpid().data(), - std::min(s.ownernpid().size(), sizeof(v.ownerId.data))); - v.hasData = s.set() ? 1 : 0; - v.lastChangedDate.tick = s.lastchangeddate(); - std::memcpy(v.lastChangedAuthorId.data, s.lastchangedauthornpid().data(), - std::min(s.lastchangedauthornpid().size(), sizeof(v.lastChangedAuthorId.data))); - v.variable = s.variable(); - v.oldVariable = s.oldvariable(); - v.ownerAccountId = static_cast(s.owneraccountid()); - v.lastChangedAuthorAccountId = - static_cast(s.lastchangedauthoraccountid()); - }; - auto fillVariableForCrossSave = [](NpTus::OrbisNpTusVariableForCrossSave& v, - const shadnet::TusVariable& s) { - v = NpTus::OrbisNpTusVariableForCrossSave{}; - CopyNpHandle(v.ownerId, s.ownernpid()); - v.hasData = s.set() ? 1 : 0; - v.lastChangedDate.tick = s.lastchangeddate(); - CopyNpHandle(v.lastChangedAuthorId, s.lastchangedauthornpid()); - v.variable = s.variable(); - v.oldVariable = s.oldvariable(); - v.ownerAccountId = static_cast(s.owneraccountid()); - v.lastChangedAuthorAccountId = - static_cast(s.lastchangedauthoraccountid()); - }; - auto fillStatus = [](NpTus::OrbisNpTusDataStatus& d, const shadnet::TusDataStatus& s) { - d = NpTus::OrbisNpTusDataStatus{}; - CopyNpHandle(d.ownerId, s.ownernpid()); - d.hasData = s.set() ? 1 : 0; - d.lastChangedDate.tick = s.lastchangeddate(); - CopyNpHandle(d.lastChangedAuthorId, s.lastchangedauthornpid()); - d.dataSize = s.datasize(); - d.info.size = s.info().size(); - const size_t cp = std::min(s.info().size(), sizeof(d.info.data)); - std::memcpy(d.info.data, s.info().data(), cp); - }; - auto fillStatusA = [](NpTus::OrbisNpTusDataStatusA& d, const shadnet::TusDataStatus& s) { - d = NpTus::OrbisNpTusDataStatusA{}; - std::memcpy(d.ownerId.data, s.ownernpid().data(), - std::min(s.ownernpid().size(), sizeof(d.ownerId.data))); - d.hasData = s.set() ? 1 : 0; - d.lastChangedDate.tick = s.lastchangeddate(); - std::memcpy(d.lastChangedAuthorId.data, s.lastchangedauthornpid().data(), - std::min(s.lastchangedauthornpid().size(), sizeof(d.lastChangedAuthorId.data))); - d.dataSize = s.datasize(); - d.info.size = s.info().size(); - const size_t cp = std::min(s.info().size(), sizeof(d.info.data)); - std::memcpy(d.info.data, s.info().data(), cp); - d.ownerAccountId = static_cast(s.owneraccountid()); - d.lastChangedAuthorAccountId = - static_cast(s.lastchangedauthoraccountid()); - }; - auto fillStatusForCrossSave = [](NpTus::OrbisNpTusDataStatusForCrossSave& d, - const shadnet::TusDataStatus& s) { - d = NpTus::OrbisNpTusDataStatusForCrossSave{}; - CopyNpHandle(d.ownerId, s.ownernpid()); - d.hasData = s.set() ? 1 : 0; - d.lastChangedDate.tick = s.lastchangeddate(); - CopyNpHandle(d.lastChangedAuthorId, s.lastchangedauthornpid()); - d.dataSize = s.datasize(); - d.info.size = s.info().size(); - const size_t cp = std::min(s.info().size(), sizeof(d.info.data)); - std::memcpy(d.info.data, s.info().data(), cp); - d.ownerAccountId = static_cast(s.owneraccountid()); - d.lastChangedAuthorAccountId = - static_cast(s.lastchangedauthoraccountid()); - }; - - switch (cmd) { - case ShadNet::CommandType::TusGetMultiSlotVariable: - case ShadNet::CommandType::TusTryAndSetVariable: - case ShadNet::CommandType::TusGetMultiUserVariable: - case ShadNet::CommandType::TusGetFriendsVariable: - case ShadNet::CommandType::TusAddAndGetVariable: { - shadnet::TusVariableResponse resp; - if (!parseTusBody(resp)) { - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - return; - } - u64 filled = 0; - if (pending.variableArrayA) { - filled = std::min(pending.arrayNum, resp.variables_size()); - for (u64 i = 0; i < filled; ++i) { - fillVariableA(pending.variableArrayA[i], resp.variables(static_cast(i))); - } - } else if (pending.variableArrayCS) { - filled = std::min(pending.arrayNum, resp.variables_size()); - for (u64 i = 0; i < filled; ++i) { - fillVariableForCrossSave(pending.variableArrayCS[i], - resp.variables(static_cast(i))); - } - } else if (pending.variableArray) { - filled = std::min(pending.arrayNum, resp.variables_size()); - for (u64 i = 0; i < filled; ++i) { - fillVariable(pending.variableArray[i], resp.variables(static_cast(i))); - } - } - // GetMultiSlotVariable / GetMultiUserVariable return the count of variables read; - // AddAndGet / TryAndSet return 0 on normal termination (value is in the struct). - if (pending.totalOut) { - // OrbisNpTusGetFriendsVariableOptParam::hits - total registered - // friends before startSerialRank/arrayNum are applied. - *pending.totalOut = - resp.total() ? resp.total() : static_cast(resp.variables_size()); - } - if (cmd == ShadNet::CommandType::TusGetMultiSlotVariable || - cmd == ShadNet::CommandType::TusGetMultiUserVariable || - cmd == ShadNet::CommandType::TusGetFriendsVariable) { - req->SetResult(static_cast(filled)); - } - break; - } - case ShadNet::CommandType::TssGetData: { - shadnet::TssGetDataResponse resp; - if (!parseTusBody(resp)) { - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - return; - } - u64 recv = 0; - if (pending.dataOut && !resp.data().empty()) { - recv = std::min(pending.dataCap, resp.data().size()); - std::memcpy(pending.dataOut, resp.data().data(), recv); - } - if (pending.tssContentLengthOut) { - *pending.tssContentLengthOut = resp.contentlength(); - } - if (pending.tssStatusOut) { - *pending.tssStatusOut = NpTus::OrbisNpTssDataStatus{}; - pending.tssStatusOut->modified.tick = resp.lastmodified(); - pending.tssStatusOut->status = - static_cast(resp.statuscodetype()); - pending.tssStatusOut->contentLength = resp.contentlength(); - } - req->SetResult(ORBIS_OK); - return; - } - case ShadNet::CommandType::TusGetData: { - shadnet::TusGetDataResponse resp; - if (!parseTusBody(resp)) { - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - return; - } - u64 recv = 0; - const u64 blob_size = resp.data().size(); - if (pending.dataOut && pending.dataOffset < blob_size) { - const u64 avail = blob_size - pending.dataOffset; - recv = std::min(pending.dataCap, avail); - std::memcpy(pending.dataOut, resp.data().data() + pending.dataOffset, recv); - } - if (pending.statusArrayA) { - NpTus::OrbisNpTusDataStatusA tmp{}; - fillStatusA(tmp, resp.status()); - if (recv) { - tmp.data = pending.dataOut; - } - const u64 cap = - pending.statusCap ? std::min(pending.statusCap, sizeof(tmp)) : sizeof(tmp); - std::memcpy(pending.statusArrayA, &tmp, cap); - } else if (pending.statusArrayCS) { - NpTus::OrbisNpTusDataStatusForCrossSave tmp{}; - fillStatusForCrossSave(tmp, resp.status()); - if (recv) { - tmp.data = pending.dataOut; - } - const u64 cap = - pending.statusCap ? std::min(pending.statusCap, sizeof(tmp)) : sizeof(tmp); - std::memcpy(pending.statusArrayCS, &tmp, cap); - } else if (pending.statusArray) { - NpTus::OrbisNpTusDataStatus tmp{}; - fillStatus(tmp, resp.status()); - if (recv) { - tmp.data = pending.dataOut; - } - const u64 cap = - pending.statusCap ? std::min(pending.statusCap, sizeof(tmp)) : sizeof(tmp); - std::memcpy(pending.statusArray, &tmp, cap); - } - req->SetResult(static_cast(recv)); - return; - } - case ShadNet::CommandType::TusGetMultiSlotDataStatus: - case ShadNet::CommandType::TusGetMultiUserDataStatus: - case ShadNet::CommandType::TusGetFriendsDataStatus: { - shadnet::TusDataStatusResponse resp; - if (!parseTusBody(resp)) { - req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); - return; - } - if (pending.statusArrayA) { - const u64 n = std::min(pending.arrayNum, resp.statuses_size()); - for (u64 i = 0; i < n; ++i) { - fillStatusA(pending.statusArrayA[i], resp.statuses(static_cast(i))); - } - } else if (pending.statusArrayCS) { - const u64 n = std::min(pending.arrayNum, resp.statuses_size()); - for (u64 i = 0; i < n; ++i) { - fillStatusForCrossSave(pending.statusArrayCS[i], - resp.statuses(static_cast(i))); - } - } else if (pending.statusArray) { - const u64 n = std::min(pending.arrayNum, resp.statuses_size()); - for (u64 i = 0; i < n; ++i) { - fillStatus(pending.statusArray[i], resp.statuses(static_cast(i))); - } - } - if (pending.totalOut) { - // hits = total registered friends before offset/cap - *pending.totalOut = - resp.total() ? resp.total() : static_cast(resp.statuses_size()); - } - if (cmd == ShadNet::CommandType::TusGetFriendsDataStatus || - cmd == ShadNet::CommandType::TusGetMultiSlotDataStatus || - cmd == ShadNet::CommandType::TusGetMultiUserDataStatus) { - // These calls return the number of statuses read (>= 0), not ORBIS_OK. - const s32 cnt = static_cast(std::min(pending.arrayNum, resp.statuses_size())); - req->SetResult(cnt); - return; - } - break; - } - default: - // Set*/Delete* carry no payload to fill. - LOG_DEBUG(NpHandler, "cmd={} ok (no payload)", static_cast(cmd)); - break; - } - req->SetResult(ORBIS_OK); -} - -} // namespace Libraries::Np diff --git a/src/core/libraries/np/np_handler/np_handler.cpp b/src/core/libraries/np/np_handler/np_handler.cpp new file mode 100644 index 000000000..b21cefbad --- /dev/null +++ b/src/core/libraries/np/np_handler/np_handler.cpp @@ -0,0 +1,754 @@ +// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include "common/elf_info.h" +#include "common/logging/log.h" +#include "core/emulator_settings.h" +#include "core/libraries/net/net_p2p.h" +#include "core/libraries/net/net_upnp.h" +#include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_handler/np_handler.h" +#include "core/libraries/np/np_manager.h" +#include "core/libraries/np/np_matching2/np_matching2_internal.h" +#include "core/libraries/np/np_matching2/np_matching2_mm.h" +#include "core/user_settings.h" +#include "imgui/shadnet_notifications_layer.h" +#include "shadnet.pb.h" +#include "shadnet/server_probe.h" + +namespace Libraries::Np { + +NpHandler::NpHandler() + : m_matching2_contexts( + std::unique_ptr(new NpMatching2::ContextManager())), + m_matching2_state(std::make_unique()) {} + +NpHandler::~NpHandler() = default; + +NpHandler& NpHandler::GetInstance() { + static NpHandler s_instance; + return s_instance; +} + +NpMatching2::ContextManager& NpHandler::GetMatching2ContextManager() { + return *m_matching2_contexts; +} + +NpMatching2::NpMatching2State& NpHandler::GetMatching2State() { + return *m_matching2_state; +} + +NpHandler::Matching2CacheGuard NpHandler::LockMatching2Cache( + NpMatching2::OrbisNpMatching2ContextId ctx_id) { + std::shared_ptr cache; + { + std::lock_guard lock(m_mutex_matching2_cache); + auto& entry = m_matching2_cache[ctx_id]; + if (!entry) { + entry = std::make_shared(); + } + cache = entry; + } + return Matching2CacheGuard(std::move(cache)); +} + +void NpHandler::ResetMatching2Cache(NpMatching2::OrbisNpMatching2ContextId ctx_id) { + std::lock_guard lock(m_mutex_matching2_cache); + m_matching2_cache.erase(ctx_id); +} + +void NpHandler::ResetMatching2Caches() { + std::lock_guard lock(m_mutex_matching2_cache); + m_matching2_cache.clear(); +} + +std::pair NpHandler::ParseServerAddress() const { + const std::string server_str = EmulatorSettings.GetShadNetServer(); + u16 port = 31313; // default port + if (server_str.empty()) { + LOG_ERROR(NpHandler, + "shadNet server address is empty,set the shadNet server field in settings. " + "Connection will fail."); + return {std::string{}, port}; + } + std::string host = server_str; + const auto colon = server_str.rfind(':'); + if (colon != std::string::npos) { + host = server_str.substr(0, colon); + const std::string port_str = server_str.substr(colon + 1); + try { + port = static_cast(std::stoi(port_str)); + } catch (const std::exception&) { + LOG_WARNING(NpHandler, + "shadNet server port '{}' is not a valid number; using default {}", + port_str, port); + } + } + return {host, port}; +} + +bool NpHandler::ConnectUserById(s32 user_id) { + if (!EmulatorSettings.IsShadNetEnabled()) + return false; + + { + std::lock_guard lock(m_mutex_clients); + if (m_clients.find(user_id) != m_clients.end()) + return false; // already connected + } + + const User* u = UserManagement.GetUserByID(user_id); + if (!u) + return false; + if (!u->shadnet_enabled) { + LOG_DEBUG(NpHandler, "user_id={} ('{}') shadNet disabled,skipping", u->user_id, + u->user_name); + return false; + } + if (u->shadnet_npid.empty() || u->shadnet_password.empty()) { + LOG_WARNING(NpHandler, "user_id={} ('{}') shadNet enabled but credentials missing,skipping", + u->user_id, u->user_name); + return false; + } + + const auto [host, port] = ParseServerAddress(); + return ConnectUser(u->user_id, host, port, u->shadnet_npid, u->shadnet_password, + u->shadnet_token); +} + +void NpHandler::StartWorker() { + bool expected = false; + if (m_worker_running.compare_exchange_strong(expected, true)) { + m_worker_thread = std::thread(&NpHandler::WorkerThread, this); + } +} + +void NpHandler::Initialize() { + if (m_initialized.exchange(true)) { + LOG_WARNING(NpHandler, "Initialize called more than once"); + return; + } + + if (!EmulatorSettings.IsShadNetEnabled()) { + LOG_INFO(NpHandler, "shadNet disabled globally we are in offline mode"); + return; + } + + { + const auto [host, port] = ParseServerAddress(); + const ShadNet::ProbeInfo probe = ShadNet::ProbeServer(host, port); + if (probe.result != ShadNet::ProbeResult::Ok) { + EmulatorSettings.SetShadNetSessionDisabled(true); + switch (probe.result) { + case ShadNet::ProbeResult::VersionMismatch: + LOG_WARNING(NpHandler, + "shadNet server {}:{} protocol version mismatch (server v{}, emulator " + "v{}); disabling shadNet for this run (saved setting unchanged)", + host, port, probe.server_version, ShadNet::SHAD_PROTOCOL_VERSION); + ImGui::ShadNetNotify::Push( + ImGui::ShadNetNotify::Kind::Info, + fmt::format("shadNet protocol version mismatch (server v{}, emulator v{}). " + "Please update shadPS4. Online features are disabled for this " + "session.", + probe.server_version, ShadNet::SHAD_PROTOCOL_VERSION)); + break; + case ShadNet::ProbeResult::ProtocolError: + LOG_WARNING(NpHandler, + "shadNet server {}:{} sent an invalid ServerInfo handshake; disabling " + "shadNet for this run (saved setting unchanged)", + host, port); + ImGui::ShadNetNotify::Push( + ImGui::ShadNetNotify::Kind::Info, + fmt::format("shadNet server ({}:{}) uses an incompatible protocol. Online " + "features are disabled for this session.", + host, port)); + break; + default: // Unreachable + LOG_WARNING(NpHandler, + "shadNet server {}:{} is offline/unreachable; disabling shadNet for " + "this run (saved setting unchanged)", + host, port); + ImGui::ShadNetNotify::Push( + ImGui::ShadNetNotify::Kind::Info, + fmt::format("shadNet server ({}:{}) is offline. Online features are disabled " + "for this session.", + host, port)); + break; + } + return; + } + } + + const auto logged_in = UserManagement.GetLoggedInUsers(); // get all login users + int connected_count = 0; + for (int i = 0; i < Libraries::UserService::ORBIS_USER_SERVICE_MAX_LOGIN_USERS; ++i) { + const User* u = logged_in[i]; + if (!u) + continue; + if (ConnectUserById(u->user_id)) + ++connected_count; + } + + if (connected_count == 0) { + LOG_WARNING(NpHandler, "no users connected to shadNet"); + return; + } + + StartWorker(); +} + +void NpHandler::Shutdown() { + if (!m_initialized.exchange(false)) + return; + + m_worker_running = false; + + // Stop any pending reconnect retries. + { + std::lock_guard lock(m_mutex_clients); + m_reconnect.clear(); + } + + // Collect user IDs to disconnect (avoid holding m_mutex_clients during Stop) + std::vector ids; + { + std::lock_guard lock(m_mutex_clients); + for (auto& [uid, _] : m_clients) + ids.push_back(uid); + } + for (s32 uid : ids) + DisconnectUser(uid); + + if (m_worker_thread.joinable()) + m_worker_thread.join(); + + // P2P goes with NP: closes the UDP port and removes its UPnP forwarding. + Net::StopP2P(); + + LOG_INFO(NpHandler, "Shutdown complete"); +} + +bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, const std::string& npid, + const std::string& password, const std::string& token) { + LOG_INFO(NpHandler, "Connecting user_id={} npid='{}' to {}:{} (timeout {}s)", user_id, npid, + host, port, ShadNet::SHAD_CONNECT_TIMEOUT_MS / 1000); + + auto client = std::make_shared(); + + // Wire per-user notification callbacks + client->onFriendQuery = [this, user_id](const ShadNet::NotifyFriendQuery& n) { + OnFriendQuery(user_id, n); + }; + client->onFriendNew = [this, user_id](const ShadNet::NotifyFriendNew& n) { + OnFriendNew(user_id, n); + }; + client->onFriendLost = [this, user_id](const ShadNet::NotifyFriendLost& n) { + OnFriendLost(user_id, n); + }; + client->onFriendStatus = [this, user_id](const ShadNet::NotifyFriendStatus& n) { + OnFriendStatus(user_id, n); + }; + client->onWebApiPushEvent = [this, user_id](const ShadNet::NotifyWebApiPushEvent& n) { + OnWebApiPushEvent(user_id, n); + }; + client->onAsyncReply = [this, user_id](ShadNet::CommandType cmd, u64 pkt_id, + ShadNet::ErrorType err, const std::vector& body) { + OnAsyncReply(user_id, cmd, pkt_id, err, body); + }; + client->onLoginResult = [this, user_id](const ShadNet::LoginResult& res) { + OnLoginResult(user_id, res); + }; + + // 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()); + client->Start(host, port, npid, password, token); + + // Shared handling for an incompatible-protocol failure (version mismatch or + // corrupt/unparseable stream): tell the user and disable shadNet for this run, + // since reconnect attempts against an incompatible server are pointless. + const auto handle_protocol_mismatch = [&client](ShadNet::ShadNetState st) { + if (st != ShadNet::ShadNetState::FailureProtocol) + return; + const u32 server_ver = client->GetServerProtocolVersion(); + EmulatorSettings.SetShadNetSessionDisabled(true); + if (server_ver != 0) { + LOG_ERROR(NpHandler, + "shadNet protocol version mismatch (server v{}, emulator v{}); disabling " + "shadNet for this run (saved setting unchanged)", + server_ver, ShadNet::SHAD_PROTOCOL_VERSION); + ImGui::ShadNetNotify::Push( + ImGui::ShadNetNotify::Kind::Info, + fmt::format("shadNet protocol version mismatch (server v{}, emulator v{}). " + "Please update shadPS4. Online features are disabled for this " + "session.", + server_ver, ShadNet::SHAD_PROTOCOL_VERSION)); + } else { + LOG_ERROR(NpHandler, "shadNet protocol error during handshake; disabling shadNet for " + "this run (saved setting unchanged)"); + ImGui::ShadNetNotify::Push( + ImGui::ShadNetNotify::Kind::Info, + "shadNet server uses an incompatible protocol. Online features are disabled " + "for this session."); + } + }; + + const ShadNet::ShadNetState conn_state = client->WaitForConnection(); + if (conn_state != ShadNet::ShadNetState::Ok) { + LOG_ERROR(NpHandler, "user_id={} connection failed (state={})", user_id, + static_cast(conn_state)); + handle_protocol_mismatch(conn_state); + client->Stop(); + return false; + } + + const ShadNet::ShadNetState auth_state = client->WaitForAuthenticated(); + if (auth_state != ShadNet::ShadNetState::Ok) { + LOG_ERROR(NpHandler, "user_id={} authentication failed (state={})", user_id, + static_cast(auth_state)); + handle_protocol_mismatch(auth_state); + client->Stop(); + return false; + } + + 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 + { + OrbisNpId np_id{}; + SetNpId(np_id, npid); + std::lock_guard lock(m_mutex_clients); + m_np_ids[user_id] = np_id; + m_clients[user_id] = std::move(client); + } + + FireStateCallback(user_id, NpManager::OrbisNpState::SignedIn); + return true; +} + +void NpHandler::SetAppearOffline(bool enable) { + m_appear_offline = enable; + std::lock_guard lock(m_mutex_clients); + for (auto& [uid, client] : m_clients) { + if (client) + client->SetAppearOffline(enable); // sends the toggle to connected sessions + } +} + +void NpHandler::FailPendingRequests(s32 user_id, s32 error_code) { + size_t failed = 0; + { + std::lock_guard lock(m_mutex_pending_tus); + for (auto it = m_pending_tus.begin(); it != m_pending_tus.end();) { + if (it->second.user_id == user_id) { + if (it->second.req) { + it->second.req->SetResult(error_code); + } + it = m_pending_tus.erase(it); + ++failed; + } else { + ++it; + } + } + } + { + std::lock_guard lock(m_mutex_pending_score); + for (auto it = m_pending_score.begin(); it != m_pending_score.end();) { + if (it->second.user_id == user_id) { + if (it->second.req) { + it->second.req->SetResult(error_code); + } + it = m_pending_score.erase(it); + ++failed; + } else { + ++it; + } + } + } + { + std::lock_guard lock(m_mutex_pending_lookup); + for (auto it = m_pending_lookup.begin(); it != m_pending_lookup.end();) { + if (it->second.user_id == user_id) { + if (it->second.on_result) { + it->second.on_result(error_code, 0, {}); + } + it = m_pending_lookup.erase(it); + ++failed; + } else { + ++it; + } + } + } + if (failed > 0) { + LOG_WARNING(NpHandler, "user_id={} failed {} pending request(s) with {:#x}", user_id, + failed, static_cast(error_code)); + } +} + +void NpHandler::DisconnectUser(s32 user_id) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) + return; + client = std::move(it->second); + m_clients.erase(it); + } + { + std::lock_guard lock(m_mutex_friend_state); + m_friend_state.erase(user_id); + } + client->Stop(); + // The reader thread is joined, so no reply can race us: complete every + // in-flight request now, or PollAsync spins and WaitAsync blocks forever. + FailPendingRequests(user_id, ORBIS_NP_ERROR_SIGNED_OUT); + FireStateCallback(user_id, NpManager::OrbisNpState::SignedOut); + LOG_INFO(NpHandler, "user_id={} disconnected", user_id); +} + +void NpHandler::OnUserLoggedIn(s32 user_id) { + if (ConnectUserById(user_id)) + StartWorker(); +} + +void NpHandler::OnUserLoggedOut(s32 user_id) { + { + std::lock_guard lock(m_mutex_clients); + m_reconnect.erase(user_id); // do not auto-reconnect + } + DisconnectUser(user_id); +} + +void NpHandler::WorkerThread() { + constexpr auto INTERVAL = std::chrono::milliseconds(500); + + while (m_worker_running) { + std::this_thread::sleep_for(INTERVAL); + + // Collect ids of dropped clients + std::vector dropped; + { + std::lock_guard lock(m_mutex_clients); + for (auto& [uid, client] : m_clients) { + if (!client->IsConnected()) + dropped.push_back(uid); + } + } + + for (s32 uid : dropped) { + LOG_WARNING(NpHandler, "user_id={} connection dropped (network); will retry", uid); + DisconnectUser(uid); // reports SignedOut + stops/removes the dead client + MarkForReconnect(uid); // schedule transparent reconnect (network drop, not logout) + } + + TryReconnect(); + } +} + +void NpHandler::MarkForReconnect(s32 user_id) { + if (!EmulatorSettings.IsShadNetEnabled()) + return; // offline mode: nothing to reconnect to + constexpr auto kInitialBackoff = std::chrono::milliseconds(2000); + std::lock_guard lock(m_mutex_clients); + auto& st = m_reconnect[user_id]; + st.backoff = kInitialBackoff; + st.next_attempt = std::chrono::steady_clock::now() + st.backoff; +} + +void NpHandler::TryReconnect() { + constexpr auto kMaxBackoff = std::chrono::milliseconds(30000); + const auto now = std::chrono::steady_clock::now(); + + std::vector due; + { + std::lock_guard lock(m_mutex_clients); + for (auto& [uid, st] : m_reconnect) { + if (m_clients.count(uid) || now >= st.next_attempt) + due.push_back(uid); + } + } + + for (s32 uid : due) { + if (!m_worker_running) + return; + if (!EmulatorSettings.IsShadNetEnabled()) { + std::lock_guard lock(m_mutex_clients); + m_reconnect.erase(uid); + continue; + } + // ConnectUserById locks m_mutex_clients internally; call it unlocked. + const bool ok = ConnectUserById(uid); + std::lock_guard lock(m_mutex_clients); + if (ok || m_clients.count(uid)) { + m_reconnect.erase(uid); + LOG_INFO(NpHandler, "user_id={} reconnected to shadNet", uid); + } else if (auto it = m_reconnect.find(uid); it != m_reconnect.end()) { + it->second.backoff = std::min(it->second.backoff * 2, kMaxBackoff); + it->second.next_attempt = std::chrono::steady_clock::now() + it->second.backoff; + LOG_DEBUG(NpHandler, "user_id={} reconnect failed; next attempt in {}ms", uid, + it->second.backoff.count()); + } + } +} + +bool NpHandler::IsPsnSignedIn(s32 user_id) const { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + return it != m_clients.end() && it->second->IsAuthenticated(); +} + +bool NpHandler::IsAnySignedIn() const { + std::lock_guard lock(m_mutex_clients); + for (auto& [_, client] : m_clients) + if (client->IsAuthenticated()) + return true; + return false; +} + +OrbisNpId NpHandler::GetNpId(s32 user_id) const { + std::lock_guard lock(m_mutex_clients); + auto it = m_np_ids.find(user_id); + return it != m_np_ids.end() ? it->second : OrbisNpId{}; +} + +OrbisNpOnlineId NpHandler::GetOnlineId(s32 user_id) const { + return GetNpId(user_id).handle; +} + +std::string NpHandler::GetAvatarUrl(s32 user_id) const { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + return it != m_clients.end() ? it->second->GetAvatarUrl() : std::string{}; +} + +OrbisNpAccountId NpHandler::GetAccountId(s32 user_id) const { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + return it != m_clients.end() ? static_cast(it->second->GetUserId()) : 0; +} + +std::string NpHandler::GetBearerToken(s32 user_id) const { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + return it != m_clients.end() ? it->second->GetBearerToken() : std::string{}; +} + +u32 NpHandler::GetLocalIpAddr(s32 user_id) const { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + return it != m_clients.end() ? it->second->GetAddrLocal() : 0; +} + +s32 NpHandler::GetUserIdByAccountId(u64 account_id) const { + std::lock_guard lock(m_mutex_clients); + for (auto& [uid, client] : m_clients) { + if (static_cast(client->GetUserId()) == account_id) + return uid; + } + return -1; +} + +s32 NpHandler::GetUserIdByOnlineId(const OrbisNpOnlineId& online_id) const { + std::lock_guard lock(m_mutex_clients); + for (auto& [uid, np_id] : m_np_ids) { + if (strncmp(np_id.handle.data, online_id.data, ORBIS_NP_ONLINEID_MAX_LENGTH) == 0) + return uid; + } + return -1; +} + +std::vector NpHandler::GetConnectedUsers() const { + std::vector out; + std::lock_guard lock(m_mutex_clients); + out.reserve(m_clients.size()); + for (const auto& [uid, c] : m_clients) { + out.push_back(uid); + } + return out; +} + +// state callbacks +s32 NpHandler::RegisterStateCallback(StateCallback cb, void* userdata) { + std::lock_guard lock(m_mutex_cbs); + const s32 h = m_next_handle++; + m_state_cbs.push_back({h, std::move(cb), userdata}); + return h; +} + +void NpHandler::UnregisterStateCallback(s32 handle) { + std::lock_guard lock(m_mutex_cbs); + m_state_cbs.erase(std::remove_if(m_state_cbs.begin(), m_state_cbs.end(), + [handle](const CbEntry& e) { return e.handle == handle; }), + m_state_cbs.end()); +} + +void NpHandler::FireStateCallback(s32 user_id, NpManager::OrbisNpState state) { + std::lock_guard lock(m_mutex_cbs); + for (const auto& e : m_state_cbs) { + if (e.cb) + e.cb(user_id, state); + } +} + +// A usable NP Communication ID is the 12-char "NPWRxxxxx_00" form. An empty or +// all-NUL buffer means npbind.dat was missing or unparsed,treat it as invalid +// so callers reject the request instead of sending a blank comm id on the wire. +bool IsValidNpCommId(const std::string& id) { + return !id.empty() && std::any_of(id.begin(), id.end(), [](char c) { return c != '\0'; }); +} + +std::string NpHandler::GetNpCommId(s32 service_label) const { + // TODO complete guess of how commid is mapping to service_label. + constexpr size_t COM_ID_LEN = 12; + const auto& ids = Common::ElfInfo::Instance().GetNpCommIds(); + std::string com_id; + if (!ids.empty()) { + const size_t idx = (service_label >= 0 && static_cast(service_label) < ids.size()) + ? static_cast(service_label) + : 0; + com_id = ids[idx]; + if (static_cast(service_label) >= ids.size()) { + LOG_WARNING(NpHandler, + "service_label={} >= npbind entry count {} — falling back " + "to index 0", + service_label, ids.size()); + } + } + if (com_id.size() < COM_ID_LEN) { + com_id.resize(COM_ID_LEN, '\0'); + } else if (com_id.size() > COM_ID_LEN) { + com_id.resize(COM_ID_LEN); + } + return com_id; +} + +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 >= 100 && cmd_val <= 200) { + NpMatching2::OnMatchingReply(cmd, pkt_id, error, body); + } else if (cmd_val >= 201 && cmd_val <= 300) { + OnTusReply(user_id, cmd, pkt_id, error, body); + } else if (cmd_val >= 301 && cmd_val <= 400) { + OnTrophyReply(user_id, cmd, pkt_id, error, body); + } else if (cmd == ShadNet::CommandType::LookupOnlineId) { + OnLookupReply(user_id, cmd, pkt_id, error, body); + } else { + OnScoreReply(user_id, cmd, pkt_id, error, body); + } +} + +void NpHandler::ResolveOnlineId( + s32 user_id, const std::string& online_id, + std::function + on_result) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "No shadNet session for user_id={}", user_id); + if (on_result) { + on_result(ORBIS_NP_ERROR_SIGNED_OUT, 0, {}); + } + return; + } + client = it->second; + } + if (!client || !client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + if (on_result) { + on_result(ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN, 0, {}); + } + return; + } + if (online_id.empty()) { + if (on_result) { + on_result(ORBIS_NP_COMMUNITY_SERVER_ERROR_NO_SUCH_USER_NPID, 0, {}); + } + return; + } + + const u64 pkt_id = client->LookupOnlineId(online_id); + if (on_result) { + std::lock_guard lock(m_mutex_pending_lookup); + PendingLookupRequest pending; + pending.user_id = user_id; + pending.on_result = std::move(on_result); + m_pending_lookup.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, "user_id={} onlineId='{}' pkt_id={}", user_id, online_id, pkt_id); +} + +void NpHandler::OnLookupReply(s32 user_id, ShadNet::CommandType /*cmd*/, u64 pkt_id, + ShadNet::ErrorType error, const std::vector& body) { + std::function on_result; + { + std::lock_guard lock(m_mutex_pending_lookup); + auto it = m_pending_lookup.find(pkt_id); + if (it == m_pending_lookup.end()) { + return; // no waiter (already flushed by a disconnect, or a stray reply) + } + on_result = std::move(it->second.on_result); + m_pending_lookup.erase(it); + } + if (!on_result) { + return; + } + + if (error == ShadNet::ErrorType::NotFound) { + LOG_INFO(NpHandler, "LookupOnlineId: user_id={} pkt_id={} -> not found", user_id, pkt_id); + on_result(ORBIS_NP_COMMUNITY_SERVER_ERROR_NO_SUCH_USER_NPID, 0, {}); + return; + } + if (error == ShadNet::ErrorType::InvalidInput) { + on_result(ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT, 0, {}); + return; + } + if (error != ShadNet::ErrorType::NoError) { + LOG_WARNING(NpHandler, "LookupOnlineId failed: error={}", static_cast(error)); + on_result(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE, 0, {}); + return; + } + + // Reply body is u32 LE blob size followed by the proto. + if (body.size() < 4) { + LOG_WARNING(NpHandler, "LookupOnlineId: truncated reply"); + on_result(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE, 0, {}); + return; + } + const u32 blob_sz = static_cast(body[0]) | (static_cast(body[1]) << 8) | + (static_cast(body[2]) << 16) | (static_cast(body[3]) << 24); + if (body.size() < 4 + blob_sz) { + LOG_WARNING(NpHandler, "LookupOnlineId: reply shorter than declared blob"); + on_result(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE, 0, {}); + return; + } + + shadnet::LookupOnlineIdReply pb; + if (!pb.ParseFromArray(body.data() + 4, static_cast(blob_sz))) { + LOG_WARNING(NpHandler, "LookupOnlineId: could not parse reply"); + on_result(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE, 0, {}); + return; + } + + LOG_INFO(NpHandler, "LookupOnlineId: user_id={} pkt_id={} -> accountId={} npid='{}'", user_id, + pkt_id, pb.account_id(), pb.npid()); + on_result(ORBIS_OK, pb.account_id(), pb.npid()); +} + +} // namespace Libraries::Np diff --git a/src/core/libraries/np/np_handler.h b/src/core/libraries/np/np_handler/np_handler.h similarity index 99% rename from src/core/libraries/np/np_handler.h rename to src/core/libraries/np/np_handler/np_handler.h index 842cc1383..42e470f98 100644 --- a/src/core/libraries/np/np_handler.h +++ b/src/core/libraries/np/np_handler/np_handler.h @@ -37,6 +37,8 @@ class ContextManager; struct NpMatching2State; } // namespace NpMatching2 +bool IsValidNpCommId(const std::string& id); + class NpHandler { public: class Matching2CacheGuard { diff --git a/src/core/libraries/np/np_handler/np_handler_friends.cpp b/src/core/libraries/np/np_handler/np_handler_friends.cpp new file mode 100644 index 000000000..b6a66f871 --- /dev/null +++ b/src/core/libraries/np/np_handler/np_handler_friends.cpp @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "common/logging/log.h" +#include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_handler/np_handler.h" +#include "imgui/shadnet_notifications_layer.h" + +namespace Libraries::Np { + +// friends calls +u32 NpHandler::GetNumFriends(s32 user_id) const { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + return it != m_clients.end() ? it->second->GetNumFriends() : 0; +} + +std::optional NpHandler::GetFriendNpid(s32 user_id, u32 index) const { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + return it != m_clients.end() ? it->second->GetFriendNpid(index) : std::nullopt; +} + +void NpHandler::OnFriendQuery(s32 user_id, const ShadNet::NotifyFriendQuery& n) { + LOG_INFO(NpHandler, "user_id={} FriendQuery from '{}'", user_id, n.fromNpid); + ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendRequest, + "Friend request from " + n.fromNpid); + std::lock_guard lock(m_mutex_friend_state); + auto& st = m_friend_state[user_id]; + if (std::find(st.requests_received.begin(), st.requests_received.end(), n.fromNpid) == + st.requests_received.end()) { + st.requests_received.push_back(n.fromNpid); + } +} + +void NpHandler::OnFriendNew(s32 user_id, const ShadNet::NotifyFriendNew& n) { + LOG_INFO(NpHandler, "user_id={} FriendNew '{}' ({})", user_id, n.npid, + n.online ? "online" : "offline"); + ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendNew, + n.npid + " is now your friend"); + std::lock_guard lock(m_mutex_friend_state); + auto& st = m_friend_state[user_id]; + auto it = std::find_if(st.friends.begin(), st.friends.end(), + [&](const FriendInfo& f) { return f.npid == n.npid; }); + if (it == st.friends.end()) { + st.friends.push_back({n.npid, n.online}); + } else { + it->online = n.online; + } + const auto drop = [&](std::vector& v) { + v.erase(std::remove(v.begin(), v.end(), n.npid), v.end()); + }; + drop(st.requests_received); + drop(st.requests_sent); +} + +void NpHandler::OnFriendLost(s32 user_id, const ShadNet::NotifyFriendLost& n) { + LOG_INFO(NpHandler, "user_id={} FriendLost '{}'", user_id, n.npid); + ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendLost, + n.npid + " removed you as a friend"); + std::lock_guard lock(m_mutex_friend_state); + auto& st = m_friend_state[user_id]; + st.friends.erase(std::remove_if(st.friends.begin(), st.friends.end(), + [&](const FriendInfo& f) { return f.npid == n.npid; }), + st.friends.end()); +} + +void NpHandler::OnFriendStatus(s32 user_id, const ShadNet::NotifyFriendStatus& n) { + LOG_DEBUG(NpHandler, "user_id={} FriendStatus '{}' is {}", user_id, n.npid, + n.online ? "online" : "offline"); + if (n.online) { + ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::Online, n.npid + " is online"); + } + std::lock_guard lock(m_mutex_friend_state); + auto& st = m_friend_state[user_id]; + auto it = std::find_if(st.friends.begin(), st.friends.end(), + [&](const FriendInfo& f) { return f.npid == n.npid; }); + if (it != st.friends.end()) { + it->online = n.online; + } else { + st.friends.push_back({n.npid, n.online}); + } +} + +void NpHandler::OnLoginResult(s32 user_id, const ShadNet::LoginResult& res) { + if (res.error != ShadNet::ErrorType::NoError) { + return; + } + FriendListSnapshot snap; + snap.friends.reserve(res.friends.size()); + for (const auto& f : res.friends) { + snap.friends.push_back({f.npid, f.online}); + } + snap.requests_sent = res.requestsSent; + snap.requests_received = res.requestsReceived; + snap.blocked = res.blocked; + { + std::lock_guard lock(m_mutex_friend_state); + m_friend_state[user_id] = std::move(snap); + } + LOG_INFO(NpHandler, + "user_id={} friend state seeded: {} friends, {} received, {} sent, {} blocked", + user_id, res.friends.size(), res.requestsReceived.size(), res.requestsSent.size(), + res.blocked.size()); + + // Surface friend requests that arrived while offline (no live notification fires for these). + if (!res.requestsReceived.empty()) { + ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendRequest, + std::to_string(res.requestsReceived.size()) + + " pending friend request(s)"); + } +} + +NpHandler::FriendListSnapshot NpHandler::GetFriendList(s32 user_id) const { + std::lock_guard lock(m_mutex_friend_state); + const auto it = m_friend_state.find(user_id); + return it == m_friend_state.end() ? FriendListSnapshot{} : it->second; +} + +s32 NpHandler::SendFriendRequest(s32 user_id, const std::string& npid) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + const auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client->AddFriend(npid); + LOG_INFO(NpHandler, "user_id={} AddFriend '{}'", user_id, npid); + + std::lock_guard lock(m_mutex_friend_state); + auto& st = m_friend_state[user_id]; + const bool accepting = std::find(st.requests_received.begin(), st.requests_received.end(), + npid) != st.requests_received.end(); + if (!accepting && std::find(st.requests_sent.begin(), st.requests_sent.end(), npid) == + st.requests_sent.end()) { + st.requests_sent.push_back(npid); + } + return ORBIS_OK; +} + +s32 NpHandler::RemoveFriend(s32 user_id, const std::string& npid) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + const auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client->RemoveFriend(npid); + LOG_INFO(NpHandler, "user_id={} RemoveFriend '{}'", user_id, npid); + + std::lock_guard lock(m_mutex_friend_state); + auto& st = m_friend_state[user_id]; + st.friends.erase(std::remove_if(st.friends.begin(), st.friends.end(), + [&](const FriendInfo& f) { return f.npid == npid; }), + st.friends.end()); + const auto drop = [&](std::vector& v) { + v.erase(std::remove(v.begin(), v.end(), npid), v.end()); + }; + drop(st.requests_received); + drop(st.requests_sent); + return ORBIS_OK; +} + +s32 NpHandler::BlockUser(s32 user_id, const std::string& npid) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + const auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client->AddBlock(npid); + LOG_INFO(NpHandler, "user_id={} AddBlock '{}'", user_id, npid); + + std::lock_guard lock(m_mutex_friend_state); + auto& st = m_friend_state[user_id]; + if (std::find(st.blocked.begin(), st.blocked.end(), npid) == st.blocked.end()) { + st.blocked.push_back(npid); + } + st.friends.erase(std::remove_if(st.friends.begin(), st.friends.end(), + [&](const FriendInfo& f) { return f.npid == npid; }), + st.friends.end()); + const auto drop = [&](std::vector& v) { + v.erase(std::remove(v.begin(), v.end(), npid), v.end()); + }; + drop(st.requests_received); + drop(st.requests_sent); + return ORBIS_OK; +} + +s32 NpHandler::UnblockUser(s32 user_id, const std::string& npid) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + const auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client->RemoveBlock(npid); + LOG_INFO(NpHandler, "user_id={} RemoveBlock '{}'", user_id, npid); + + std::lock_guard lock(m_mutex_friend_state); + auto& st = m_friend_state[user_id]; + st.blocked.erase(std::remove(st.blocked.begin(), st.blocked.end(), npid), st.blocked.end()); + return ORBIS_OK; +} + +} // namespace Libraries::Np diff --git a/src/core/libraries/np/np_handler/np_handler_invitation.cpp b/src/core/libraries/np/np_handler/np_handler_invitation.cpp new file mode 100644 index 000000000..96ce69ad9 --- /dev/null +++ b/src/core/libraries/np/np_handler/np_handler_invitation.cpp @@ -0,0 +1,221 @@ +// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include "common/logging/log.h" +#include "core/emulator_settings.h" +#include "core/libraries/invitation_dialog/invitation_dialog.h" +#include "core/libraries/np/np_handler/np_handler.h" +#include "core/libraries/system/systemservice.h" +#include "imgui/invitation_prompt_layer.h" + +namespace Libraries::Np { + +// Minimal JSON string escaper for the invitation-request body (npids are safe but the user message +// can contain quotes/backslashes/control chars). +std::string JsonEscape(const std::string& in) { + std::string out; + out.reserve(in.size() + 8); + for (const char c : in) { + switch (c) { + case '"': + out += "\\\""; + break; + case '\\': + out += "\\\\"; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + if (static_cast(c) < 0x20) { + char buf[8]; + std::snprintf(buf, sizeof(buf), "\\u%04x", c); + out += buf; + } else { + out += c; + } + } + } + return out; +} + +bool NpHandler::SendSessionInvitation(s32 user_id, const std::string& session_id, + const std::vector& to, + const std::string& message) { + if (session_id.empty() || to.empty()) { + LOG_ERROR(NpHandler, "empty session_id or recipient list"); + return false; + } + const std::string base_url = EmulatorSettings.GetShadNetWebApiServer(); + if (base_url.empty()) { + LOG_ERROR(NpHandler, "WebAPI server address is empty"); + return false; + } + const std::string token = GetBearerToken(user_id); + if (token.empty()) { + LOG_ERROR(NpHandler, "no bearer token for user_id={}", user_id); + return false; + } + + // invitation-request JSON: {"to":[...],"message":"..."} + std::string json = "{\"to\":["; + for (size_t i = 0; i < to.size(); ++i) { + if (i != 0) { + json += ','; + } + json += '"'; + json += JsonEscape(to[i]); + json += '"'; + } + json += ']'; + if (!message.empty()) { + json += ",\"message\":\""; + json += JsonEscape(message); + json += '"'; + } + json += '}'; + + // multipart/mixed with a single JSON part; shadNet keys on the Content-Description. + const std::string boundary = "shadps4invite" + std::to_string(user_id); + std::string body; + body += "--" + boundary + "\r\n"; + body += "Content-Type: application/json; charset=utf-8\r\n"; + body += "Content-Description: invitation-request\r\n\r\n"; + body += json; + body += "\r\n--" + boundary + "--\r\n"; + + const std::string path = "/v1/sessions/" + session_id + "/invitations"; + const std::string content_type = "multipart/mixed; boundary=" + boundary; + + httplib::Client cli(base_url); + cli.set_connection_timeout(5); + cli.set_read_timeout(10); + const httplib::Headers headers = {{"Authorization", "Bearer " + token}}; + const auto res = cli.Post(path.c_str(), headers, body, content_type.c_str()); + if (!res) { + LOG_ERROR(NpHandler, "POST {} failed (no response)", path); + return false; + } + if (res->status != 200 && res->status != 204) { + LOG_ERROR(NpHandler, "POST {} -> HTTP {}", path, res->status); + return false; + } + LOG_INFO(NpHandler, "sent invite to {} recipient(s) for session '{}'", to.size(), session_id); + return true; +} + +void NpHandler::PostSessionInvitationEvent(s32 user_id, const std::string& session_id, + const std::string& invitation_id, + const std::string& accepter_online_id, + const std::string& inviter_online_id, + OrbisNpAccountId inviter_account_id) { + using Libraries::InvitationDialog::ORBIS_NP_SESSION_INVITATION_EVENT_FLAG_INVITATION; + using Libraries::InvitationDialog::OrbisNpSessionInvitationEventParam; + + Libraries::SystemService::OrbisSystemServiceEvent event{}; + event.event_type = Libraries::SystemService::OrbisSystemServiceEventType::SessionInvitation; + + auto* param = reinterpret_cast(event.param); + std::memset(param, 0, sizeof(*param)); + std::strncpy(param->sessionId.data, session_id.c_str(), sizeof(param->sessionId.data) - 1); + if (!invitation_id.empty()) { + std::strncpy(param->invitationId.data, invitation_id.c_str(), + sizeof(param->invitationId.data) - 1); + param->flag = ORBIS_NP_SESSION_INVITATION_EVENT_FLAG_INVITATION; + } else { + param->flag = 0; // join from session info (no invitation id in the push) + } + // data[16] is followed by its own 'term' byte, so a full 16-char online id must not be cut. + std::strncpy(param->onlineId.data, accepter_online_id.c_str(), sizeof(param->onlineId.data)); + param->userId = user_id; + std::strncpy(param->referralOnlineId.data, inviter_online_id.c_str(), + sizeof(param->referralOnlineId.data)); + param->referralAccountId = inviter_account_id; + + Libraries::SystemService::PushSystemServiceEvent(event); + LOG_INFO(NpHandler, + "Posted SESSION_INVITATION user_id={} session='{}' invitation='{}' flag={} " + "onlineId='{}' referral='{}'({})", + user_id, session_id, invitation_id, param->flag, accepter_online_id, inviter_online_id, + inviter_account_id); +} + +std::vector NpHandler::GetPendingInvitations(s32 user_id) const { + std::lock_guard lk(m_mutex_pending_invites); + const auto it = m_pending_invites.find(user_id); + if (it == m_pending_invites.end()) { + return {}; + } + const auto now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + std::vector out; + out.reserve(it->second.size()); + for (const auto& p : it->second) { + if (p.valid_until == 0 || now <= p.valid_until) { + out.push_back(p); + } + } + return out; +} + +bool NpHandler::AcceptSessionInvitation(s32 user_id, const std::string& invitation_id) { + // Pull the matching stashed invite (and drop it). + PendingInvitation inv; + bool found = false; + { + std::lock_guard lk(m_mutex_pending_invites); + const auto mit = m_pending_invites.find(user_id); + if (mit != m_pending_invites.end()) { + auto& v = mit->second; + for (auto it = v.begin(); it != v.end(); ++it) { + if (it->invitation_id == invitation_id) { + inv = *it; + v.erase(it); + found = true; + break; + } + } + } + } + // Whatever surface handled it (RECV dialog or the emulator prompt), retire the other one. + ImGui::InvitationPrompt::Dismiss(invitation_id); + if (!found) { + LOG_ERROR(NpHandler, "no pending invite '{}' for user_id={}", invitation_id, user_id); + return false; + } + // Raise the join event now that the user has explicitly accepted (via the RECV dialog or the + // emulator's system-UI equivalent). + PostSessionInvitationEvent(user_id, inv.session_id, invitation_id, inv.to_npid, inv.from_npid, + inv.from_account_id); + LOG_INFO(NpHandler, "accepted '{}' session='{}'", invitation_id, inv.session_id); + return true; +} + +void NpHandler::DeclineSessionInvitation(s32 user_id, const std::string& invitation_id) { + ImGui::InvitationPrompt::Dismiss(invitation_id); + std::lock_guard lock(m_mutex_pending_invites); + auto uit = m_pending_invites.find(user_id); + if (uit == m_pending_invites.end()) { + return; + } + auto& v = uit->second; + v.erase(std::remove_if( + v.begin(), v.end(), + [&](const PendingInvitation& p) { return p.invitation_id == invitation_id; }), + v.end()); + LOG_INFO(NpHandler, "dismissed '{}' for user_id={}", invitation_id, user_id); +} + +} // namespace Libraries::Np diff --git a/src/core/libraries/np/np_handler/np_handler_score.cpp b/src/core/libraries/np/np_handler/np_handler_score.cpp new file mode 100644 index 000000000..bcf892220 --- /dev/null +++ b/src/core/libraries/np/np_handler/np_handler_score.cpp @@ -0,0 +1,1202 @@ +// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "common/logging/log.h" +#include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_handler/np_handler.h" +#include "core/libraries/np/np_score/np_score.h" +#include "shadnet.pb.h" + +namespace Libraries::Np { + +s32 NpHandler::RecordScore(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score, + const char* comment, size_t commentLen, const u8* gameInfoData, + size_t gameInfoSize, std::shared_ptr req) { + // Look up the user's shadNet session. + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + // Build RecordScoreRequest proto. + shadnet::RecordScoreRequest proto; + proto.set_boardid(boardId); + proto.set_pcid(pcId); + proto.set_score(score); + if (comment && commentLen > 0) { + proto.set_comment(std::string(comment, commentLen)); + } + if (gameInfoData && gameInfoSize > 0) { + proto.set_data(std::string(reinterpret_cast(gameInfoData), gameInfoSize)); + } + + const std::string proto_bytes = proto.SerializeAsString(); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size()); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::RecordScore, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::RecordScore; + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "user_id={} service_label={} board={} pcId={} score={} commentLen={} " + "gameInfoSize={} pkt_id={} com_id='{}'", + user_id, service_label, boardId, pcId, score, commentLen, gameInfoSize, pkt_id, + com_id); + return ORBIS_OK; +} + +s32 NpHandler::RecordGameData(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score, + const u8* data, size_t size, + std::shared_ptr req) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + shadnet::RecordScoreGameDataRequest proto; + proto.set_boardid(boardId); + proto.set_pcid(pcId); + proto.set_score(score); + + const std::string proto_bytes = proto.SerializeAsString(); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size() + size); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + if (data != nullptr && size > 0) { + payload.insert(payload.end(), data, data + size); + } + + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::RecordScoreData, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::RecordScoreData; + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "user_id={} service_label={} board={} pcId={} score={} dataSize={} " + "pkt_id={} com_id='{}'", + user_id, service_label, boardId, pcId, score, size, pkt_id, com_id); + return ORBIS_OK; +} + +s32 NpHandler::GetGameData(s32 user_id, s32 service_label, u32 boardId, const std::string& npId, + s32 pcId, void* dataOut, u64 recvSize, u64* totalSizeOut, + std::shared_ptr req) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + shadnet::GetScoreGameDataRequest proto; + proto.set_boardid(boardId); + proto.set_npid(npId); + proto.set_pcid(pcId); + + const std::string proto_bytes = proto.SerializeAsString(); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size()); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreData, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::GetScoreData; + pending.dataOut = dataOut; + pending.recvSize = recvSize; + pending.totalSizeOut = totalSizeOut; + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "user_id={} service_label={} board={} npId='{}' pcId={} recvSize={} " + "pkt_id={} com_id='{}'", + user_id, service_label, boardId, npId, pcId, recvSize, pkt_id, com_id); + return ORBIS_OK; +} + +s32 NpHandler::GetGameDataByAccountId(s32 user_id, s32 service_label, u32 boardId, u64 accountId, + s32 pcId, void* dataOut, u64 recvSize, u64* totalSizeOut, + std::shared_ptr req) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + shadnet::GetScoreGameDataByAccountIdRequest proto; + proto.set_boardid(boardId); + proto.set_accountid(accountId); + proto.set_pcid(pcId); + + const std::string proto_bytes = proto.SerializeAsString(); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size()); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + + const u64 pkt_id = + client->SubmitRequest(ShadNet::CommandType::GetScoreGameDataByAccId, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::GetScoreGameDataByAccId; + pending.dataOut = dataOut; + pending.recvSize = recvSize; + pending.totalSizeOut = totalSizeOut; + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "user_id={} service_label={} board={} accountId={} pcId={} " + "recvSize={} pkt_id={} com_id='{}'", + user_id, service_label, boardId, accountId, pcId, recvSize, pkt_id, com_id); + return ORBIS_OK; +} + +s32 NpHandler::GetBoardInfo(s32 user_id, s32 service_label, u32 boardId, + NpScore::OrbisNpScoreBoardInfo* boardInfo, + std::shared_ptr req) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + std::vector payload; + payload.reserve(12 + 4); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + payload.push_back(static_cast(boardId)); + payload.push_back(static_cast(boardId >> 8)); + payload.push_back(static_cast(boardId >> 16)); + payload.push_back(static_cast(boardId >> 24)); + + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetBoardInfos, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::GetBoardInfos; + pending.boardInfo = boardInfo; + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, "user_id={} service_label={} board={} pkt_id={} com_id='{}'", user_id, + service_label, boardId, pkt_id, com_id); + return ORBIS_OK; +} + +s32 NpHandler::GetRankingByNpId(s32 user_id, s32 service_label, u32 boardId, + const std::vector& npIds, + const std::vector& pcIds, + NpScore::OrbisNpScorePlayerRankData* rankArray, + NpScore::OrbisNpScoreComment* commentArray, + NpScore::OrbisNpScoreGameInfo* infoArray, + Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, + std::shared_ptr req) { + // pcIds must either be empty (use 0 for everything) or match npIds in size. + if (!pcIds.empty() && pcIds.size() != npIds.size()) { + LOG_ERROR(NpHandler, "pcIds size {} != npIds size {}", pcIds.size(), npIds.size()); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + // Look up the user's session. + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + shadnet::GetScoreNpIdRequest proto; + proto.set_boardid(boardId); + proto.set_withcomment(commentArray != nullptr); + proto.set_withgameinfo(infoArray != nullptr); + for (size_t i = 0; i < npIds.size(); ++i) { + auto* entry = proto.add_npids(); + entry->set_npid(npIds[i]); + entry->set_pcid(pcIds.empty() ? 0 : pcIds[i]); + } + + const std::string proto_bytes = proto.SerializeAsString(); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size()); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreNpid, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::GetScoreNpid; + pending.requestedNpIds = npIds; + pending.rankArray = rankArray; + pending.commentArray = commentArray; + pending.infoArray = infoArray; + pending.lastSortDate = lastSortDate; + pending.totalRecord = totalRecord; + pending.arrayNum = npIds.size(); + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "user_id={} service_label={} board={} npIdCount={} " + "withPcId={} withComment={} withGameInfo={} pkt_id={} com_id='{}'", + user_id, service_label, boardId, npIds.size(), !pcIds.empty(), commentArray != nullptr, + infoArray != nullptr, pkt_id, com_id); + return ORBIS_OK; +} + +s32 NpHandler::GetRankingByRange(s32 user_id, s32 service_label, u32 boardId, u32 startSerialRank, + u32 arrayNum, NpScore::OrbisNpScoreRankData* rankArray, + NpScore::OrbisNpScoreComment* commentArray, + NpScore::OrbisNpScoreGameInfo* infoArray, + Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, + std::shared_ptr req) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + shadnet::GetScoreRangeRequest proto; + proto.set_boardid(boardId); + proto.set_startrank(startSerialRank); + proto.set_numranks(arrayNum); + proto.set_withcomment(commentArray != nullptr); + proto.set_withgameinfo(infoArray != nullptr); + + const std::string proto_bytes = proto.SerializeAsString(); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size()); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreRange, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::GetScoreRange; + pending.plainRankArray = rankArray; + pending.commentArray = commentArray; + pending.infoArray = infoArray; + pending.lastSortDate = lastSortDate; + pending.totalRecord = totalRecord; + pending.arrayNum = arrayNum; + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "user_id={} service_label={} board={} startRank={} numRanks={} " + "withComment={} withGameInfo={} pkt_id={} com_id='{}'", + user_id, service_label, boardId, startSerialRank, arrayNum, commentArray != nullptr, + infoArray != nullptr, pkt_id, com_id); + return ORBIS_OK; +} + +s32 NpHandler::GetRankingByRangeA(s32 user_id, s32 service_label, u32 boardId, u32 startSerialRank, + u32 arrayNum, NpScore::OrbisNpScoreRankDataA* rankArray, + NpScore::OrbisNpScoreComment* commentArray, + NpScore::OrbisNpScoreGameInfo* infoArray, + Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, + std::shared_ptr req) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + shadnet::GetScoreRangeRequest proto; + proto.set_boardid(boardId); + proto.set_startrank(startSerialRank); + proto.set_numranks(arrayNum); + proto.set_withcomment(commentArray != nullptr); + proto.set_withgameinfo(infoArray != nullptr); + + const std::string proto_bytes = proto.SerializeAsString(); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size()); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreRange, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::GetScoreRange; + pending.aRankArray = rankArray; + pending.commentArray = commentArray; + pending.infoArray = infoArray; + pending.lastSortDate = lastSortDate; + pending.totalRecord = totalRecord; + pending.arrayNum = arrayNum; + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "user_id={} service_label={} board={} startRank={} numRanks={} " + "withComment={} withGameInfo={} pkt_id={} com_id='{}'", + user_id, service_label, boardId, startSerialRank, arrayNum, commentArray != nullptr, + infoArray != nullptr, pkt_id, com_id); + return ORBIS_OK; +} + +s32 NpHandler::GetRankingByAccountId(s32 user_id, s32 service_label, u32 boardId, + const std::vector& accountIds, + const std::vector& pcIds, + NpScore::OrbisNpScorePlayerRankDataA* rankArray, + NpScore::OrbisNpScoreComment* commentArray, + NpScore::OrbisNpScoreGameInfo* infoArray, + Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, + std::shared_ptr req) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + shadnet::GetScoreAccountIdRequest proto; + proto.set_boardid(boardId); + for (size_t i = 0; i < accountIds.size(); ++i) { + auto* entry = proto.add_ids(); + entry->set_accountid(static_cast(accountIds[i])); + // If the caller provided per-entry pcIds, use them; otherwise all zero. + entry->set_pcid(i < pcIds.size() ? pcIds[i] : 0); + } + proto.set_withcomment(commentArray != nullptr); + proto.set_withgameinfo(infoArray != nullptr); + + const std::string proto_bytes = proto.SerializeAsString(); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size()); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreAccountId, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::GetScoreAccountId; + pending.aPlayerRankArray = rankArray; + pending.commentArray = commentArray; + pending.infoArray = infoArray; + pending.lastSortDate = lastSortDate; + pending.totalRecord = totalRecord; + pending.arrayNum = accountIds.size(); + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "user_id={} service_label={} board={} accountIdCount={} " + "withComment={} withGameInfo={} pkt_id={} com_id='{}'", + user_id, service_label, boardId, accountIds.size(), commentArray != nullptr, + infoArray != nullptr, pkt_id, com_id); + return ORBIS_OK; +} + +s32 NpHandler::GetFriendsRanking(s32 user_id, s32 service_label, u32 boardId, bool includeSelf, + u32 arrayNum, NpScore::OrbisNpScoreRankData* rankArray, + NpScore::OrbisNpScoreComment* commentArray, + NpScore::OrbisNpScoreGameInfo* infoArray, + Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, + std::shared_ptr req) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + shadnet::GetScoreFriendsRequest proto; + proto.set_boardid(boardId); + proto.set_includeself(includeSelf); + proto.set_max(arrayNum); + proto.set_withcomment(commentArray != nullptr); + proto.set_withgameinfo(infoArray != nullptr); + + const std::string proto_bytes = proto.SerializeAsString(); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size()); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreFriends, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::GetScoreFriends; + pending.plainRankArray = rankArray; + pending.commentArray = commentArray; + pending.infoArray = infoArray; + pending.lastSortDate = lastSortDate; + pending.totalRecord = totalRecord; + pending.arrayNum = arrayNum; + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "user_id={} service_label={} board={} includeSelf={} " + "arrayNum={} withComment={} withGameInfo={} pkt_id={} com_id='{}'", + user_id, service_label, boardId, includeSelf, arrayNum, commentArray != nullptr, + infoArray != nullptr, pkt_id, com_id); + return ORBIS_OK; +} + +s32 NpHandler::GetFriendsRankingA(s32 user_id, s32 service_label, u32 boardId, bool includeSelf, + u32 arrayNum, NpScore::OrbisNpScoreRankDataA* rankArray, + NpScore::OrbisNpScoreComment* commentArray, + NpScore::OrbisNpScoreGameInfo* infoArray, + Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord, + std::shared_ptr req) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_WARNING(NpHandler, "user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "user_id={} not authenticated", user_id); + return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN; + } + + shadnet::GetScoreFriendsRequest proto; + proto.set_boardid(boardId); + proto.set_includeself(includeSelf); + proto.set_max(arrayNum); + proto.set_withcomment(commentArray != nullptr); + proto.set_withgameinfo(infoArray != nullptr); + + const std::string proto_bytes = proto.SerializeAsString(); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); " + "rejecting request"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size()); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreFriends, payload); + { + std::lock_guard lock(m_mutex_pending_score); + PendingScoreRequest pending; + pending.req = std::move(req); + pending.cmd = ShadNet::CommandType::GetScoreFriends; + pending.aRankArray = rankArray; + pending.commentArray = commentArray; + pending.infoArray = infoArray; + pending.lastSortDate = lastSortDate; + pending.totalRecord = totalRecord; + pending.arrayNum = arrayNum; + pending.user_id = user_id; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "user_id={} service_label={} board={} includeSelf={} " + "arrayNum={} withComment={} withGameInfo={} pkt_id={} com_id='{}'", + user_id, service_label, boardId, includeSelf, arrayNum, commentArray != nullptr, + infoArray != nullptr, pkt_id, com_id); + return ORBIS_OK; +} + +static u32 FillPlainRankArrayFromProto(const shadnet::GetScoreResponse& resp, u64 maxSlots, + NpScore::OrbisNpScoreRankData* rankArray, + NpScore::OrbisNpScoreComment* commentArray, + NpScore::OrbisNpScoreGameInfo* infoArray) { + const int n_resp = resp.rankarray_size(); + u32 out_i = 0; + for (int i = 0; i < n_resp && out_i < maxSlots; ++i) { + const auto& r = resp.rankarray(i); + if (r.npid().empty()) { + continue; + } + auto& out = rankArray[out_i]; + // Copy OnlineId (the npId string) into the 16-byte data field. + const std::string& npid = r.npid(); + const size_t cp = std::min(npid.size(), sizeof(out.npId.handle.data)); + std::memcpy(out.npId.handle.data, npid.data(), cp); + out.npId.handle.term = 0; + LOG_INFO(NpHandler, + "out[{}] (resp[{}]) npid='{}' (len={}) rank={} " + "score={}", + out_i, i, npid, npid.size(), r.rank(), r.score()); + out.pcId = r.pcid(); + out.serialRank = r.rank(); + out.rank = r.rank(); + out.highestRank = r.rank(); + out.hasGameData = r.hasgamedata() ? 1 : 0; + out.scoreValue = r.score(); + out.recordDate.tick = r.recorddate(); + + if (commentArray != nullptr && i < resp.commentarray_size()) { + const std::string& cmt = resp.commentarray(i); + const size_t ccp = + std::min(cmt.size(), sizeof(commentArray[out_i].utf8Comment) - 1); + std::memcpy(commentArray[out_i].utf8Comment, cmt.data(), ccp); + } + if (infoArray != nullptr && i < resp.infoarray_size()) { + const std::string& gi = resp.infoarray(i).data(); + const size_t gcp = std::min(gi.size(), sizeof(infoArray[out_i].data)); + infoArray[out_i].infoSize = static_cast(gcp); + std::memcpy(infoArray[out_i].data, gi.data(), gcp); + } + ++out_i; + } + return out_i; +} + +static u32 FillPlainRankArrayAFromProto(const shadnet::GetScoreResponse& resp, u64 maxSlots, + NpScore::OrbisNpScoreRankDataA* rankArray, + NpScore::OrbisNpScoreComment* commentArray, + NpScore::OrbisNpScoreGameInfo* infoArray) { + const int n_resp = resp.rankarray_size(); + u32 out_i = 0; + for (int i = 0; i < n_resp && out_i < maxSlots; ++i) { + const auto& r = resp.rankarray(i); + if (r.npid().empty()) { + continue; + } + auto& out = rankArray[out_i]; + // RankDataA stores OnlineId directly (not wrapped in NpId). + const std::string& npid = r.npid(); + const size_t cp = std::min(npid.size(), sizeof(out.onlineId.data)); + std::memcpy(out.onlineId.data, npid.data(), cp); + out.onlineId.term = 0; + LOG_INFO(NpHandler, + "out[{}] (resp[{}]) npid='{}' (len={}) rank={} " + "score={} accountId={}", + out_i, i, npid, npid.size(), r.rank(), r.score(), r.accountid()); + out.pcId = r.pcid(); + out.serialRank = r.rank(); + out.rank = r.rank(); + out.highestRank = r.rank(); + out.hasGameData = r.hasgamedata() ? 1 : 0; + out.scoreValue = r.score(); + out.recordDate.tick = r.recorddate(); + out.accountId = static_cast(r.accountid()); + + if (commentArray != nullptr && i < resp.commentarray_size()) { + const std::string& cmt = resp.commentarray(i); + const size_t ccp = + std::min(cmt.size(), sizeof(commentArray[out_i].utf8Comment) - 1); + std::memcpy(commentArray[out_i].utf8Comment, cmt.data(), ccp); + } + if (infoArray != nullptr && i < resp.infoarray_size()) { + const std::string& gi = resp.infoarray(i).data(); + const size_t gcp = std::min(gi.size(), sizeof(infoArray[out_i].data)); + infoArray[out_i].infoSize = static_cast(gcp); + std::memcpy(infoArray[out_i].data, gi.data(), gcp); + } + ++out_i; + } + return out_i; +} + +static u32 FillPlayerRankArrayAFromProto(const shadnet::GetScoreResponse& resp, u64 requestedCount, + NpScore::OrbisNpScorePlayerRankDataA* rankArray, + NpScore::OrbisNpScoreComment* commentArray, + NpScore::OrbisNpScoreGameInfo* infoArray) { + const int n_resp = resp.rankarray_size(); + const int n_req = static_cast(requestedCount); + if (n_resp != n_req) { + LOG_WARNING(NpHandler, + "response count {} != request count {} — " + "will fill up to min() and leave extras at hasData=0", + n_resp, n_req); + } + const int n = std::min(n_resp, n_req); + u32 found = 0; + for (int i = 0; i < n; ++i) { + const auto& r = resp.rankarray(i); + if (r.npid().empty()) { + continue; // no score on this board for this input accountId + } + auto& out = rankArray[i]; + out.hasData = 1; + const std::string& npid = r.npid(); + const size_t cp = std::min(npid.size(), sizeof(out.rankData.onlineId.data)); + std::memcpy(out.rankData.onlineId.data, npid.data(), cp); + out.rankData.onlineId.term = 0; + out.rankData.pcId = r.pcid(); + out.rankData.serialRank = r.rank(); + out.rankData.rank = r.rank(); + out.rankData.highestRank = r.rank(); + out.rankData.hasGameData = r.hasgamedata() ? 1 : 0; + out.rankData.scoreValue = r.score(); + out.rankData.recordDate.tick = r.recorddate(); + out.rankData.accountId = static_cast(r.accountid()); + + if (commentArray != nullptr && i < resp.commentarray_size()) { + const std::string& cmt = resp.commentarray(i); + const size_t ccp = + std::min(cmt.size(), sizeof(commentArray[i].utf8Comment) - 1); + std::memcpy(commentArray[i].utf8Comment, cmt.data(), ccp); + } + if (infoArray != nullptr && i < resp.infoarray_size()) { + const std::string& gi = resp.infoarray(i).data(); + const size_t gcp = std::min(gi.size(), sizeof(infoArray[i].data)); + infoArray[i].infoSize = static_cast(gcp); + std::memcpy(infoArray[i].data, gi.data(), gcp); + } + ++found; + } + return found; +} + +static u32 FillRankArrayFromProto(const shadnet::GetScoreResponse& resp, + const std::vector& requestedNpIds, + NpScore::OrbisNpScorePlayerRankData* rankArray, + NpScore::OrbisNpScoreComment* commentArray, + NpScore::OrbisNpScoreGameInfo* infoArray) { + const int n_resp = resp.rankarray_size(); + const int n_req = static_cast(requestedNpIds.size()); + if (n_resp != n_req) { + LOG_WARNING(NpHandler, + "response count {} != request count {} — " + "will fill up to min() and leave extras at hasData=0", + n_resp, n_req); + } + const int n = std::min(n_resp, n_req); + u32 found = 0; + for (int i = 0; i < n; ++i) { + const auto& r = resp.rankarray(i); + if (r.npid().empty()) { + continue; // server signalled "no data for this slot" + } + + auto& out = rankArray[i]; + out.hasData = 1; + const std::string& npid = r.npid(); + const size_t cp = std::min(npid.size(), sizeof(out.rankData.npId.handle.data)); + std::memcpy(out.rankData.npId.handle.data, npid.data(), cp); + out.rankData.npId.handle.term = 0; + out.rankData.pcId = r.pcid(); + out.rankData.serialRank = r.rank(); + out.rankData.rank = r.rank(); + out.rankData.highestRank = r.rank(); + out.rankData.hasGameData = r.hasgamedata() ? 1 : 0; + out.rankData.scoreValue = r.score(); + out.rankData.recordDate.tick = r.recorddate(); + + if (commentArray != nullptr && i < resp.commentarray_size()) { + const std::string& cmt = resp.commentarray(i); + const size_t ccp = + std::min(cmt.size(), sizeof(commentArray[i].utf8Comment) - 1); + std::memcpy(commentArray[i].utf8Comment, cmt.data(), ccp); + } + if (infoArray != nullptr && i < resp.infoarray_size()) { + const std::string& gi = resp.infoarray(i).data(); + const size_t gcp = std::min(gi.size(), sizeof(infoArray[i].data)); + infoArray[i].infoSize = static_cast(gcp); + std::memcpy(infoArray[i].data, gi.data(), gcp); + } + ++found; + } + return found; +} + +void NpHandler::OnScoreReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, + ShadNet::ErrorType error, const std::vector& body) { + PendingScoreRequest pending; + { + std::lock_guard lock(m_mutex_pending_score); + auto it = m_pending_score.find(pkt_id); + if (it == m_pending_score.end()) { + LOG_WARNING(NpHandler, "no pending request for pkt_id={} cmd={}", pkt_id, + static_cast(cmd)); + return; + } + pending = std::move(it->second); + m_pending_score.erase(it); + } + auto& req = pending.req; + + // Server-side errors take precedence over success-path parsing. + if (error != ShadNet::ErrorType::NoError) { + s32 orbis_err = ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE; + switch (error) { + case ShadNet::ErrorType::ScoreNotBest: + orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_NOT_BEST_SCORE; + break; + case ShadNet::ErrorType::ScoreInvalid: + orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_INVALID_SCORE; + break; + case ShadNet::ErrorType::NotFound: + orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_RANKING_BOARD_MASTER_NOT_FOUND; + break; + case ShadNet::ErrorType::Unauthorized: + orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_FORBIDDEN; + break; + case ShadNet::ErrorType::DbFail: + orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_INTERNAL_SERVER_ERROR; + break; + case ShadNet::ErrorType::Malformed: + case ShadNet::ErrorType::Invalid: + case ShadNet::ErrorType::InvalidInput: + orbis_err = ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE; + break; + default: + break; + } + LOG_WARNING(NpHandler, "user_id={} pkt_id={} cmd={} server_error={} → orbis_err={:#x}", + user_id, pkt_id, static_cast(cmd), static_cast(error), orbis_err); + req->SetResult(orbis_err); + return; + } + + // Per-command success-path parsing. + switch (cmd) { + case ShadNet::CommandType::RecordScore: { + // Reply body = u32 LE rank. + if (req->tmpRankOut != nullptr && body.size() >= 4) { + const u32 rank = static_cast(body[0]) | (static_cast(body[1]) << 8) | + (static_cast(body[2]) << 16) | (static_cast(body[3]) << 24); + *req->tmpRankOut = rank; + LOG_INFO(NpHandler, "RecordScore user_id={} pkt_id={} rank={}", user_id, pkt_id, rank); + } + req->SetResult(ORBIS_OK); + break; + } + + case ShadNet::CommandType::RecordScoreData: { + LOG_INFO(NpHandler, "RecordScoreData user_id={} pkt_id={} ok", user_id, pkt_id); + req->SetResult(ORBIS_OK); + break; + } + + case ShadNet::CommandType::GetScoreData: + case ShadNet::CommandType::GetScoreGameDataByAccId: { + const char* cmd_name = (cmd == ShadNet::CommandType::GetScoreData) + ? "GetScoreData" + : "GetScoreGameDataByAccId"; + if (body.size() < 4) { + LOG_ERROR(NpHandler, "{} body too small ({})", cmd_name, body.size()); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + const u32 stored_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | + (static_cast(body[2]) << 16) | + (static_cast(body[3]) << 24); + if (static_cast(4) + stored_size > body.size()) { + LOG_ERROR(NpHandler, "{} blob size {} exceeds body size {}", cmd_name, stored_size, + body.size()); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + if (pending.totalSizeOut != nullptr) { + *pending.totalSizeOut = stored_size; + } + if (pending.dataOut != nullptr && pending.recvSize > 0) { + const u64 copy_n = std::min(static_cast(stored_size), pending.recvSize); + std::memcpy(pending.dataOut, body.data() + 4, static_cast(copy_n)); + if (copy_n < stored_size) { + LOG_WARNING(NpHandler, "{} truncated user_id={} pkt_id={} stored={} recv={}", + cmd_name, user_id, pkt_id, stored_size, pending.recvSize); + } + } + LOG_INFO(NpHandler, "{} user_id={} pkt_id={} storedSize={} recvSize={}", cmd_name, user_id, + pkt_id, stored_size, pending.recvSize); + req->SetResult(ORBIS_OK); + break; + } + + case ShadNet::CommandType::GetBoardInfos: { + if (body.size() < 4) { + LOG_ERROR(NpHandler, "GetBoardInfos body too small ({})", body.size()); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + const u32 proto_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | + (static_cast(body[2]) << 16) | + (static_cast(body[3]) << 24); + if (static_cast(4) + proto_size > body.size()) { + LOG_ERROR(NpHandler, "GetBoardInfos proto size {} exceeds body size {}", proto_size, + body.size()); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + shadnet::BoardInfo bi; + if (!bi.ParseFromArray(body.data() + 4, static_cast(proto_size))) { + LOG_ERROR(NpHandler, "GetBoardInfos proto parse failed"); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + if (pending.boardInfo != nullptr) { + pending.boardInfo->rankLimit = bi.ranklimit(); + pending.boardInfo->updateMode = bi.updatemode(); + pending.boardInfo->sortMode = bi.sortmode(); + pending.boardInfo->uploadNumLimit = bi.uploadnumlimit(); + pending.boardInfo->uploadSizeLimit = bi.uploadsizelimit(); + } + LOG_INFO(NpHandler, + "GetBoardInfos user_id={} pkt_id={} rankLimit={} updateMode={} " + "sortMode={} uploadNumLimit={} uploadSizeLimit={}", + user_id, pkt_id, bi.ranklimit(), bi.updatemode(), bi.sortmode(), + bi.uploadnumlimit(), bi.uploadsizelimit()); + req->SetResult(ORBIS_OK); + break; + } + + case ShadNet::CommandType::GetScoreNpid: { + // Reply body = u32 LE proto size + GetScoreResponse proto bytes. + if (body.size() < 4) { + LOG_ERROR(NpHandler, "GetScoreNpid body too small ({})", body.size()); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + const u32 proto_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | + (static_cast(body[2]) << 16) | + (static_cast(body[3]) << 24); + if (static_cast(4) + proto_size > body.size()) { + LOG_ERROR(NpHandler, "GetScoreNpid proto size {} exceeds body size {}", proto_size, + body.size()); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + shadnet::GetScoreResponse resp; + if (!resp.ParseFromArray(body.data() + 4, static_cast(proto_size))) { + LOG_ERROR(NpHandler, "GetScoreNpid proto parse failed"); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + const u32 found = FillRankArrayFromProto(resp, pending.requestedNpIds, pending.rankArray, + pending.commentArray, pending.infoArray); + if (pending.lastSortDate != nullptr) { + pending.lastSortDate->tick = resp.lastsortdate(); + } + if (pending.totalRecord != nullptr) { + *pending.totalRecord = resp.totalrecord(); + } + LOG_INFO(NpHandler, "GetScoreNpid user_id={} pkt_id={} found={}/{} totalRecord={}", user_id, + pkt_id, found, pending.arrayNum, resp.totalrecord()); + req->SetResult(static_cast(found)); + break; + } + case ShadNet::CommandType::GetScoreAccountId: { + if (body.size() < 4) { + LOG_ERROR(NpHandler, "GetScoreAccountId body too small ({})", body.size()); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + const u32 proto_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | + (static_cast(body[2]) << 16) | + (static_cast(body[3]) << 24); + if (static_cast(4) + proto_size > body.size()) { + LOG_ERROR(NpHandler, "GetScoreAccountId proto size {} exceeds body size {}", proto_size, + body.size()); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + shadnet::GetScoreResponse resp; + if (!resp.ParseFromArray(body.data() + 4, static_cast(proto_size))) { + LOG_ERROR(NpHandler, "GetScoreAccountId proto parse failed"); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + const u32 found = + FillPlayerRankArrayAFromProto(resp, pending.arrayNum, pending.aPlayerRankArray, + pending.commentArray, pending.infoArray); + if (pending.lastSortDate != nullptr) { + pending.lastSortDate->tick = resp.lastsortdate(); + } + if (pending.totalRecord != nullptr) { + *pending.totalRecord = resp.totalrecord(); + } + LOG_INFO(NpHandler, "GetScoreAccountId user_id={} pkt_id={} found={}/{} totalRecord={}", + user_id, pkt_id, found, pending.arrayNum, resp.totalrecord()); + req->SetResult(static_cast(found)); + break; + } + + case ShadNet::CommandType::GetScoreRange: + case ShadNet::CommandType::GetScoreFriends: { + const char* cmd_name = + (cmd == ShadNet::CommandType::GetScoreRange) ? "GetScoreRange" : "GetScoreFriends"; + if (body.size() < 4) { + LOG_ERROR(NpHandler, "{} body too small ({})", cmd_name, body.size()); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + const u32 proto_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | + (static_cast(body[2]) << 16) | + (static_cast(body[3]) << 24); + if (static_cast(4) + proto_size > body.size()) { + LOG_ERROR(NpHandler, "{} proto size {} exceeds body size {}", cmd_name, proto_size, + body.size()); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + shadnet::GetScoreResponse resp; + if (!resp.ParseFromArray(body.data() + 4, static_cast(proto_size))) { + LOG_ERROR(NpHandler, "{} proto parse failed", cmd_name); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } + u32 found = 0; + if (pending.aRankArray != nullptr) { + found = FillPlainRankArrayAFromProto(resp, pending.arrayNum, pending.aRankArray, + pending.commentArray, pending.infoArray); + } else { + found = FillPlainRankArrayFromProto(resp, pending.arrayNum, pending.plainRankArray, + pending.commentArray, pending.infoArray); + } + if (pending.lastSortDate != nullptr) { + pending.lastSortDate->tick = resp.lastsortdate(); + } + if (pending.totalRecord != nullptr) { + *pending.totalRecord = resp.totalrecord(); + } + LOG_INFO(NpHandler, "{} user_id={} pkt_id={} found={}/{} totalRecord={}{}", cmd_name, + user_id, pkt_id, found, pending.arrayNum, resp.totalrecord(), + pending.aRankArray != nullptr ? " (A-variant)" : ""); + req->SetResult(static_cast(found)); + break; + } + + default: + LOG_WARNING(NpHandler, "unexpected cmd={} pkt_id={}", static_cast(cmd), pkt_id); + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + break; + } +} + +} // namespace Libraries::Np diff --git a/src/core/libraries/np/np_handler/np_handler_trophy.cpp b/src/core/libraries/np/np_handler/np_handler_trophy.cpp new file mode 100644 index 000000000..cf65c34c1 --- /dev/null +++ b/src/core/libraries/np/np_handler/np_handler_trophy.cpp @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/logging/log.h" +#include "core/libraries/np/np_handler/np_handler.h" +#include "shadnet.pb.h" + +namespace Libraries::Np { + +void NpHandler::ReportTrophyUnlock(s32 user_id, s32 service_label, s32 trophy_id, u64 timestamp) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_DEBUG(NpHandler, "no shadNet session for user_id={}; skipping", user_id); + return; + } + client = it->second; + } + if (!client) { + LOG_DEBUG(NpHandler, "null client for user_id={}; skipping", user_id); + return; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "not authenticated; trophy {} not mirrored", trophy_id); + return; + } + if (!client->IsTrophiesEnabled()) { + LOG_WARNING(NpHandler, + "server has not advertised trophy support; trophy {} " + "not mirrored", + trophy_id); + return; + } + + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_WARNING(NpHandler, + "no valid NP Communication ID for service_label={}; " + "skipping trophy {}", + service_label, trophy_id); + return; + } + + const u64 pkt_id = client->UnlockTrophy(com_id, trophy_id, timestamp); + LOG_INFO(NpHandler, "user_id={} trophy={} com_id='{}' pkt_id={}", user_id, trophy_id, com_id, + pkt_id); +} + +void NpHandler::SyncTrophies( + s32 user_id, s32 service_label, const std::vector>& local_trophies, + std::function>&)> on_merged) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + LOG_DEBUG(NpHandler, "no shadNet session for user_id={}; skipping", user_id); + return; + } + client = it->second; + } + if (!client) { + LOG_DEBUG(NpHandler, "null client for user_id={}; skipping", user_id); + return; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "not authenticated; skipping sync"); + return; + } + if (!client->IsTrophiesEnabled()) { + LOG_WARNING(NpHandler, "server has not advertised trophy support; " + "skipping sync"); + return; + } + + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + LOG_WARNING(NpHandler, "no valid NP Communication ID for service_label={}; skipping", + service_label); + return; + } + + const u64 pkt_id = client->SyncTrophies(com_id, local_trophies); + if (on_merged) { + std::lock_guard lock(m_mutex_pending_trophy); + m_pending_trophy.emplace(pkt_id, std::move(on_merged)); + } + LOG_INFO(NpHandler, "user_id={} uploading {} com_id='{}' pkt_id={}", user_id, + local_trophies.size(), com_id, pkt_id); +} + +void NpHandler::OnTrophyReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, + ShadNet::ErrorType error, const std::vector& body) { + if (cmd == ShadNet::CommandType::UnlockTrophy) { + if (error != ShadNet::ErrorType::NoError) { + LOG_WARNING(NpHandler, "UnlockTrophy rejected by server: error={}", + static_cast(error)); + } + return; + } + if (cmd != ShadNet::CommandType::SyncTrophies) + return; + + std::function>&)> on_merged; + { + std::lock_guard lock(m_mutex_pending_trophy); + auto it = m_pending_trophy.find(pkt_id); + if (it == m_pending_trophy.end()) + return; + on_merged = std::move(it->second); + m_pending_trophy.erase(it); + } + + if (error != ShadNet::ErrorType::NoError) { + LOG_WARNING(NpHandler, "SyncTrophies failed: error={}", static_cast(error)); + return; // leave local trophies exactly as they were + } + + // Reply body is u32 LE blob size followed by the proto. + if (body.size() < 4) { + LOG_WARNING(NpHandler, "SyncTrophies: truncated reply"); + return; + } + const u32 blob_sz = static_cast(body[0]) | (static_cast(body[1]) << 8) | + (static_cast(body[2]) << 16) | (static_cast(body[3]) << 24); + if (body.size() < 4 + blob_sz) { + LOG_WARNING(NpHandler, "SyncTrophies: reply shorter than declared blob"); + return; + } + + shadnet::SyncTrophiesReply pb; + if (!pb.ParseFromArray(body.data() + 4, static_cast(blob_sz))) { + LOG_WARNING(NpHandler, "could not parse reply"); + return; + } + + std::vector> merged; + merged.reserve(pb.trophies_size()); + for (const auto& t : pb.trophies()) + merged.emplace_back(t.trophyid(), t.timestamp()); + + LOG_INFO(NpHandler, "server holds {} trophies for user_id={}", merged.size(), user_id); + on_merged(merged); +} + +} // namespace Libraries::Np diff --git a/src/core/libraries/np/np_handler/np_handler_tus.cpp b/src/core/libraries/np/np_handler/np_handler_tus.cpp new file mode 100644 index 000000000..b7af73b69 --- /dev/null +++ b/src/core/libraries/np/np_handler/np_handler_tus.cpp @@ -0,0 +1,1022 @@ +// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "common/logging/log.h" +#include "common/string_util.h" +#include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_handler/np_handler.h" +#include "shadnet.pb.h" + +namespace Libraries::Np { + +std::vector BuildTusPayload(const std::string& com_id, const std::string& proto_bytes) { + std::vector payload; + payload.reserve(12 + 4 + proto_bytes.size()); + payload.insert(payload.end(), com_id.begin(), com_id.end()); + const u32 sz = static_cast(proto_bytes.size()); + payload.push_back(static_cast(sz)); + payload.push_back(static_cast(sz >> 8)); + payload.push_back(static_cast(sz >> 16)); + payload.push_back(static_cast(sz >> 24)); + payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end()); + return payload; +} + +void CopyNpHandle(OrbisNpId& dst, const std::string& src) { + dst = OrbisNpId{}; + const size_t cp = std::min(src.size(), sizeof(dst.handle.data) - 1); + std::memcpy(dst.handle.data, src.data(), cp); +} + +s32 NpHandler::TusGetMultiSlotVariable( + s32 user_id, s32 service_label, const std::string& ownerNpId, const std::string& virtualUser, + const std::vector& slotIds, NpTus::OrbisNpTusVariable* variablesOut, u64 arrayNum, + std::shared_ptr ctx, NpTus::OrbisNpTusVariableA* variablesAOut, + s64 ownerAccountId, NpTus::OrbisNpTusVariableForCrossSave* variablesCSOut) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusGetMultiSlotVariableRequest proto; + proto.set_ownernpid(ownerNpId); + if (!virtualUser.empty()) { + proto.set_virtualuser(virtualUser); + } else if (ownerAccountId != 0) { + proto.set_owneraccountid(ownerAccountId); + } else if (ownerNpId.empty() || + Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { + proto.set_owneraccountid(static_cast(client->GetUserId())); + } + for (s32 s : slotIds) { + proto.add_slotids(s); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetMultiSlotVariable, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusGetMultiSlotVariable; + p.variableArray = variablesOut; + p.variableArrayA = variablesAOut; + p.variableArrayCS = variablesCSOut; + p.arrayNum = arrayNum; + p.user_id = user_id; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusSetMultiSlotVariable(s32 user_id, s32 service_label, const std::string& ownerNpId, + const std::string& virtualUser, + const std::vector& slotIds, + const std::vector& values, + std::shared_ptr ctx, + s64 ownerAccountId) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusSetMultiSlotVariableRequest proto; + proto.set_ownernpid(ownerNpId); + if (!virtualUser.empty()) { + proto.set_virtualuser(virtualUser); + } else if (ownerAccountId != 0) { + proto.set_owneraccountid(ownerAccountId); + } else if (ownerNpId.empty() || + Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { + proto.set_owneraccountid(static_cast(client->GetUserId())); + } + + for (s32 s : slotIds) { + proto.add_slotids(s); + } + for (s64 v : values) { + proto.add_values(v); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusSetMultiSlotVariable, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusSetMultiSlotVariable; + p.user_id = user_id; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TssGetData(s32 user_id, s32 service_label, s32 slotId, bool hasOffset, u64 offset, + bool hasLastByte, u64 lastByte, bool hasIfParam, s32 ifType, + u64 ifLastModified, NpTus::OrbisNpTssDataStatus* statusOut, void* dataOut, + u64 dataCap, std::shared_ptr ctx, + u64* contentLengthOut) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TssGetDataRequest proto; + proto.set_slotid(slotId); + if (hasOffset) { + proto.set_hasoffset(true); + proto.set_offset(offset); + } + if (hasLastByte) { + proto.set_haslastbyte(true); + proto.set_lastbyte(lastByte); + } + if (hasIfParam) { + proto.set_hasifparam(true); + proto.set_iftype(ifType); + proto.set_iflastmodified(ifLastModified); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TssGetData, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TssGetData; + p.user_id = user_id; + p.tssStatusOut = statusOut; + p.tssContentLengthOut = contentLengthOut; + p.dataOut = dataOut; + p.dataCap = dataCap; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusGetData(s32 user_id, s32 service_label, const std::string& ownerNpId, + const std::string& virtualUser, s64 ownerAccountId, s32 slotId, + NpTus::OrbisNpTusDataStatusA* statusAOut, u64 statusCap, void* dataOut, + u64 dataCap, u64 dataOffset, std::shared_ptr ctx, + NpTus::OrbisNpTusDataStatus* statusOut, + NpTus::OrbisNpTusDataStatusForCrossSave* statusCSOut) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusGetDataRequest proto; + proto.set_ownernpid(ownerNpId); + proto.set_slotid(slotId); + // Same targeting precedence as the variable paths. + if (!virtualUser.empty()) { + proto.set_virtualuser(virtualUser); + } else if (ownerAccountId != 0) { + proto.set_owneraccountid(ownerAccountId); + } else if (ownerNpId.empty() || + Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { + proto.set_owneraccountid(static_cast(client->GetUserId())); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetData, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusGetData; + p.user_id = user_id; + p.statusArrayA = statusAOut; + p.statusArray = statusOut; + p.statusArrayCS = statusCSOut; + p.statusCap = statusCap; + p.dataOut = dataOut; + p.dataCap = dataCap; + p.dataOffset = dataOffset; + p.arrayNum = 1; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusDeleteMultiSlotData(s32 user_id, s32 service_label, const std::string& ownerNpId, + const std::string& virtualUser, s64 ownerAccountId, + const std::vector& slotIds, + std::shared_ptr ctx) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusDeleteMultiSlotDataRequest proto; + proto.set_ownernpid(ownerNpId); + if (!virtualUser.empty()) { + proto.set_virtualuser(virtualUser); + } else if (ownerAccountId != 0) { + proto.set_owneraccountid(ownerAccountId); + } else if (ownerNpId.empty() || + Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { + proto.set_owneraccountid(static_cast(client->GetUserId())); + } + for (s32 s : slotIds) { + proto.add_slotids(s); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusDeleteMultiSlotData, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusDeleteMultiSlotData; + p.user_id = user_id; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusDeleteMultiSlotVariable(s32 user_id, s32 service_label, + const std::string& ownerNpId, + const std::string& virtualUser, s64 ownerAccountId, + const std::vector& slotIds, + std::shared_ptr ctx) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusDeleteMultiSlotVariableRequest proto; + proto.set_ownernpid(ownerNpId); + if (!virtualUser.empty()) { + proto.set_virtualuser(virtualUser); + } else if (ownerAccountId != 0) { + proto.set_owneraccountid(ownerAccountId); + } else if (ownerNpId.empty() || + Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { + proto.set_owneraccountid(static_cast(client->GetUserId())); + } + for (s32 s : slotIds) { + proto.add_slotids(s); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusDeleteMultiSlotVariable, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusDeleteMultiSlotVariable; + p.user_id = user_id; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusGetMultiUserDataStatus(s32 user_id, s32 service_label, s32 slotId, + const std::vector& ownerNpIds, + const std::vector& virtualUsers, + const std::vector& ownerAccountIds, + NpTus::OrbisNpTusDataStatus* statusOut, + NpTus::OrbisNpTusDataStatusA* statusAOut, u64 arrayNum, + std::shared_ptr ctx, + NpTus::OrbisNpTusDataStatusForCrossSave* statusCSOut) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusGetMultiUserDataStatusRequest proto; + proto.set_slotid(slotId); + for (const auto& vu : virtualUsers) { + proto.add_virtualusers(vu); + } + for (const auto& np : ownerNpIds) { + proto.add_ownernpids(np); + } + for (s64 acc : ownerAccountIds) { + proto.add_owneraccountids(acc); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetMultiUserDataStatus, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusGetMultiUserDataStatus; + p.user_id = user_id; + p.statusArray = statusOut; + p.statusArrayA = statusAOut; + p.statusArrayCS = statusCSOut; + p.arrayNum = arrayNum; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusGetMultiUserVariable(s32 user_id, s32 service_label, s32 slotId, + const std::vector& ownerNpIds, + const std::vector& virtualUsers, + const std::vector& ownerAccountIds, + NpTus::OrbisNpTusVariable* variablesOut, + NpTus::OrbisNpTusVariableA* variablesAOut, u64 arrayNum, + std::shared_ptr ctx, + NpTus::OrbisNpTusVariableForCrossSave* variablesCSOut) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusGetMultiUserVariableRequest proto; + proto.set_slotid(slotId); + for (const auto& vu : virtualUsers) { + proto.add_virtualusers(vu); + } + for (const auto& np : ownerNpIds) { + proto.add_ownernpids(np); + } + for (s64 acc : ownerAccountIds) { + proto.add_owneraccountids(acc); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetMultiUserVariable, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusGetMultiUserVariable; + p.user_id = user_id; + p.variableArray = variablesOut; + p.variableArrayA = variablesAOut; + p.variableArrayCS = variablesCSOut; + p.arrayNum = arrayNum; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusTryAndSetVariable(s32 user_id, s32 service_label, const std::string& ownerNpId, + const std::string& virtualUser, s64 ownerAccountId, s32 slotId, + s32 opeType, s64 value, bool hasCompare, s64 compareValue, + bool hasAuthorCheck, s64 isLastChangedAuthor, + const std::string& isLastChangedAuthorNpId, bool hasDateCheck, + u64 isLastChangedDate, NpTus::OrbisNpTusVariable* variableOut, + NpTus::OrbisNpTusVariableA* variableAOut, + std::shared_ptr ctx, + NpTus::OrbisNpTusVariableForCrossSave* variableCSOut) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusTryAndSetVariableRequest proto; + proto.set_ownernpid(ownerNpId); + proto.set_slotid(slotId); + proto.set_opetype(opeType); + proto.set_value(value); + // compareValue is optional: when absent the server compares against value + if (hasCompare) { + proto.set_hascompare(true); + proto.set_comparevalue(compareValue); + } + if (!virtualUser.empty()) { + proto.set_virtualuser(virtualUser); + } else if (ownerAccountId != 0) { + proto.set_owneraccountid(ownerAccountId); + } else if (ownerNpId.empty() || + Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { + proto.set_owneraccountid(static_cast(client->GetUserId())); + } + if (hasAuthorCheck) { + proto.set_hasauthorcheck(true); + proto.set_islastchangedauthor(isLastChangedAuthor); + if (!isLastChangedAuthorNpId.empty()) { + proto.set_islastchangedauthornpid(isLastChangedAuthorNpId); + } + } + if (hasDateCheck) { + proto.set_hasdatecheck(true); + proto.set_islastchangeddate(isLastChangedDate); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusTryAndSetVariable, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusTryAndSetVariable; + p.user_id = user_id; + p.variableArray = variableOut; + p.variableArrayA = variableAOut; + p.variableArrayCS = variableCSOut; + p.arrayNum = 1; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusAddAndGetVariable(s32 user_id, s32 service_label, const std::string& ownerNpId, + const std::string& virtualUser, s64 ownerAccountId, s32 slotId, + s64 inVariable, bool hasAuthorCheck, s64 isLastChangedAuthor, + const std::string& isLastChangedAuthorNpId, bool hasDateCheck, + u64 isLastChangedDate, NpTus::OrbisNpTusVariable* variableOut, + NpTus::OrbisNpTusVariableA* variableAOut, + std::shared_ptr ctx, + NpTus::OrbisNpTusVariableForCrossSave* variableCSOut) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusAddAndGetVariableRequest proto; + proto.set_ownernpid(ownerNpId); + proto.set_slotid(slotId); + proto.set_invalue(inVariable); + if (!virtualUser.empty()) { + proto.set_virtualuser(virtualUser); + } else if (ownerAccountId != 0) { + proto.set_owneraccountid(ownerAccountId); + } else if (ownerNpId.empty() || + Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { + proto.set_owneraccountid(static_cast(client->GetUserId())); + } + if (hasAuthorCheck) { + proto.set_hasauthorcheck(true); + proto.set_islastchangedauthor(isLastChangedAuthor); + if (!isLastChangedAuthorNpId.empty()) { + proto.set_islastchangedauthornpid(isLastChangedAuthorNpId); + } + } + if (hasDateCheck) { + proto.set_hasdatecheck(true); + proto.set_islastchangeddate(isLastChangedDate); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusAddAndGetVariable, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusAddAndGetVariable; + p.user_id = user_id; + p.variableArray = variableOut; + p.variableArrayA = variableAOut; + p.variableArrayCS = variableCSOut; + p.arrayNum = 1; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusGetFriendsDataStatus(s32 user_id, s32 service_label, s32 slotId, bool includeSelf, + s32 sortType, u32 max, + NpTus::OrbisNpTusDataStatus* statusOut, + NpTus::OrbisNpTusDataStatusA* statusAOut, u64 arrayNum, + std::shared_ptr ctx, u32 startOffset, + u32* hitsOut, + NpTus::OrbisNpTusDataStatusForCrossSave* statusCSOut) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + // Always targets the caller's own friend list, so there is no owner field. + shadnet::TusGetFriendsDataStatusRequest proto; + proto.set_slotid(slotId); + proto.set_includeself(includeSelf); + proto.set_sorttype(sortType); + proto.set_max(max); + proto.set_startoffset(startOffset); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetFriendsDataStatus, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusGetFriendsDataStatus; + p.user_id = user_id; + p.statusArray = statusOut; + p.statusArrayA = statusAOut; + p.statusArrayCS = statusCSOut; + p.arrayNum = arrayNum; + p.totalOut = hitsOut; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusGetFriendsVariable(s32 user_id, s32 service_label, s32 slotId, bool includeSelf, + s32 sortType, u32 max, NpTus::OrbisNpTusVariable* variablesOut, + NpTus::OrbisNpTusVariableA* variablesAOut, u64 arrayNum, + std::shared_ptr ctx, u32 startOffset, + u32* hitsOut, + NpTus::OrbisNpTusVariableForCrossSave* variablesCSOut) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + // Always targets the caller's own friend list, so there is no owner field. + shadnet::TusGetFriendsVariableRequest proto; + proto.set_slotid(slotId); + proto.set_includeself(includeSelf); + proto.set_sorttype(sortType); + proto.set_max(max); + proto.set_startoffset(startOffset); + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetFriendsVariable, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusGetFriendsVariable; + p.user_id = user_id; + p.variableArray = variablesOut; + p.variableArrayA = variablesAOut; + p.variableArrayCS = variablesCSOut; + p.arrayNum = arrayNum; + p.totalOut = hitsOut; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusGetMultiSlotDataStatus(s32 user_id, s32 service_label, + const std::string& ownerNpId, + const std::string& virtualUser, s64 ownerAccountId, + const std::vector& slotIds, + NpTus::OrbisNpTusDataStatus* statusOut, + NpTus::OrbisNpTusDataStatusA* statusAOut, u64 arrayNum, + std::shared_ptr ctx, + NpTus::OrbisNpTusDataStatusForCrossSave* statusCSOut) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusGetMultiSlotDataStatusRequest proto; + proto.set_ownernpid(ownerNpId); + if (!virtualUser.empty()) { + proto.set_virtualuser(virtualUser); + } else if (ownerAccountId != 0) { + proto.set_owneraccountid(ownerAccountId); + } else if (ownerNpId.empty() || + Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { + proto.set_owneraccountid(static_cast(client->GetUserId())); + } + for (s32 s : slotIds) { + proto.add_slotids(s); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusGetMultiSlotDataStatus, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusGetMultiSlotDataStatus; + p.user_id = user_id; + p.statusArray = statusOut; + p.statusArrayA = statusAOut; + p.statusArrayCS = statusCSOut; + p.arrayNum = arrayNum; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +s32 NpHandler::TusSetData(s32 user_id, s32 service_label, const std::string& ownerNpId, + const std::string& virtualUser, s64 ownerAccountId, s32 slotId, + const std::vector& blob, const std::vector& info, + bool hasAuthorCheck, s64 isLastChangedAuthor, + const std::string& isLastChangedAuthorNpId, bool hasDateCheck, + u64 isLastChangedDate, std::shared_ptr ctx) { + std::shared_ptr client; + { + std::lock_guard lock(m_mutex_clients); + auto it = m_clients.find(user_id); + if (it == m_clients.end()) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + shadnet::TusSetDataRequest proto; + proto.set_ownernpid(ownerNpId); + proto.set_slotid(slotId); + proto.set_data(blob.data(), blob.size()); + if (!info.empty()) { + proto.set_info(info.data(), info.size()); + } + if (!virtualUser.empty()) { + proto.set_virtualuser(virtualUser); + } else if (ownerAccountId != 0) { + proto.set_owneraccountid(ownerAccountId); + } else if (ownerNpId.empty() || + Common::ToLower(ownerNpId) == Common::ToLower(client->GetNpid())) { + proto.set_owneraccountid(static_cast(client->GetUserId())); + } + if (hasAuthorCheck) { + proto.set_hasauthorcheck(true); + proto.set_islastchangedauthor(isLastChangedAuthor); + if (!isLastChangedAuthorNpId.empty()) { + proto.set_islastchangedauthornpid(isLastChangedAuthorNpId); + } + } + if (hasDateCheck) { + proto.set_hasdatecheck(true); + proto.set_islastchangeddate(isLastChangedDate); + } + const std::string com_id = GetNpCommId(service_label); + if (!IsValidNpCommId(com_id)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::TusSetData, + BuildTusPayload(com_id, proto.SerializeAsString())); + std::lock_guard lock(m_mutex_pending_tus); + PendingTusRequest p; + p.req = std::move(ctx); + p.cmd = ShadNet::CommandType::TusSetData; + p.user_id = user_id; + m_pending_tus.emplace(pkt_id, std::move(p)); + return ORBIS_OK; +} + +void NpHandler::OnTusReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, + ShadNet::ErrorType error, const std::vector& body) { + PendingTusRequest pending; + { + std::lock_guard lock(m_mutex_pending_tus); + auto it = m_pending_tus.find(pkt_id); + if (it == m_pending_tus.end()) { + LOG_WARNING(NpHandler, "no pending request for pkt_id={} cmd={}", pkt_id, + static_cast(cmd)); + return; + } + pending = std::move(it->second); + m_pending_tus.erase(it); + } + auto& req = pending.req; + + if (error != ShadNet::ErrorType::NoError) { + s32 orbis_err = ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE; + switch (error) { + case ShadNet::ErrorType::Unauthorized: + orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_FORBIDDEN; + break; + case ShadNet::ErrorType::NotFound: + // Owner npid did not resolve to a registered user. + orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_USER_PROFILE_NOT_FOUND; + break; + case ShadNet::ErrorType::DbFail: + orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_INTERNAL_SERVER_ERROR; + break; + case ShadNet::ErrorType::CondFail: + orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_CONDITIONS_NOT_SATISFIED; + break; + default: + break; + } + LOG_WARNING(NpHandler, "user_id={} pkt_id={} cmd={} server_error={} -> orbis {:#x}", + user_id, pkt_id, static_cast(cmd), static_cast(error), + static_cast(orbis_err)); + req->SetResult(orbis_err); + return; + } + + auto parseTusBody = [&](auto& msg) -> bool { + if (body.size() < 4) { + LOG_ERROR(NpHandler, "OnTusReply: cmd={} body too small ({})", static_cast(cmd), + body.size()); + return false; + } + const u32 proto_size = static_cast(body[0]) | (static_cast(body[1]) << 8) | + (static_cast(body[2]) << 16) | + (static_cast(body[3]) << 24); + if (static_cast(4) + proto_size > body.size()) { + LOG_ERROR(NpHandler, "OnTusReply: cmd={} proto size {} exceeds body size {}", + static_cast(cmd), proto_size, body.size()); + return false; + } + if (!msg.ParseFromArray(body.data() + 4, static_cast(proto_size))) { + LOG_ERROR(NpHandler, "OnTusReply: cmd={} proto parse failed", static_cast(cmd)); + return false; + } + return true; + }; + + auto fillVariable = [](NpTus::OrbisNpTusVariable& v, const shadnet::TusVariable& s) { + v = NpTus::OrbisNpTusVariable{}; + CopyNpHandle(v.ownerId, s.ownernpid()); + v.hasData = s.set() ? 1 : 0; + v.lastChangedDate.tick = s.lastchangeddate(); + CopyNpHandle(v.lastChangedAuthorId, s.lastchangedauthornpid()); + v.variable = s.variable(); + v.oldVariable = s.oldvariable(); + v.ownerAccountId = static_cast(s.owneraccountid()); + v.lastChangedAuthorAccountId = + static_cast(s.lastchangedauthoraccountid()); + }; + auto fillVariableA = [](NpTus::OrbisNpTusVariableA& v, const shadnet::TusVariable& s) { + v = NpTus::OrbisNpTusVariableA{}; + std::memcpy(v.ownerId.data, s.ownernpid().data(), + std::min(s.ownernpid().size(), sizeof(v.ownerId.data))); + v.hasData = s.set() ? 1 : 0; + v.lastChangedDate.tick = s.lastchangeddate(); + std::memcpy(v.lastChangedAuthorId.data, s.lastchangedauthornpid().data(), + std::min(s.lastchangedauthornpid().size(), sizeof(v.lastChangedAuthorId.data))); + v.variable = s.variable(); + v.oldVariable = s.oldvariable(); + v.ownerAccountId = static_cast(s.owneraccountid()); + v.lastChangedAuthorAccountId = + static_cast(s.lastchangedauthoraccountid()); + }; + auto fillVariableForCrossSave = [](NpTus::OrbisNpTusVariableForCrossSave& v, + const shadnet::TusVariable& s) { + v = NpTus::OrbisNpTusVariableForCrossSave{}; + CopyNpHandle(v.ownerId, s.ownernpid()); + v.hasData = s.set() ? 1 : 0; + v.lastChangedDate.tick = s.lastchangeddate(); + CopyNpHandle(v.lastChangedAuthorId, s.lastchangedauthornpid()); + v.variable = s.variable(); + v.oldVariable = s.oldvariable(); + v.ownerAccountId = static_cast(s.owneraccountid()); + v.lastChangedAuthorAccountId = + static_cast(s.lastchangedauthoraccountid()); + }; + auto fillStatus = [](NpTus::OrbisNpTusDataStatus& d, const shadnet::TusDataStatus& s) { + d = NpTus::OrbisNpTusDataStatus{}; + CopyNpHandle(d.ownerId, s.ownernpid()); + d.hasData = s.set() ? 1 : 0; + d.lastChangedDate.tick = s.lastchangeddate(); + CopyNpHandle(d.lastChangedAuthorId, s.lastchangedauthornpid()); + d.dataSize = s.datasize(); + d.info.size = s.info().size(); + const size_t cp = std::min(s.info().size(), sizeof(d.info.data)); + std::memcpy(d.info.data, s.info().data(), cp); + }; + auto fillStatusA = [](NpTus::OrbisNpTusDataStatusA& d, const shadnet::TusDataStatus& s) { + d = NpTus::OrbisNpTusDataStatusA{}; + std::memcpy(d.ownerId.data, s.ownernpid().data(), + std::min(s.ownernpid().size(), sizeof(d.ownerId.data))); + d.hasData = s.set() ? 1 : 0; + d.lastChangedDate.tick = s.lastchangeddate(); + std::memcpy(d.lastChangedAuthorId.data, s.lastchangedauthornpid().data(), + std::min(s.lastchangedauthornpid().size(), sizeof(d.lastChangedAuthorId.data))); + d.dataSize = s.datasize(); + d.info.size = s.info().size(); + const size_t cp = std::min(s.info().size(), sizeof(d.info.data)); + std::memcpy(d.info.data, s.info().data(), cp); + d.ownerAccountId = static_cast(s.owneraccountid()); + d.lastChangedAuthorAccountId = + static_cast(s.lastchangedauthoraccountid()); + }; + auto fillStatusForCrossSave = [](NpTus::OrbisNpTusDataStatusForCrossSave& d, + const shadnet::TusDataStatus& s) { + d = NpTus::OrbisNpTusDataStatusForCrossSave{}; + CopyNpHandle(d.ownerId, s.ownernpid()); + d.hasData = s.set() ? 1 : 0; + d.lastChangedDate.tick = s.lastchangeddate(); + CopyNpHandle(d.lastChangedAuthorId, s.lastchangedauthornpid()); + d.dataSize = s.datasize(); + d.info.size = s.info().size(); + const size_t cp = std::min(s.info().size(), sizeof(d.info.data)); + std::memcpy(d.info.data, s.info().data(), cp); + d.ownerAccountId = static_cast(s.owneraccountid()); + d.lastChangedAuthorAccountId = + static_cast(s.lastchangedauthoraccountid()); + }; + + switch (cmd) { + case ShadNet::CommandType::TusGetMultiSlotVariable: + case ShadNet::CommandType::TusTryAndSetVariable: + case ShadNet::CommandType::TusGetMultiUserVariable: + case ShadNet::CommandType::TusGetFriendsVariable: + case ShadNet::CommandType::TusAddAndGetVariable: { + shadnet::TusVariableResponse resp; + if (!parseTusBody(resp)) { + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + return; + } + u64 filled = 0; + if (pending.variableArrayA) { + filled = std::min(pending.arrayNum, resp.variables_size()); + for (u64 i = 0; i < filled; ++i) { + fillVariableA(pending.variableArrayA[i], resp.variables(static_cast(i))); + } + } else if (pending.variableArrayCS) { + filled = std::min(pending.arrayNum, resp.variables_size()); + for (u64 i = 0; i < filled; ++i) { + fillVariableForCrossSave(pending.variableArrayCS[i], + resp.variables(static_cast(i))); + } + } else if (pending.variableArray) { + filled = std::min(pending.arrayNum, resp.variables_size()); + for (u64 i = 0; i < filled; ++i) { + fillVariable(pending.variableArray[i], resp.variables(static_cast(i))); + } + } + // GetMultiSlotVariable / GetMultiUserVariable return the count of variables read; + // AddAndGet / TryAndSet return 0 on normal termination (value is in the struct). + if (pending.totalOut) { + // OrbisNpTusGetFriendsVariableOptParam::hits - total registered + // friends before startSerialRank/arrayNum are applied. + *pending.totalOut = + resp.total() ? resp.total() : static_cast(resp.variables_size()); + } + if (cmd == ShadNet::CommandType::TusGetMultiSlotVariable || + cmd == ShadNet::CommandType::TusGetMultiUserVariable || + cmd == ShadNet::CommandType::TusGetFriendsVariable) { + req->SetResult(static_cast(filled)); + } + break; + } + case ShadNet::CommandType::TssGetData: { + shadnet::TssGetDataResponse resp; + if (!parseTusBody(resp)) { + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + return; + } + u64 recv = 0; + if (pending.dataOut && !resp.data().empty()) { + recv = std::min(pending.dataCap, resp.data().size()); + std::memcpy(pending.dataOut, resp.data().data(), recv); + } + if (pending.tssContentLengthOut) { + *pending.tssContentLengthOut = resp.contentlength(); + } + if (pending.tssStatusOut) { + *pending.tssStatusOut = NpTus::OrbisNpTssDataStatus{}; + pending.tssStatusOut->modified.tick = resp.lastmodified(); + pending.tssStatusOut->status = + static_cast(resp.statuscodetype()); + pending.tssStatusOut->contentLength = resp.contentlength(); + } + req->SetResult(ORBIS_OK); + return; + } + case ShadNet::CommandType::TusGetData: { + shadnet::TusGetDataResponse resp; + if (!parseTusBody(resp)) { + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + return; + } + u64 recv = 0; + const u64 blob_size = resp.data().size(); + if (pending.dataOut && pending.dataOffset < blob_size) { + const u64 avail = blob_size - pending.dataOffset; + recv = std::min(pending.dataCap, avail); + std::memcpy(pending.dataOut, resp.data().data() + pending.dataOffset, recv); + } + if (pending.statusArrayA) { + NpTus::OrbisNpTusDataStatusA tmp{}; + fillStatusA(tmp, resp.status()); + if (recv) { + tmp.data = pending.dataOut; + } + const u64 cap = + pending.statusCap ? std::min(pending.statusCap, sizeof(tmp)) : sizeof(tmp); + std::memcpy(pending.statusArrayA, &tmp, cap); + } else if (pending.statusArrayCS) { + NpTus::OrbisNpTusDataStatusForCrossSave tmp{}; + fillStatusForCrossSave(tmp, resp.status()); + if (recv) { + tmp.data = pending.dataOut; + } + const u64 cap = + pending.statusCap ? std::min(pending.statusCap, sizeof(tmp)) : sizeof(tmp); + std::memcpy(pending.statusArrayCS, &tmp, cap); + } else if (pending.statusArray) { + NpTus::OrbisNpTusDataStatus tmp{}; + fillStatus(tmp, resp.status()); + if (recv) { + tmp.data = pending.dataOut; + } + const u64 cap = + pending.statusCap ? std::min(pending.statusCap, sizeof(tmp)) : sizeof(tmp); + std::memcpy(pending.statusArray, &tmp, cap); + } + req->SetResult(static_cast(recv)); + return; + } + case ShadNet::CommandType::TusGetMultiSlotDataStatus: + case ShadNet::CommandType::TusGetMultiUserDataStatus: + case ShadNet::CommandType::TusGetFriendsDataStatus: { + shadnet::TusDataStatusResponse resp; + if (!parseTusBody(resp)) { + req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE); + return; + } + if (pending.statusArrayA) { + const u64 n = std::min(pending.arrayNum, resp.statuses_size()); + for (u64 i = 0; i < n; ++i) { + fillStatusA(pending.statusArrayA[i], resp.statuses(static_cast(i))); + } + } else if (pending.statusArrayCS) { + const u64 n = std::min(pending.arrayNum, resp.statuses_size()); + for (u64 i = 0; i < n; ++i) { + fillStatusForCrossSave(pending.statusArrayCS[i], + resp.statuses(static_cast(i))); + } + } else if (pending.statusArray) { + const u64 n = std::min(pending.arrayNum, resp.statuses_size()); + for (u64 i = 0; i < n; ++i) { + fillStatus(pending.statusArray[i], resp.statuses(static_cast(i))); + } + } + if (pending.totalOut) { + // hits = total registered friends before offset/cap + *pending.totalOut = + resp.total() ? resp.total() : static_cast(resp.statuses_size()); + } + if (cmd == ShadNet::CommandType::TusGetFriendsDataStatus || + cmd == ShadNet::CommandType::TusGetMultiSlotDataStatus || + cmd == ShadNet::CommandType::TusGetMultiUserDataStatus) { + // These calls return the number of statuses read (>= 0), not ORBIS_OK. + const s32 cnt = static_cast(std::min(pending.arrayNum, resp.statuses_size())); + req->SetResult(cnt); + return; + } + break; + } + default: + // Set*/Delete* carry no payload to fill. + LOG_DEBUG(NpHandler, "cmd={} ok (no payload)", static_cast(cmd)); + break; + } + req->SetResult(ORBIS_OK); +} + +} // namespace Libraries::Np diff --git a/src/core/libraries/np/np_handler/np_handler_web_api.cpp b/src/core/libraries/np/np_handler/np_handler_web_api.cpp new file mode 100644 index 000000000..da636f451 --- /dev/null +++ b/src/core/libraries/np/np_handler/np_handler_web_api.cpp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "common/logging/log.h" +#include "core/libraries/np/np_handler/np_handler.h" +#include "core/libraries/np/np_web_api/np_web_api.h" +#include "imgui/invitation_prompt_layer.h" + +namespace Libraries::Np { + +void NpHandler::OnWebApiPushEvent(s32 user_id, const ShadNet::NotifyWebApiPushEvent& n) { + LOG_INFO(NpHandler, "user_id={} WebApiPushEvent svc='{}' type='{}' bytes={}", user_id, + n.npServiceName, n.dataType, n.data.size()); + NpWebApi::PushEventInput ev; + ev.targetUserId = user_id; + ev.npServiceName = n.npServiceName; + ev.npServiceLabel = n.npServiceLabel; + ev.dataType = n.dataType; + ev.data = n.data; + if (!n.fromNpid.empty()) { + ev.hasFrom = true; + SetNpOnlineId(ev.fromOnlineId, n.fromNpid); + } + if (!n.toNpid.empty()) { + ev.hasTo = true; + SetNpOnlineId(ev.toOnlineId, n.toNpid); + } + ev.extdData = n.extdData; // extended-data (key,value) pairs -> dispatched as pExtdData + // Account ids for pFrom/pTo. + ev.toAccountId = n.toAccountId != 0 ? n.toAccountId : GetAccountId(user_id); + ev.fromAccountId = n.fromAccountId; + if (ev.fromAccountId == 0) { + for (const auto& kv : n.extdData) { + if (kv.first == "fromAccountId") { + ev.fromAccountId = std::strtoull(kv.second.c_str(), nullptr, 10); + break; + } + } + } + if (ev.toAccountId != 0) { + ev.hasTo = true; + if (n.toNpid.empty()) { + // e.g. friendlist events carry no toNpid; the recipient is still us. + std::lock_guard lock(m_mutex_clients); + if (const auto it = m_np_ids.find(user_id); it != m_np_ids.end()) { + std::memcpy(ev.toOnlineId.data, it->second.handle.data, sizeof(ev.toOnlineId.data)); + } + } + } + NpWebApi::EnqueuePushEvent(ev); + + // Also surface a SESSION_INVITATION system-service event for titles that watch it instead of + // (or in addition to) the WebAPI push callback + if (n.npServiceName == "sessionInvitation") { + std::string session_id, invitation_id; + int64_t valid_until = 0; + for (const auto& kv : n.extdData) { + if (kv.first == "sessionId") { + session_id = kv.second; + } else if (kv.first == "invitationId") { + invitation_id = kv.second; + } else if (kv.first == "validUntil") { + valid_until = std::strtoll(kv.second.c_str(), nullptr, 10); + } + } + if (!session_id.empty()) { + { + std::lock_guard lk(m_mutex_pending_invites); + auto& v = m_pending_invites[user_id]; + v.erase(std::remove_if(v.begin(), v.end(), + [&](const PendingInvitation& p) { + return p.invitation_id == invitation_id; + }), + v.end()); + PendingInvitation inv{session_id, invitation_id, n.fromNpid, n.toNpid, valid_until}; + inv.from_account_id = ev.fromAccountId; + v.push_back(std::move(inv)); + } + ImGui::InvitationPrompt::Push(user_id, invitation_id, session_id, n.fromNpid); + } + } +} + +} // namespace Libraries::Np diff --git a/src/core/libraries/np/np_manager.cpp b/src/core/libraries/np/np_manager.cpp index 688dd0b27..459181480 100644 --- a/src/core/libraries/np/np_manager.cpp +++ b/src/core/libraries/np/np_manager.cpp @@ -15,11 +15,11 @@ #include "core/libraries/kernel/process.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_manager.h" #include "core/tls.h" #include "core/user_manager.h" #include "core/user_settings.h" -#include "np_handler.h" namespace Libraries::Np::NpManager { diff --git a/src/core/libraries/np/np_matching2/np_matching2.cpp b/src/core/libraries/np/np_matching2/np_matching2.cpp index 135e1181c..8e9ddfc1c 100644 --- a/src/core/libraries/np/np_matching2/np_matching2.cpp +++ b/src/core/libraries/np/np_matching2/np_matching2.cpp @@ -11,7 +11,7 @@ #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/net/net.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #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" 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 538f6b170..4397b6b7f 100644 --- a/src/core/libraries/np/np_matching2/np_matching2_internal.cpp +++ b/src/core/libraries/np/np_matching2/np_matching2_internal.cpp @@ -14,7 +14,7 @@ #include "core/libraries/net/net.h" #include "core/libraries/np/np_common.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_matching2/np_matching2_internal.h" #include "shadnet.pb.h" 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 58be9c725..a6a8fe68a 100644 --- a/src/core/libraries/np/np_matching2/np_matching2_mm.cpp +++ b/src/core/libraries/np/np_matching2/np_matching2_mm.cpp @@ -19,7 +19,7 @@ #include "core/libraries/net/net_p2p.h" #include "core/libraries/net/net_upnp.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.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_signaling.h" 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 9adc94245..f25a4fcf5 100644 --- a/src/core/libraries/np/np_matching2/np_matching2_signaling.cpp +++ b/src/core/libraries/np/np_matching2/np_matching2_signaling.cpp @@ -14,7 +14,7 @@ #include "core/libraries/net/net_util.h" #include "core/libraries/network/netctl.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.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_signaling.h" diff --git a/src/core/libraries/np/np_score/np_score.cpp b/src/core/libraries/np/np_score/np_score.cpp index 405e9a092..e9e106455 100644 --- a/src/core/libraries/np/np_score/np_score.cpp +++ b/src/core/libraries/np/np_score/np_score.cpp @@ -12,7 +12,7 @@ #include "core/libraries/kernel/process.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_score/np_score.h" #include "core/libraries/np/np_score/np_score_ctx.h" diff --git a/src/core/libraries/np/np_signaling/np_signaling.cpp b/src/core/libraries/np/np_signaling/np_signaling.cpp index ce2127026..4b9e8b87e 100644 --- a/src/core/libraries/np/np_signaling/np_signaling.cpp +++ b/src/core/libraries/np/np_signaling/np_signaling.cpp @@ -12,7 +12,7 @@ #include "core/libraries/net/net_util.h" #include "core/libraries/network/netctl.h" #include "core/libraries/np/np_common.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_manager.h" #include "core/libraries/np/np_signaling/np_signaling.h" #include "core/libraries/np/np_signaling/np_signaling_helpers.h" 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 bb311353e..4ac4e7903 100644 --- a/src/core/libraries/np/np_signaling/np_signaling_helpers.cpp +++ b/src/core/libraries/np/np_signaling/np_signaling_helpers.cpp @@ -20,7 +20,7 @@ #include "core/libraries/net/net_util.h" #include "core/libraries/np/np_common.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_signaling/np_signaling.h" #include "core/libraries/np/np_signaling/np_signaling_helpers.h" #include "core/libraries/np/np_signaling/np_signaling_state.h" 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 348da9f74..05d318d31 100644 --- a/src/core/libraries/np/np_signaling/np_signaling_state.cpp +++ b/src/core/libraries/np/np_signaling/np_signaling_state.cpp @@ -17,7 +17,7 @@ #include "core/libraries/net/net.h" #include "core/libraries/net/net_util.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_signaling/np_signaling_state.h" #include "core/libraries/np/np_signaling/np_signaling_transport.h" diff --git a/src/core/libraries/np/np_trophy.cpp b/src/core/libraries/np/np_trophy.cpp index e976703a9..0c7369163 100644 --- a/src/core/libraries/np/np_trophy.cpp +++ b/src/core/libraries/np/np_trophy.cpp @@ -13,7 +13,7 @@ #include "core/emulator_settings.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_trophy.h" #include "core/libraries/np/trophy_ui.h" #include "core/libraries/system/userservice.h" diff --git a/src/core/libraries/np/np_tus/np_tus.cpp b/src/core/libraries/np/np_tus/np_tus.cpp index 600009b6f..a66211b69 100644 --- a/src/core/libraries/np/np_tus/np_tus.cpp +++ b/src/core/libraries/np/np_tus/np_tus.cpp @@ -9,7 +9,7 @@ #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_manager.h" #include "core/libraries/np/np_tus/np_tus.h" #include "core/libraries/np/np_types.h" diff --git a/src/core/libraries/np/np_utility/np_utility.cpp b/src/core/libraries/np/np_utility/np_utility.cpp index 9398b8d09..48f3913cf 100644 --- a/src/core/libraries/np/np_utility/np_utility.cpp +++ b/src/core/libraries/np/np_utility/np_utility.cpp @@ -12,7 +12,7 @@ #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_utility/np_utility.h" #include "core/libraries/np/np_utility/np_utility_ctx.h" diff --git a/src/core/libraries/np/np_web_api/np_web_api_internal.cpp b/src/core/libraries/np/np_web_api/np_web_api_internal.cpp index bbde526ea..31cece50e 100644 --- a/src/core/libraries/np/np_web_api/np_web_api_internal.cpp +++ b/src/core/libraries/np/np_web_api/np_web_api_internal.cpp @@ -15,7 +15,7 @@ #include "core/libraries/kernel/time.h" #include "core/libraries/network/http.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "np_web_api_internal.h" namespace Libraries::Np::NpWebApi { diff --git a/src/core/libraries/np/np_web_api2/np_web_api2_context.cpp b/src/core/libraries/np/np_web_api2/np_web_api2_context.cpp index 1424ff054..5975cd870 100644 --- a/src/core/libraries/np/np_web_api2/np_web_api2_context.cpp +++ b/src/core/libraries/np/np_web_api2/np_web_api2_context.cpp @@ -6,7 +6,7 @@ #include "core/libraries/network/http2.h" #include "core/libraries/np/np_common.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_web_api2/np_web_api2_context.h" namespace Libraries::Np::NpWebApi2 { diff --git a/src/core/libraries/np/signaling_handler.cpp b/src/core/libraries/np/signaling_handler.cpp index fab66677f..dd4c3b1c4 100644 --- a/src/core/libraries/np/signaling_handler.cpp +++ b/src/core/libraries/np/signaling_handler.cpp @@ -18,7 +18,7 @@ #include "core/libraries/net/net.h" #include "core/libraries/net/net_util.h" #include "core/libraries/np/np_error.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_matching2/np_matching2_internal.h" #include "core/libraries/np/np_matching2/np_matching2_signaling.h" #include "core/libraries/np/np_signaling/np_signaling_state.h" diff --git a/src/core/libraries/system/userservice.cpp b/src/core/libraries/system/userservice.cpp index d7924797c..9a5082507 100644 --- a/src/core/libraries/system/userservice.cpp +++ b/src/core/libraries/system/userservice.cpp @@ -10,7 +10,7 @@ #include "common/singleton.h" #include "core/emulator_settings.h" #include "core/libraries/libs.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_manager.h" #include "core/libraries/system/userservice.h" #include "core/libraries/system/userservice_error.h" diff --git a/src/emulator.cpp b/src/emulator.cpp index 0b3bae00d..76b6145b4 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -38,7 +38,7 @@ #include "core/file_sys/fs.h" #include "core/libraries/kernel/kernel.h" #include "core/libraries/libs.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "core/libraries/np/np_trophy.h" #include "core/libraries/save_data/save_backup.h" #include "core/linker.h" diff --git a/src/imgui/friends_layer.cpp b/src/imgui/friends_layer.cpp index 7906c2d72..04b811c13 100644 --- a/src/imgui/friends_layer.cpp +++ b/src/imgui/friends_layer.cpp @@ -8,7 +8,7 @@ #include #include "common/types.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "imgui/friends_layer.h" #include "imgui/imgui_layer.h" #include "imgui/shadnet_notifications_layer.h" diff --git a/src/imgui/invitation_prompt_layer.cpp b/src/imgui/invitation_prompt_layer.cpp index 6dcc9b751..dd7f18b42 100644 --- a/src/imgui/invitation_prompt_layer.cpp +++ b/src/imgui/invitation_prompt_layer.cpp @@ -11,7 +11,7 @@ #include #include "common/types.h" -#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_handler/np_handler.h" #include "imgui/imgui_layer.h" #include "imgui/invitation_prompt_layer.h" #include "imgui/renderer/imgui_core.h"