From aff387e8a9fdc01e93d4ee79cf3a910867cd2818 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Wed, 24 Jun 2026 23:57:43 +0300 Subject: [PATCH] First ShadNet integration (#4607) * initial shadnet client (based on my previous prs) * some np_handler work mostly based on our previous pr * improved login/logout event of shadnet * added np_score support from previous pr * friends and notifications ui * clang is not my friend * clang fix again * added webapi support for shadnet * applied metrik's fix * clang * fixed some wrong LOG_ERRORS * updated emulator settings * trying to fix npcommid bugs * fixed npcommid * improved NPcommid detection so it won't write null values to database * metrik's suggestion * removed one line * more command types * fixed duplicated log print * fixed compile * improved reconnection retries * simplify error code * stephen suggestion * made friends hotkey * fixup * more proper handling of reconnecting * fixed return code --- .gitmodules | 3 + CMakeLists.txt | 28 + externals/CMakeLists.txt | 12 + externals/protobuf | 1 + src/common/elf_info.h | 9 + src/common/logging/classes.h | 2 + src/common/logging/log.cpp | 2 + src/core/emulator_settings.h | 19 +- src/core/libraries/network/http.cpp | 9 +- src/core/libraries/np/np_handler.cpp | 1844 ++++++++++++++++- src/core/libraries/np/np_handler.h | 255 ++- src/core/libraries/np/np_manager.cpp | 432 +++- src/core/libraries/np/np_score/np_score.cpp | 1771 ++++++++++++---- src/core/libraries/np/np_score/np_score.h | 4 +- .../libraries/np/np_web_api/np_web_api.cpp | 6 +- .../np/np_web_api/np_web_api_internal.cpp | 134 +- .../np/np_web_api/np_web_api_internal.h | 3 + src/emulator.cpp | 2 + src/imgui/friends_layer.cpp | 208 ++ src/imgui/friends_layer.h | 16 + src/imgui/shadnet_notifications_layer.cpp | 175 ++ src/imgui/shadnet_notifications_layer.h | 52 + src/input/input_handler.cpp | 4 + src/input/input_handler.h | 6 +- src/sdl_window.cpp | 4 + src/shadnet/client.cpp | 751 +++++++ src/shadnet/client.h | 298 +++ src/shadnet/shadnet.proto | 180 ++ .../renderer_vulkan/vk_presenter.cpp | 6 + 29 files changed, 5749 insertions(+), 487 deletions(-) create mode 160000 externals/protobuf create mode 100644 src/imgui/friends_layer.cpp create mode 100644 src/imgui/friends_layer.h create mode 100644 src/imgui/shadnet_notifications_layer.cpp create mode 100644 src/imgui/shadnet_notifications_layer.h create mode 100644 src/shadnet/client.cpp create mode 100644 src/shadnet/client.h create mode 100644 src/shadnet/shadnet.proto diff --git a/.gitmodules b/.gitmodules index 2fabb2873..248a04748 100644 --- a/.gitmodules +++ b/.gitmodules @@ -155,3 +155,6 @@ path = externals/vulkan-loader url = https://github.com/KhronosGroup/Vulkan-Loader shallow = true +[submodule "externals/protobuf"] + path = externals/protobuf + url = https://github.com/shadexternals/protobuf.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 094e6fda0..2c1890de0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1129,6 +1129,10 @@ set(IMGUI src/imgui/imgui_config.h src/imgui/imgui_texture.h src/imgui/imgui_translations.cpp src/imgui/imgui_translations.h + src/imgui/friends_layer.cpp + src/imgui/friends_layer.h + src/imgui/shadnet_notifications_layer.cpp + src/imgui/shadnet_notifications_layer.h src/imgui/notifications_layer.cpp src/imgui/notifications_layer.h src/imgui/renderer/imgui_core.cpp @@ -1169,6 +1173,27 @@ set(EMULATOR src/emulator.cpp src/sdl_window.cpp ) +# shadNet protobuf generation +set(SHADNET_PROTO_OUT "${CMAKE_CURRENT_BINARY_DIR}/shadnet_proto_gen") +file(MAKE_DIRECTORY "${SHADNET_PROTO_OUT}") +add_custom_command( + OUTPUT + "${SHADNET_PROTO_OUT}/shadnet.pb.cc" + "${SHADNET_PROTO_OUT}/shadnet.pb.h" + COMMAND protobuf::protoc + "--proto_path=${CMAKE_CURRENT_SOURCE_DIR}/src/shadnet" + "--cpp_out=${SHADNET_PROTO_OUT}" + "${CMAKE_CURRENT_SOURCE_DIR}/src/shadnet/shadnet.proto" + DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/shadnet/shadnet.proto" + COMMENT "Generating shadnet protobuf sources" + VERBATIM +) + +set(SHADNET src/shadnet/client.cpp + src/shadnet/client.h + "${SHADNET_PROTO_OUT}/shadnet.pb.cc" +) if (APPLE) list(APPEND EMULATOR src/sdl_window_apple.mm) endif() @@ -1184,6 +1209,7 @@ add_executable(shadps4 ${SHADER_RECOMPILER} ${VIDEO_CORE} ${EMULATOR} + ${SHADNET} src/main.cpp src/emulator.cpp src/emulator.h @@ -1198,6 +1224,8 @@ target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::to target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 pugixml::pugixml) target_link_libraries(shadps4 PRIVATE stb::headers lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz::miniz fdk-aac CLI11::CLI11 OpenAL::OpenAL Cpp_Httplib miniupnpc::miniupnpc) target_link_libraries(shadps4 PRIVATE Freetype::Freetype) +target_link_libraries(shadps4 PRIVATE libprotobuf) +target_include_directories(shadps4 PRIVATE "${SHADNET_PROTO_OUT}") if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") target_link_libraries(shadps4 PRIVATE "/usr/lib/libusb.so") diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index be1848cef..16b7253fc 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -381,3 +381,15 @@ if (APPLE AND NOT ENABLE_SYSTEM_VULKAN AND NOT ENABLE_TESTS) set(VULKAN_DRIVER_PATH "${CMAKE_CURRENT_BINARY_DIR}/mesa-kosmickrisp/outputs/libvulkan_kosmickrisp.dylib" PARENT_SCOPE) set(VULKAN_ICD_PATH "${CMAKE_CURRENT_BINARY_DIR}/mesa-kosmickrisp/outputs/kosmickrisp_mesa_icd.json" PARENT_SCOPE) endif() + +# protobuf +set(protobuf_BUILD_TESTS OFF CACHE INTERNAL "") +set(protobuf_BUILD_EXAMPLES OFF CACHE INTERNAL "") +set(protobuf_BUILD_PROTOC_BINARIES ON CACHE INTERNAL "") +set(protobuf_INSTALL OFF CACHE INTERNAL "") +set(protobuf_WITH_ZLIB OFF CACHE INTERNAL "") +set(protobuf_BUILD_SHARED_LIBS OFF CACHE INTERNAL "") +if(MSVC) +set(protobuf_MSVC_STATIC_RUNTIME OFF CACHE BOOL "" FORCE) +endif() +add_subdirectory(protobuf EXCLUDE_FROM_ALL) diff --git a/externals/protobuf b/externals/protobuf new file mode 160000 index 000000000..5917ee224 --- /dev/null +++ b/externals/protobuf @@ -0,0 +1 @@ +Subproject commit 5917ee224a7e47383a78acc129628511fe210dff diff --git a/src/common/elf_info.h b/src/common/elf_info.h index 0525679a9..6241ccc0d 100644 --- a/src/common/elf_info.h +++ b/src/common/elf_info.h @@ -75,6 +75,7 @@ class ElfInfo { std::filesystem::path splash_path{}; std::filesystem::path game_folder{}; + std::vector npCommIds{}; std::map trophy_index_map{}; public: @@ -148,6 +149,14 @@ public: [[nodiscard]] const std::map& GetTrophyIndexMap() const { return trophy_index_map; } + + [[nodiscard]] const std::vector& GetNpCommIds() const { + return npCommIds; + } + + void SetNpCommIds(std::vector ids) { + npCommIds = std::move(ids); + } }; } // namespace Common diff --git a/src/common/logging/classes.h b/src/common/logging/classes.h index 3a5240f1d..3d1863db5 100644 --- a/src/common/logging/classes.h +++ b/src/common/logging/classes.h @@ -107,9 +107,11 @@ constexpr auto Lib_WebBrowserDialog = "Lib.WebBrowserDialog"; ///< The Lib constexpr auto Lib_Zlib = "Lib.Zlib"; ///< The LibSceZlib implementation. constexpr auto Loader = "Loader"; ///< ROM loader constexpr auto Log = "Log"; ///< Messages about the log system itself +constexpr auto NpHandler = "NpHandler"; ///< NpHandler shadNet manager constexpr auto Render = "Render"; ///< Video Core constexpr auto Render_Recompiler = "Render.Recompiler"; ///< Shader recompiler constexpr auto Render_Vulkan = "Render.Vulkan"; ///< Vulkan backend +constexpr auto ShadNet = "ShadNet"; ///< shadNet binary protocol client constexpr auto Tty = "Tty"; ///< Debug output from emu // clang-format on } // namespace Common::Log::Class diff --git a/src/common/logging/log.cpp b/src/common/logging/log.cpp index a50090f58..94e28a43b 100644 --- a/src/common/logging/log.cpp +++ b/src/common/logging/log.cpp @@ -131,9 +131,11 @@ std::unordered_map> ALL_LOGGER {Class::Lib_Zlib, nullptr}, {Class::Loader, nullptr}, {Class::Log, nullptr}, + {Class::NpHandler, nullptr}, {Class::Render, nullptr}, {Class::Render_Recompiler, nullptr}, {Class::Render_Vulkan, nullptr}, + {Class::ShadNet, nullptr}, {Class::Tty, nullptr}, }; diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h index 85448f10c..79797396c 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -183,9 +183,9 @@ struct GeneralSettings { Setting show_fps_counter{false}; Setting console_language{1}; Setting big_picture_scale{1000}; - Setting shadnet_server{""}; - Setting signaling_addr{""}; - Setting signaling_port{}; + Setting shadnet_server{"srv.shadps4.net:31313"}; + Setting shadnet_webapi_server{"http://srv.shadps4.net:31315"}; + Setting signaling_info{}; Setting enable_upnp{true}; // return a vector of override descriptors (runtime, but tiny) @@ -205,10 +205,7 @@ struct GeneralSettings { make_override("trophy_notification_side", &GeneralSettings::trophy_notification_side), make_override("connected_to_network", - &GeneralSettings::connected_to_network), - make_override("signaling_addr", &GeneralSettings::signaling_addr), - make_override("signaling_port", &GeneralSettings::signaling_port), - make_override("enable_upnp", &GeneralSettings::enable_upnp)}; + &GeneralSettings::connected_to_network)}; } }; @@ -218,8 +215,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GeneralSettings, install_dirs, addon_install_ trophy_notification_duration, show_splash, trophy_notification_side, connected_to_network, discord_rpc_enabled, show_fps_counter, console_language, - big_picture_scale, shadnet_server, signaling_addr, - signaling_port, enable_upnp) + big_picture_scale, shadnet_server, shadnet_webapi_server, + signaling_info, enable_upnp) // ------------------------------- // Log settings @@ -605,8 +602,8 @@ public: SETTING_FORWARD(m_general, ConsoleLanguage, console_language) SETTING_FORWARD(m_general, BigPictureScale, big_picture_scale) SETTING_FORWARD(m_general, ShadNetServer, shadnet_server) - SETTING_FORWARD(m_general, SignalingAddr, signaling_addr) - SETTING_FORWARD(m_general, SignalingPort, signaling_port) + SETTING_FORWARD(m_general, ShadNetWebApiServer, shadnet_webapi_server) + SETTING_FORWARD(m_general, SignalingInfo, signaling_info) SETTING_FORWARD_BOOL(m_general, UPnPEnabled, enable_upnp) // Log settings diff --git a/src/core/libraries/network/http.cpp b/src/core/libraries/network/http.cpp index 829e6c12a..4d78800a6 100644 --- a/src/core/libraries/network/http.cpp +++ b/src/core/libraries/network/http.cpp @@ -1522,6 +1522,11 @@ int PS4_SYSV_ABI sceHttpSendRequest(int reqId, const void* postData, u64 size) { plan.method = req.method; plan.method_str = req.method_str; + // np_web_api sends ORBIS_HTTP_METHOD_CUSTOM (8) with no method string as an + // out-of-band PATCH marker + if (plan.method == ORBIS_HTTP_METHOD_CUSTOM && plan.method_str.empty()) { + plan.method_str = "PATCH"; + } plan.path = ExtractPathFromUrl(req.url); plan.settings = req.settings; if (auto conn_it = g_state.connections.find(req.conn_id); @@ -2577,8 +2582,8 @@ int PS4_SYSV_ABI sceHttpGetStatusCode(int reqId, int* statusCode) { auto& req = *it->second; int wr = WaitForResponseReady(req, lock); if (wr == ORBIS_HTTP_ERROR_EAGAIN) { - LOG_DEBUG(Lib_Http, "reqId={}: response not yet ready, returning BEFORE_SEND", reqId); - return ORBIS_HTTP_ERROR_BEFORE_SEND; + LOG_DEBUG(Lib_Http, "reqId={}: response not yet ready, returning EAGAIN", reqId); + return ORBIS_HTTP_ERROR_EAGAIN; } if (wr != ORBIS_OK) { LOG_ERROR(Lib_Http, "Wait failed for reqId={}: {:#x}", reqId, static_cast(wr)); diff --git a/src/core/libraries/np/np_handler.cpp b/src/core/libraries/np/np_handler.cpp index 70abe28cf..28d05fc75 100644 --- a/src/core/libraries/np/np_handler.cpp +++ b/src/core/libraries/np/np_handler.cpp @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include "common/elf_info.h" #include "common/logging/log.h" @@ -10,7 +11,9 @@ #include "core/libraries/np/np_manager.h" #include "core/libraries/np/np_score/np_score.h" #include "core/user_settings.h" +#include "imgui/shadnet_notifications_layer.h" #include "np_handler.h" +#include "shadnet.pb.h" namespace Libraries::Np { @@ -19,16 +22,1843 @@ NpHandler& NpHandler::GetInstance() { return s_instance; } -bool NpHandler::IsPsnSignedIn(s32 user_id) const { - if (EmulatorSettings.IsShadNetEnabled()) { - return true; // fake just return true if shadnet is enabled //TODO +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 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(); + + 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->onAsyncReply = [this, user_id](ShadNet::CommandType cmd, u64 pkt_id, + ShadNet::ErrorType err, const std::vector& body) { + OnScoreReply(user_id, cmd, pkt_id, err, body); + }; + client->onLoginResult = [this, user_id](const ShadNet::LoginResult& res) { + OnLoginResult(user_id, res); + }; + + client->Start(host, port, npid, password, token); + + 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)); + 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)); + client->Stop(); + return false; + } + + LOG_INFO(NpHandler, "user_id={} signed in npid='{}' accountId={}", user_id, npid, + client->GetUserId()); + + // Build OrbisNpId + { + OrbisNpId np_id{}; + strncpy(np_id.handle.data, npid.c_str(), sizeof(np_id.handle.data) - 1); + 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::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(); + 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; } -s32 NpHandler::GetUserIdByOnlineId(const OrbisNpOnlineId& online_id) const { - UserManager& user_manager = UserManagement; - return user_manager.GetDefaultUser().user_id; // dummy return default user id for now //TODO +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{}; } -} // namespace Libraries::Np \ No newline at end of file +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; +} + +// 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)"); + } +} + +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, + "GetNpCommId: 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, "RecordScore: user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "RecordScore: 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; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "RecordScore: 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, "RecordGameData: user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "RecordGameData: 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; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "RecordGameData: 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, "GetGameData: user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "GetGameData: 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; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "GetGameData: 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, "GetGameDataByAccountId: user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "GetGameDataByAccountId: 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; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "GetGameDataByAccountId: 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, "GetBoardInfo: user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "GetBoardInfo: 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; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, "GetBoardInfo: 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, "GetRankingByNpId: 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, "GetRankingByNpId: user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "GetRankingByNpId: 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(); + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "GetRankingByNpId: 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, "GetRankingByRange: user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "GetRankingByRange: 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; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "GetRankingByRange: 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, "GetRankingByRangeA: user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "GetRankingByRangeA: 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; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "GetRankingByRangeA: 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, "GetRankingByAccountId: 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(); + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "GetRankingByAccountId: 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, "GetFriendsRanking: user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "GetFriendsRanking: 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; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "GetFriendsRanking: 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, "GetFriendsRankingA: user_id={} not connected", user_id); + return ORBIS_NP_ERROR_SIGNED_OUT; + } + client = it->second; + } + if (!client->IsAuthenticated()) { + LOG_WARNING(NpHandler, "GetFriendsRankingA: 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; + m_pending_score.emplace(pkt_id, std::move(pending)); + } + LOG_INFO(NpHandler, + "GetFriendsRankingA: 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, + "FillPlainRankArrayFromProto: 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, + "FillPlainRankArrayAFromProto: 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, + "FillPlayerRankArrayAFromProto: 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, + "FillRankArrayFromProto: 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, "OnScoreReply: 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, + "OnScoreReply: 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, "OnScoreReply: 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, "OnScoreReply: 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, "OnScoreReply: {} 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, "OnScoreReply: {} 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, + "OnScoreReply: {} truncated user_id={} pkt_id={} stored={} recv={}", + cmd_name, user_id, pkt_id, stored_size, pending.recvSize); + } + } + LOG_INFO(NpHandler, "OnScoreReply: {} 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, "OnScoreReply: 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, "OnScoreReply: 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, "OnScoreReply: 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, + "OnScoreReply: 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, "OnScoreReply: 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, "OnScoreReply: 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, "OnScoreReply: 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, + "OnScoreReply: 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, "OnScoreReply: 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, + "OnScoreReply: 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, "OnScoreReply: 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, + "OnScoreReply: 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, "OnScoreReply: {} 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, "OnScoreReply: {} 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, "OnScoreReply: {} 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, "OnScoreReply: {} 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, "OnScoreReply: 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.h b/src/core/libraries/np/np_handler.h index 2a0bd54ba..7d725d45e 100644 --- a/src/core/libraries/np/np_handler.h +++ b/src/core/libraries/np/np_handler.h @@ -3,8 +3,28 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include -#include "np_types.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/types.h" +#include "core/libraries/np/np_manager.h" +#include "core/libraries/np/np_score/np_score.h" +#include "core/libraries/np/np_score/np_score_ctx.h" +#include "core/libraries/np/np_types.h" +#include "core/libraries/rtc/rtc.h" +#include "core/libraries/system/userservice.h" +#include "shadnet/client.h" namespace Libraries::Np { @@ -15,17 +35,248 @@ public: NpHandler(const NpHandler&) = delete; NpHandler& operator=(const NpHandler&) = delete; + // Connect every currently-logged-in user that has shadNet credentials. + void Initialize(); + + // Disconnect all clients, stop worker threads, fire SignedOut for each. + void Shutdown(); + + // Connect/disconnect a single user in response to a user service login/logout event. + void OnUserLoggedIn(s32 user_id); + void OnUserLoggedOut(s32 user_id); + // True if this specific user is authenticated to the shadNet server. bool IsPsnSignedIn(s32 user_id) const; + /// True if any user is currently signed in + bool IsAnySignedIn() const; + + /// Full NP ID for this user, built once from shadnet_npid after login. + OrbisNpId GetNpId(s32 user_id) const; + + /// The Online ID embedded in the NP ID (npid.handle). + OrbisNpOnlineId GetOnlineId(s32 user_id) const; + + // Avatar URL returned by the server. + std::string GetAvatarUrl(s32 user_id) const; + + // 64-bit account ID assigned by the server. + OrbisNpAccountId GetAccountId(s32 user_id) const; + + // WebAPI bearer token + std::string GetBearerToken(s32 user_id) const; + + // Local IP address (network byte order) as seen at connect time. + u32 GetLocalIpAddr(s32 user_id) const; + + // Reverse lookup: server account_id to local user_id. + // Returns -1 if no connected user owns that account_id. + s32 GetUserIdByAccountId(u64 account_id) const; + // Reverse lookup: OrbisNpOnlineId to local user_id. // Scans m_np_ids for a matching handle.data string. // Returns -1 if no connected user has that Online ID. s32 GetUserIdByOnlineId(const OrbisNpOnlineId& online_id) const; + // Friend list + u32 GetNumFriends(s32 user_id) const; + std::optional GetFriendNpid(s32 user_id, u32 index) const; + + // ---- Friend list / requests / blocked (shadNet) ---- + struct FriendInfo { + std::string npid; + bool online = false; + }; + struct FriendListSnapshot { + std::vector friends; + std::vector requests_received; + std::vector requests_sent; + std::vector blocked; + }; + + // Local users that currently have an authenticated shadNet session. + std::vector GetConnectedUsers() const; + + // user's friend state for UI display. + FriendListSnapshot GetFriendList(s32 user_id) const; + + // Friend/block actions + s32 SendFriendRequest(s32 user_id, const std::string& npid); + s32 RemoveFriend(s32 user_id, const std::string& npid); + s32 BlockUser(s32 user_id, const std::string& npid); + s32 UnblockUser(s32 user_id, const std::string& npid); + + // Submit a RecordScore request to the shadNet server. + s32 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); + + s32 RecordGameData(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score, + const u8* data, size_t size, std::shared_ptr req); + + s32 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); + + s32 GetGameDataByAccountId(s32 user_id, s32 service_label, u32 boardId, u64 accountId, s32 pcId, + void* dataOut, u64 recvSize, u64* totalSizeOut, + std::shared_ptr req); + + s32 GetBoardInfo(s32 user_id, s32 service_label, u32 boardId, + NpScore::OrbisNpScoreBoardInfo* boardInfo, + std::shared_ptr req); + + // Submit a GetRankingByNpId request to the shadNet server. + s32 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); + + // Submit a GetRankingByRange request to the shadNet server. + s32 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); + + // A-variant of GetRankingByRange. + s32 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); + + s32 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); + + // Submit a GetFriendsRanking request to the shadNet server. + s32 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); + + // A-variant of GetFriendsRanking + s32 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); + + // State callbacks + using StateCallback = std::function; + + s32 RegisterStateCallback(StateCallback cb, void* userdata); + void UnregisterStateCallback(s32 handle); + private: NpHandler() = default; ~NpHandler() = default; + + /// Connect one user. Blocks until connected+authenticated or failed. + bool ConnectUser(s32 user_id, const std::string& host, u16 port, const std::string& npid, + const std::string& password, const std::string& token); + + // Connect a single logged-in user by id (looks up credentials, parses server). + bool ConnectUserById(s32 user_id); + + // Parse the configured shadNet server "host:port" (default port 31313). + std::pair ParseServerAddress() const; + + // Start the health-monitor worker thread if not already running (idempotent). + void StartWorker(); + + // Disconnect and remove one user's client. + void DisconnectUser(s32 user_id); + + void WorkerThread(); + // Transparent reconnect after a network drop + void MarkForReconnect(s32 user_id); + void TryReconnect(); + void FireStateCallback(s32 user_id, NpManager::OrbisNpState state); + + // Notification forwarders wired into each client + void OnFriendQuery(s32 user_id, const ShadNet::NotifyFriendQuery& n); + void OnFriendNew(s32 user_id, const ShadNet::NotifyFriendNew& n); + void OnFriendLost(s32 user_id, const ShadNet::NotifyFriendLost& n); + void OnFriendStatus(s32 user_id, const ShadNet::NotifyFriendStatus& n); + void OnLoginResult(s32 user_id, const ShadNet::LoginResult& res); + + // Async reply dispatch for score commands. Called from the per-user + // ShadNetClient on the reader thread. + void OnScoreReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error, + const std::vector& body); + + // 12-byte NP Communication ID + std::string GetNpCommId(s32 service_label) const; + + // Per-user client map + mutable std::mutex m_mutex_clients; + std::map> m_clients; + // Per-user NP ID built once from shadnet_npid after login. + std::map m_np_ids; + + // Score requests awaiting a reply, keyed by the submit packet id. + struct PendingScoreRequest { + std::shared_ptr req; + ShadNet::CommandType cmd; + std::vector requestedNpIds; + NpScore::OrbisNpScorePlayerRankData* rankArray = nullptr; + NpScore::OrbisNpScoreRankData* plainRankArray = nullptr; + NpScore::OrbisNpScoreRankDataA* aRankArray = nullptr; + NpScore::OrbisNpScorePlayerRankDataA* aPlayerRankArray = nullptr; + NpScore::OrbisNpScoreComment* commentArray = nullptr; + NpScore::OrbisNpScoreGameInfo* infoArray = nullptr; + NpScore::OrbisNpScoreBoardInfo* boardInfo = nullptr; + // GetGameData / GetGameDataByAccountId output buffers. + void* dataOut = nullptr; + u64 recvSize = 0; + u64* totalSizeOut = nullptr; + Libraries::Rtc::OrbisRtcTick* lastSortDate = nullptr; + u32* totalRecord = nullptr; + u64 arrayNum = 0; + }; + mutable std::mutex m_mutex_pending_score; + std::map m_pending_score; + + // Worker thread + std::atomic m_initialized{false}; + std::atomic m_worker_running{false}; + std::thread m_worker_thread; + + // Users dropped by a network error and awaiting transparent reconnect + struct ReconnectState { + std::chrono::steady_clock::time_point next_attempt{}; + std::chrono::milliseconds backoff{0}; + }; + std::unordered_map m_reconnect; + + // State callbacks + struct CbEntry { + s32 handle; + StateCallback cb; + void* userdata; + }; + mutable std::mutex m_mutex_cbs; + std::vector m_state_cbs; + s32 m_next_handle{1}; + + // Friend state per user, seeded from LoginReply and updated by notifications/actions. + mutable std::mutex m_mutex_friend_state; + std::map m_friend_state; }; } // namespace Libraries::Np \ No newline at end of file diff --git a/src/core/libraries/np/np_manager.cpp b/src/core/libraries/np/np_manager.cpp index c9ee76f86..efffc5ff3 100644 --- a/src/core/libraries/np/np_manager.cpp +++ b/src/core/libraries/np/np_manager.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include "common/elf_info.h" @@ -366,6 +366,39 @@ s32 PS4_SYSV_ABI sceNpAbortRequest(s32 req_id) { return ORBIS_OK; } +s32 PS4_SYSV_ABI sceNpSetTimeout(s32 req_id, s32 resolve_retry, u32 resolve_timeout, + u32 conn_timeout, u32 send_timeout, u32 recv_timeout) { + LOG_DEBUG(Lib_NpManager, "called req_id = {:#x}", req_id); + + if (req_id <= 0) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + + // ORBIS_NP_TIMEOUT_NO_EFFECT (0) is allowed per field (use default) but not for every field. + const auto unset_or_at_least = [](u32 v, u32 min_us) { return v == 0 || v >= min_us; }; + const bool all_zero = resolve_retry == 0 && resolve_timeout == 0 && conn_timeout == 0 && + send_timeout == 0 && recv_timeout == 0; + if (all_zero || resolve_retry < 0 || !unset_or_at_least(resolve_timeout, 1'000'000) || + !unset_or_at_least(conn_timeout, 10'000'000) || + !unset_or_at_least(send_timeout, 10'000'000) || + !unset_or_at_least(recv_timeout, 10'000'000)) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + + std::scoped_lock lk{g_request_mutex}; + + s32 req_index = req_id - ORBIS_NP_MANAGER_REQUEST_ID_OFFSET - 1; + if (g_active_requests == 0 || req_index < 0 || + req_index >= static_cast(g_requests.size()) || + g_requests[req_index].state == NpRequestState::None) { + return ORBIS_NP_ERROR_REQUEST_NOT_FOUND; + } + + // shadNet performs no real network I/O, so the timeouts are validated and accepted but + // not applied. + return ORBIS_OK; +} + s32 PS4_SYSV_ABI sceNpWaitAsync(s32 req_id, s32* result) { if (result == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; @@ -514,7 +547,8 @@ s32 PS4_SYSV_ABI sceNpGetGamePresenceStatus(OrbisNpOnlineId* online_id, s32 PS4_SYSV_ABI sceNpGetGamePresenceStatusA(Libraries::UserService::OrbisUserServiceUserId user_id, OrbisNpGamePresenseStatus* game_status) { - if (game_status == nullptr) { + if (game_status == nullptr || + user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } *game_status = @@ -529,65 +563,78 @@ s32 PS4_SYSV_ABI sceNpGetAccountId(OrbisNpOnlineId* online_id, u64* account_id) if (online_id == nullptr || account_id == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - if (!g_shadnet_enabled) { + const s32 user_id = Libraries::Np::NpHandler::GetInstance().GetUserIdByOnlineId(*online_id); + if (user_id == -1) { + *account_id = 0; + return ORBIS_NP_ERROR_USER_NOT_FOUND; + } + if (!g_shadnet_enabled || !Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id)) { *account_id = 0; return ORBIS_NP_ERROR_SIGNED_OUT; } - *account_id = 0xFEEDFACE; + *account_id = Libraries::Np::NpHandler::GetInstance().GetAccountId(user_id); return ORBIS_OK; } s32 PS4_SYSV_ABI sceNpGetAccountIdA(Libraries::UserService::OrbisUserServiceUserId user_id, u64* account_id) { LOG_DEBUG(Lib_NpManager, "user_id {}", user_id); - if (account_id == nullptr) { + if (account_id == nullptr || + user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - if (UserManagement.GetUserByID(user_id) == nullptr) { - *account_id = 0; - return ORBIS_NP_ERROR_USER_NOT_FOUND; - } - if (!g_shadnet_enabled) { + if (!g_shadnet_enabled || !Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id)) { *account_id = 0; return ORBIS_NP_ERROR_SIGNED_OUT; } - *account_id = 0xFEEDFACE; + *account_id = Libraries::Np::NpHandler::GetInstance().GetAccountId(user_id); return ORBIS_OK; } s32 PS4_SYSV_ABI sceNpGetNpId(Libraries::UserService::OrbisUserServiceUserId user_id, OrbisNpId* np_id) { LOG_DEBUG(Lib_NpManager, "user_id {}", user_id); + if (user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { + return (g_firmware_version >= 0 && g_firmware_version < Common::ElfInfo::FW_900) + ? ORBIS_NP_ERROR_USER_NOT_FOUND + : ORBIS_NP_ERROR_INVALID_ARGUMENT; + } if (np_id == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - const auto* user = UserManagement.GetUserByID(user_id); - if (user == nullptr) { - return ORBIS_NP_ERROR_USER_NOT_FOUND; - } - if (!g_shadnet_enabled) { + if (!g_shadnet_enabled || !Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id)) { + LOG_WARNING(Lib_NpManager, + "sceNpGetNpId: SIGNED_OUT (user_id={} shadnet_enabled={} signed_in={})", + user_id, g_shadnet_enabled, + Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id)); return ORBIS_NP_ERROR_SIGNED_OUT; } - memset(np_id, 0, sizeof(OrbisNpId)); - strncpy(np_id->handle.data, user->user_name.c_str(), sizeof(np_id->handle.data) - 1); + *np_id = Libraries::Np::NpHandler::GetInstance().GetNpId(user_id); + LOG_INFO(Lib_NpManager, "sceNpGetNpId: user_id={} handle.data='{}' (strnlen={})", user_id, + np_id->handle.data, strnlen(np_id->handle.data, sizeof(np_id->handle.data))); return ORBIS_OK; } s32 PS4_SYSV_ABI sceNpGetOnlineId(Libraries::UserService::OrbisUserServiceUserId user_id, OrbisNpOnlineId* online_id) { LOG_DEBUG(Lib_NpManager, "user_id {}", user_id); + if (user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { + return (g_firmware_version >= 0 && g_firmware_version < Common::ElfInfo::FW_900) + ? ORBIS_NP_ERROR_USER_NOT_FOUND + : ORBIS_NP_ERROR_INVALID_ARGUMENT; + } if (online_id == nullptr) { + LOG_ERROR(Lib_NpManager, "invalid argument: online_id is null"); return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - const auto* user = UserManagement.GetUserByID(user_id); - if (user == nullptr) { - return ORBIS_NP_ERROR_USER_NOT_FOUND; - } - if (!g_shadnet_enabled) { + if (!g_shadnet_enabled || !Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id)) { + LOG_INFO(Lib_NpManager, + "sceNpGetOnlineId: SIGNED_OUT (user_id={} shadnet_enabled={} signed_in={})", + user_id, g_shadnet_enabled, + Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id)); return ORBIS_NP_ERROR_SIGNED_OUT; } - memset(online_id, 0, sizeof(OrbisNpOnlineId)); - strncpy(online_id->data, user->user_name.c_str(), sizeof(online_id->data) - 1); + *online_id = Libraries::Np::NpHandler::GetInstance().GetOnlineId(user_id); return ORBIS_OK; } @@ -596,10 +643,6 @@ s32 PS4_SYSV_ABI sceNpGetNpReachabilityState(Libraries::UserService::OrbisUserSe if (state == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - if (UserManagement.GetUserByID(user_id) == nullptr) { - return ORBIS_NP_ERROR_USER_NOT_FOUND; - } - if (user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { if (g_firmware_version < 0 || g_firmware_version >= Common::ElfInfo::FW_400) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; @@ -616,9 +659,6 @@ s32 PS4_SYSV_ABI sceNpGetState(Libraries::UserService::OrbisUserServiceUserId us if (state == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - if (UserManagement.GetUserByID(user_id) == nullptr) { - return ORBIS_NP_ERROR_USER_NOT_FOUND; - } if (user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { if (g_firmware_version < 0 || g_firmware_version >= Common::ElfInfo::FW_900) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; @@ -639,14 +679,47 @@ s32 PS4_SYSV_ABI sceNpGetState(Libraries::UserService::OrbisUserServiceUserId us s32 PS4_SYSV_ABI sceNpGetUserIdByAccountId(u64 account_id, Libraries::UserService::OrbisUserServiceUserId* user_id) { - if (user_id == nullptr) { + if (account_id == 0 || user_id == nullptr) { + LOG_ERROR(Lib_NpManager, "invalid argument: account_id={}", account_id); return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - if (!g_shadnet_enabled) { + if (!g_shadnet_enabled) return ORBIS_NP_ERROR_SIGNED_OUT; + + const s32 found = Libraries::Np::NpHandler::GetInstance().GetUserIdByAccountId(account_id); + if (found == -1) + return ORBIS_NP_ERROR_SIGNED_OUT; + + // Verify the resolved user_id is actually a logged-in local user + const User* u = UserManagement.GetUserByID(found); + if (!u || !u->logged_in) + return ORBIS_NP_ERROR_USER_NOT_FOUND; + + *user_id = found; + LOG_DEBUG(Lib_NpManager, "account_id={} returns user_id={}", account_id, *user_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpGetUserIdByOnlineId(const OrbisNpOnlineId* online_id, + Libraries::UserService::OrbisUserServiceUserId* user_id) { + if (online_id == nullptr || user_id == nullptr) { + LOG_ERROR(Lib_NpManager, "invalid argument"); + return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - *user_id = 1000; - LOG_DEBUG(Lib_NpManager, "userid({}) = {}", account_id, *user_id); + if (!g_shadnet_enabled) + return ORBIS_NP_ERROR_SIGNED_OUT; + + const s32 found = Libraries::Np::NpHandler::GetInstance().GetUserIdByOnlineId(*online_id); + if (found == -1) + return ORBIS_NP_ERROR_SIGNED_OUT; + + // Verify the resolved user_id is actually a logged-in local user + const User* u = UserManagement.GetUserByID(found); + if (!u || !u->logged_in) + return ORBIS_NP_ERROR_USER_NOT_FOUND; + + *user_id = found; + LOG_DEBUG(Lib_NpManager, "returns user_id={}", *user_id); return ORBIS_OK; } @@ -657,15 +730,15 @@ s32 PS4_SYSV_ABI sceNpHasSignedUp(Libraries::UserService::OrbisUserServiceUserId return ORBIS_NP_ERROR_INVALID_ARGUMENT; } *has_signed_up = false; - const User* u = UserManagement.GetUserByID(user_id); - if (u == nullptr) { - return ORBIS_NP_ERROR_USER_NOT_FOUND; - } if (user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { if (g_firmware_version < 0 || g_firmware_version >= Common::ElfInfo::FW_900) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } } + const User* u = UserManagement.GetUserByID(user_id); + if (u == nullptr) { + return ORBIS_NP_ERROR_USER_NOT_FOUND; + } // A user has signed up if they have a shadNet npid configured. // This is independent of shadnet_enabled and current connection state. *has_signed_up = !u->shadnet_npid.empty(); @@ -721,6 +794,18 @@ struct NpStateCallbackAEntry { static std::array g_np_state_callbacks{}; +struct NpReachabilityStateCallback { + OrbisNpReachabilityStateCallback func; + void* userdata; +}; + +NpReachabilityStateCallback NpReachabilityCb; + +// Last reachability state delivered to NpReachabilityCb, per user. Keeps the reachability +// callback edge-triggered: it fires only when the derived state changes for that user. +static std::map + g_np_reachability_last; + struct PendingNpStateEvent { Libraries::UserService::OrbisUserServiceUserId user_id; OrbisNpState state; @@ -732,18 +817,13 @@ static std::deque g_np_state_events; static void QueueNpStateEvent(Libraries::UserService::OrbisUserServiceUserId user_id, OrbisNpState state) { - const auto* user = UserManagement.GetUserByID(user_id); - if (user == nullptr) { - return; - } - PendingNpStateEvent event{}; event.user_id = user_id; event.state = state; event.has_np_id = state == OrbisNpState::SignedIn; if (event.has_np_id) { - std::strncpy(event.np_id.handle.data, user->user_name.c_str(), - sizeof(event.np_id.handle.data) - 1); + // NpId is built from the user's shadnet_npid at login; GetNpId returns by value. + event.np_id = Libraries::Np::NpHandler::GetInstance().GetNpId(user_id); } std::scoped_lock lk{g_np_state_events_mutex}; @@ -754,11 +834,20 @@ void NotifyNpStateFromUserServiceEvent(Libraries::UserService::OrbisUserServiceE Libraries::UserService::OrbisUserServiceUserId user_id) { switch (event_type) { case Libraries::UserService::OrbisUserServiceEventType::Login: - QueueNpStateEvent(user_id, - g_shadnet_enabled ? OrbisNpState::SignedIn : OrbisNpState::SignedOut); + if (g_shadnet_enabled) { + // Handler connects the user and fires SignedIn via the bridge on success. + Libraries::Np::NpHandler::GetInstance().OnUserLoggedIn(user_id); + } else { + QueueNpStateEvent(user_id, OrbisNpState::SignedOut); + } break; case Libraries::UserService::OrbisUserServiceEventType::Logout: - QueueNpStateEvent(user_id, OrbisNpState::SignedOut); + if (g_shadnet_enabled) { + // Handler disconnects the user and fires SignedOut via the bridge. + Libraries::Np::NpHandler::GetInstance().OnUserLoggedOut(user_id); + } else { + QueueNpStateEvent(user_id, OrbisNpState::SignedOut); + } break; default: break; @@ -814,7 +903,11 @@ static s32 UnregisterStateCallbackAById(s32 callback_id) { static void DispatchPendingNpStateCallbacks() { std::deque pending_events; LegacyNpStateCallback legacy_callback{}; + NpStateCallbackForNpToolkit toolkit_callback{}; + NpReachabilityStateCallback reachability_callback{}; std::array callbacks; + std::vector> + reachability_changes; { std::scoped_lock lk{g_np_state_events_mutex, g_np_state_callbacks_mutex}; if (g_np_state_events.empty()) { @@ -823,6 +916,21 @@ static void DispatchPendingNpStateCallbacks() { pending_events.swap(g_np_state_events); legacy_callback = LegacyNpStateCb; callbacks = g_np_state_callbacks; + toolkit_callback = NpStateCbForNp; + reachability_callback = NpReachabilityCb; + + if (reachability_callback.func != nullptr) { + for (const auto& event : pending_events) { + const OrbisNpReachabilityState reach = event.state == OrbisNpState::SignedIn + ? OrbisNpReachabilityState::Reachable + : OrbisNpReachabilityState::Unavailable; + auto it = g_np_reachability_last.find(event.user_id); + if (it == g_np_reachability_last.end() || it->second != reach) { + g_np_reachability_last[event.user_id] = reach; + reachability_changes.emplace_back(event.user_id, reach); + } + } + } } for (auto& event : pending_events) { @@ -842,22 +950,25 @@ static void DispatchPendingNpStateCallbacks() { } } - if (NpStateCbForNp.func != nullptr) { - NpStateCbForNp.func(event.user_id, event.state, NpStateCbForNp.userdata); + if (toolkit_callback.func != nullptr) { + toolkit_callback.func(event.user_id, event.state, toolkit_callback.userdata); } } + + // Reachability callback fires only on a change, after the state callbacks. + for (const auto& [user_id, reach] : reachability_changes) { + reachability_callback.func(user_id, reach, reachability_callback.userdata); + } } s32 PS4_SYSV_ABI sceNpCheckCallback() { - LOG_DEBUG(Lib_NpManager, "(STUBBED) called"); + LOG_DEBUG(Lib_NpManager, "called"); DispatchPendingNpStateCallbacks(); std::scoped_lock lk{g_np_callbacks_mutex}; - - for (auto i : g_np_callbacks) { - (i.second)(); + for (auto& [key, cb] : g_np_callbacks) { + cb(); } - return ORBIS_OK; } @@ -883,18 +994,13 @@ s32 PS4_SYSV_ABI sceNpRegisterStateCallback(OrbisNpStateCallback callback, void* return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpUnregisterStateCallback(s32 callback_id) { - LOG_INFO(Lib_NpManager, "called, callback_id = {}", callback_id); - if (callback_id != 0) { - return ORBIS_NP_ERROR_INVALID_ARGUMENT; - } - +s32 PS4_SYSV_ABI sceNpUnregisterStateCallback() { std::scoped_lock lk{g_np_state_callbacks_mutex}; if (LegacyNpStateCb.func == nullptr) { return ORBIS_NP_ERROR_CALLBACK_NOT_REGISTERED; } - - LegacyNpStateCb = {}; + LegacyNpStateCb.func = nullptr; + LegacyNpStateCb.userdata = nullptr; return ORBIS_OK; } @@ -908,59 +1014,191 @@ s32 PS4_SYSV_ABI sceNpUnregisterStateCallbackA(s32 callback_id) { return UnregisterStateCallbackAById(callback_id); } -struct NpReachabilityStateCallback { - OrbisNpReachabilityStateCallback func; - void* userdata; -}; - -NpReachabilityStateCallback NpReachabilityCb; - s32 PS4_SYSV_ABI sceNpRegisterNpReachabilityStateCallback(OrbisNpReachabilityStateCallback callback, void* userdata) { if (callback == nullptr) { + LOG_ERROR(Lib_NpManager, "callback is nullptr"); return ORBIS_NP_ERROR_INVALID_ARGUMENT; } + + std::scoped_lock lk{g_np_state_callbacks_mutex}; if (NpReachabilityCb.func != nullptr) { + LOG_ERROR(Lib_NpManager, "callback already registered, cannot register multiple"); return ORBIS_NP_ERROR_CALLBACK_ALREADY_REGISTERED; } - LOG_INFO(Lib_NpManager, "called"); NpReachabilityCb.func = callback; NpReachabilityCb.userdata = userdata; + // Reset the per-user cache so the next state transition reports fresh. + g_np_reachability_last.clear(); return ORBIS_OK; } s32 PS4_SYSV_ABI sceNpUnregisterNpReachabilityStateCallback() { + std::scoped_lock lk{g_np_state_callbacks_mutex}; if (NpReachabilityCb.func == nullptr) { + LOG_ERROR(Lib_NpManager, "callback not registered"); return ORBIS_NP_ERROR_CALLBACK_NOT_REGISTERED; } - - NpReachabilityCb = {}; + NpReachabilityCb.func = nullptr; + NpReachabilityCb.userdata = nullptr; + g_np_reachability_last.clear(); return ORBIS_OK; } s32 PS4_SYSV_ABI sceNpRegisterStateCallbackForToolkit(OrbisNpStateCallbackForNpToolkit callback, void* userdata) { - static s32 id = 0; LOG_ERROR(Lib_NpManager, "(STUBBED) called"); + std::scoped_lock lk{g_np_state_callbacks_mutex}; NpStateCbForNp.func = callback; NpStateCbForNp.userdata = userdata; - return id; + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpUnregisterStateCallbackForToolkit() { + std::scoped_lock lk{g_np_state_callbacks_mutex}; + if (NpStateCbForNp.func == nullptr) { + return ORBIS_NP_ERROR_CALLBACK_NOT_REGISTERED; + } + NpStateCbForNp.func = nullptr; + NpStateCbForNp.userdata = nullptr; + return ORBIS_OK; +} + +// shadNet has no live game-presence or PS Plus event source, so registered callbacks are +// stored but never invoked +using OrbisNpGamePresenceCallback = PS4_SYSV_ABI void (*)(const OrbisNpOnlineId* online_id, + void* userdata); +using OrbisNpPlusEventCallback = PS4_SYSV_ABI void (*)( + Libraries::UserService::OrbisUserServiceUserId user_id, s32 event, void* userdata); + +static std::mutex g_presence_plus_mutex; + +static OrbisNpGamePresenceCallback g_game_presence_cb = nullptr; +static void* g_game_presence_cb_userdata = nullptr; + +struct GamePresenceCallbackEntry { + s32 handle; + OrbisNpGamePresenceCallback func; + void* userdata; + bool in_use; +}; +static std::array g_game_presence_cbs_a{}; +static s32 g_game_presence_next_handle = 0; + +static OrbisNpPlusEventCallback g_plus_event_cb = nullptr; +static void* g_plus_event_cb_userdata = nullptr; + +s32 PS4_SYSV_ABI sceNpSetGamePresenceOnline(s32 req_id, const OrbisNpOnlineId* online_id, + u32 presence) { + if (req_id == 0 || online_id == nullptr) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + + std::scoped_lock lk{g_request_mutex}; + s32 req_index = req_id - ORBIS_NP_MANAGER_REQUEST_ID_OFFSET - 1; + if (g_active_requests == 0 || req_index < 0 || + req_index >= static_cast(g_requests.size()) || + g_requests[req_index].state == NpRequestState::None) { + return ORBIS_NP_ERROR_REQUEST_NOT_FOUND; + } + + // No live presence service under shadNet; accept and report success. + LOG_DEBUG(Lib_NpManager, "(STUBBED) called req_id = {:#x}", req_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpSetGamePresenceOnlineA(s32 req_id, + Libraries::UserService::OrbisUserServiceUserId user_id, + u32 presence) { + if (user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + const OrbisNpOnlineId online_id = Libraries::Np::NpHandler::GetInstance().GetOnlineId(user_id); + return sceNpSetGamePresenceOnline(req_id, &online_id, presence); +} + +void PS4_SYSV_ABI sceNpRegisterGamePresenceCallback(OrbisNpGamePresenceCallback callback, + void* userdata) { + std::scoped_lock lk{g_presence_plus_mutex}; + g_game_presence_cb = callback; + g_game_presence_cb_userdata = userdata; +} + +s32 PS4_SYSV_ABI sceNpRegisterGamePresenceCallbackA(OrbisNpGamePresenceCallback callback, + void* userdata) { + if (callback == nullptr) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + + std::scoped_lock lk{g_presence_plus_mutex}; + for (auto& entry : g_game_presence_cbs_a) { + if (entry.in_use) { + continue; + } + entry.handle = ++g_game_presence_next_handle; + entry.func = callback; + entry.userdata = userdata; + entry.in_use = true; + return entry.handle; + } + return ORBIS_NP_ERROR_CALLBACK_MAX; +} + +s32 PS4_SYSV_ABI sceNpUnregisterGamePresenceCallbackA(s32 callback_id) { + std::scoped_lock lk{g_presence_plus_mutex}; + for (auto& entry : g_game_presence_cbs_a) { + if (entry.in_use && entry.handle == callback_id) { + entry = {}; + return ORBIS_OK; + } + } + return ORBIS_NP_ERROR_CALLBACK_NOT_REGISTERED; +} + +s32 PS4_SYSV_ABI sceNpIsPlusMember(Libraries::UserService::OrbisUserServiceUserId user_id, + bool* result) { + if (result == nullptr) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + // shadNet has no PS Plus concept; report not a Plus member. + *result = false; + LOG_DEBUG(Lib_NpManager, "user_id {} not a Plus member", user_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpRegisterPlusEventCallback(OrbisNpPlusEventCallback callback, void* userdata) { + if (callback == nullptr) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + std::scoped_lock lk{g_presence_plus_mutex}; + if (g_plus_event_cb != nullptr) { + return ORBIS_NP_ERROR_CALLBACK_ALREADY_REGISTERED; + } + g_plus_event_cb = callback; + g_plus_event_cb_userdata = userdata; + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpUnregisterPlusEventCallback() { + std::scoped_lock lk{g_presence_plus_mutex}; + if (g_plus_event_cb == nullptr) { + return ORBIS_NP_ERROR_CALLBACK_NOT_REGISTERED; + } + g_plus_event_cb = nullptr; + g_plus_event_cb_userdata = nullptr; + return ORBIS_OK; } void RegisterNpCallback(std::string key, std::function cb) { std::scoped_lock lk{g_np_callbacks_mutex}; - LOG_DEBUG(Lib_NpManager, "registering callback processing for {}", key); - g_np_callbacks.emplace(key, cb); } void DeregisterNpCallback(std::string key) { std::scoped_lock lk{g_np_callbacks_mutex}; - LOG_DEBUG(Lib_NpManager, "deregistering callback processing for {}", key); - g_np_callbacks.erase(key); } @@ -969,6 +1207,16 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { "Failed to get compiled SDK version."); g_shadnet_enabled = EmulatorSettings.IsShadNetEnabled(); + // Route live NP state changes from NpHandler into the dispatch queue so they are + // delivered on the game's thread during sceNpCheckCallback (single delivery path). + // Registered before Initialize() so the initial SignedIn events are captured. + Libraries::Np::NpHandler::GetInstance().RegisterStateCallback( + [](Libraries::UserService::OrbisUserServiceUserId user_id, OrbisNpState state) { + QueueNpStateEvent(user_id, state); + }, + nullptr); + Libraries::Np::NpHandler::GetInstance().Initialize(); + LIB_FUNCTION("GpLQDNKICac", "libSceNpManager", 1, "libSceNpManager", sceNpCreateRequest); LIB_FUNCTION("eiqMCt9UshI", "libSceNpManager", 1, "libSceNpManager", sceNpCreateAsyncRequest); LIB_FUNCTION("2rsFmlGWleQ", "libSceNpManager", 1, "libSceNpManager", sceNpCheckNpAvailability); @@ -982,6 +1230,7 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("m9L3O6yst-U", "libSceNpManager", 1, "libSceNpManager", sceNpGetParentalControlInfoA); LIB_FUNCTION("OzKvTvg3ZYU", "libSceNpManager", 1, "libSceNpManager", sceNpAbortRequest); + LIB_FUNCTION("-QglDeRr8D8", "libSceNpManager", 1, "libSceNpManager", sceNpSetTimeout); LIB_FUNCTION("jyi5p9XWUSs", "libSceNpManager", 1, "libSceNpManager", sceNpWaitAsync); LIB_FUNCTION("uqcPJLWL08M", "libSceNpManager", 1, "libSceNpManager", sceNpPollAsync); LIB_FUNCTION("S7QTn72PrDw", "libSceNpManager", 1, "libSceNpManager", sceNpDeleteRequest); @@ -996,6 +1245,21 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { sceNpGetGamePresenceStatus); LIB_FUNCTION("oPO9U42YpgI", "libSceNpManager", 1, "libSceNpManager", sceNpGetGamePresenceStatusA); + LIB_FUNCTION("KO+11cgC7N0", "libSceNpManager", 1, "libSceNpManager", + sceNpSetGamePresenceOnline); + LIB_FUNCTION("C0gNCiRIi4U", "libSceNpManager", 1, "libSceNpManager", + sceNpSetGamePresenceOnlineA); + LIB_FUNCTION("uFJpaKNBAj4", "libSceNpManager", 1, "libSceNpManager", + sceNpRegisterGamePresenceCallback); + LIB_FUNCTION("KswxLxk4c1Y", "libSceNpManager", 1, "libSceNpManager", + sceNpRegisterGamePresenceCallbackA); + LIB_FUNCTION("aJZyCcHxzu4", "libSceNpManager", 1, "libSceNpManager", + sceNpUnregisterGamePresenceCallbackA); + LIB_FUNCTION("Ybu6AxV6S0o", "libSceNpManager", 1, "libSceNpManager", sceNpIsPlusMember); + LIB_FUNCTION("GImICnh+boA", "libSceNpManager", 1, "libSceNpManager", + sceNpRegisterPlusEventCallback); + LIB_FUNCTION("xViqJdDgKl0", "libSceNpManager", 1, "libSceNpManager", + sceNpUnregisterPlusEventCallback); LIB_FUNCTION("e-ZuhGEoeC4", "libSceNpManager", 1, "libSceNpManager", sceNpGetNpReachabilityState); @@ -1005,11 +1269,11 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("XDncXQIJUSk", "libSceNpManager", 1, "libSceNpManager", sceNpGetOnlineId); LIB_FUNCTION("eQH7nWPcAgc", "libSceNpManager", 1, "libSceNpManager", sceNpGetState); LIB_FUNCTION("VgYczPGB5ss", "libSceNpManager", 1, "libSceNpManager", sceNpGetUserIdByAccountId); - LIB_FUNCTION("Oad3rvY-NJQ", "libSceNpManager", 1, "libSceNpManager", sceNpHasSignedUp); + LIB_FUNCTION("F6E4ycq9Dbg", "libSceNpManager", 1, "libSceNpManager", sceNpGetUserIdByOnlineId); LIB_FUNCTION("A2CQ3kgSopQ", "libSceNpManager", 1, "libSceNpManager", sceNpSetContentRestriction); LIB_FUNCTION("Ec63y59l9tw", "libSceNpManager", 1, "libSceNpManager", sceNpSetNpTitleId); - + LIB_FUNCTION("Oad3rvY-NJQ", "libSceNpManager", 1, "libSceNpManager", sceNpHasSignedUp); LIB_FUNCTION("3Zl8BePTh9Y", "libSceNpManager", 1, "libSceNpManager", sceNpCheckCallback); LIB_FUNCTION("JELHf4xPufo", "libSceNpManager", 1, "libSceNpManager", sceNpCheckCallbackForLib); LIB_FUNCTION("VfRSmPmj8Q8", "libSceNpManager", 1, "libSceNpManager", @@ -1028,6 +1292,8 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { sceNpCheckCallbackForLib); LIB_FUNCTION("0c7HbXRKUt4", "libSceNpManagerForToolkit", 1, "libSceNpManager", sceNpRegisterStateCallbackForToolkit); + LIB_FUNCTION("YIvqqvJyjEc", "libSceNpManagerForToolkit", 1, "libSceNpManager", + sceNpUnregisterStateCallbackForToolkit); LIB_FUNCTION("2rsFmlGWleQ", "libSceNpManagerCompat", 1, "libSceNpManager", sceNpCheckNpAvailability); diff --git a/src/core/libraries/np/np_score/np_score.cpp b/src/core/libraries/np/np_score/np_score.cpp index af47b8993..405e9a092 100644 --- a/src/core/libraries/np/np_score/np_score.cpp +++ b/src/core/libraries/np/np_score/np_score.cpp @@ -1,14 +1,20 @@ // SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include -#include -#include +#include +#include +#include +#include "common/elf_info.h" #include "common/logging/log.h" #include "core/libraries/error_codes.h" +#include "core/libraries/kernel/process.h" #include "core/libraries/libs.h" -#include "np_score.h" -#include "np_score_ctx.h" +#include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_handler.h" +#include "core/libraries/np/np_score/np_score.h" +#include "core/libraries/np/np_score/np_score_ctx.h" namespace Libraries::Np::NpScore { @@ -174,40 +180,678 @@ s32 PS4_SYSV_ABI sceNpScoreAbortRequest(s32 reqId) { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreCensorComment(s32 reqId, const char* comment, void* option) { - LOG_ERROR(Lib_NpScore, "(STUBBED) called reqId={}, comment={}, option={}", reqId, - comment ? comment : "null", PTR(option)); +//*********************************** +// Async functions +//*********************************** +s32 PS4_SYSV_ABI sceNpScorePollAsync(s32 reqId, s32* result) { + // return 0 = async op completed; *result holds its final return value + // return 1 = async op still in flight + // return <0 = this poll call itself failed (e.g. invalid reqId) + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + } + if (!req) { + LOG_ERROR(Lib_NpScore, "PollAsync invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + std::lock_guard rlock(req->mutex); + if (!req->result.has_value()) { + return 1; // still running + } + if (result != nullptr) { + *result = *req->result; + } + LOG_INFO(Lib_NpScore, "PollAsync reqId={} completed (result={:#x})", reqId, *req->result); + return 0; +} + +s32 PS4_SYSV_ABI sceNpScoreWaitAsync(s32 reqId, s32* result) { + // Block until the async op finishes. Shape mirrors PollAsync but waits on + // the request's cv instead of bailing out if result isn't set yet. + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + } + if (!req) { + LOG_ERROR(Lib_NpScore, "WaitAsync invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + std::unique_lock rlock(req->mutex); + req->cv.wait(rlock, [&] { return req->result.has_value(); }); + if (result != nullptr) { + *result = *req->result; + } + LOG_INFO(Lib_NpScore, "WaitAsync reqId={} completed (result={:#x})", reqId, *req->result); + return 0; +} + +//*********************************** +// Record Score functions +//*********************************** +s32 PS4_SYSV_ABI sceNpScoreRecordScore(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpScoreValue score, + const OrbisNpScoreComment* scoreComment, + const OrbisNpScoreGameInfo* gameInfo, + OrbisNpScoreRankNumber* tmpRank, + const Rtc::OrbisRtcTick* compareDate, void* option) { + LOG_INFO(Lib_NpScore, + "reqId={} boardId={} score={} scoreComment={} gameInfo={} tmpRank={} " + "compareDate={} option={}", + reqId, boardId, score, PTR(scoreComment), PTR(gameInfo), PTR(tmpRank), + PTR(compareDate), PTR(option)); + + if (option != nullptr) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (gameInfo != nullptr && gameInfo->infoSize > ORBIS_NP_SCORE_GAMEINFO_MAXSIZE) { + LOG_ERROR(Lib_NpScore, "gameInfo->infoSize {} exceeds max {}", gameInfo->infoSize, + ORBIS_NP_SCORE_GAMEINFO_MAXSIZE); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + // Stash the caller's tmpRank pointer so the reply handler can write to it. + req->tmpRankOut = tmpRank; + + const char* commentBytes = (scoreComment != nullptr) ? scoreComment->utf8Comment : nullptr; + const size_t commentLen = (scoreComment != nullptr) ? strnlen(scoreComment->utf8Comment, + ORBIS_NP_SCORE_COMMENT_MAXLEN) + : 0; + const u8* gameInfoBytes = (gameInfo != nullptr) ? gameInfo->data : nullptr; + const size_t gameInfoLen = (gameInfo != nullptr) ? gameInfo->infoSize : 0; + + const s32 dispatch_err = NpHandler::GetInstance().RecordScore( + req->userId, ServiceLabelForRequest(req), boardId, req->pcId, score, commentBytes, + commentLen, gameInfoBytes, gameInfoLen, req); + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + return req->Wait(); +} + +s32 PS4_SYSV_ABI sceNpScoreRecordScoreAsync(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpScoreValue score, + const OrbisNpScoreComment* scoreComment, + const OrbisNpScoreGameInfo* gameInfo, + OrbisNpScoreRankNumber* tmpRank, + const Rtc::OrbisRtcTick* compareDate, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, score={}, " + "scoreComment={}, gameInfo={}, tmpRank={}, compareDate={}, option={}", + reqId, boardId, score, PTR(scoreComment), PTR(gameInfo), PTR(tmpRank), + PTR(compareDate), PTR(option)); + if (option != nullptr) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (gameInfo != nullptr && gameInfo->infoSize > ORBIS_NP_SCORE_GAMEINFO_MAXSIZE) { + LOG_ERROR(Lib_NpScore, "gameInfo->infoSize {} exceeds max {}", gameInfo->infoSize, + ORBIS_NP_SCORE_GAMEINFO_MAXSIZE); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + // Stash the caller's tmpRank pointer so the reply handler can write to it. + req->tmpRankOut = tmpRank; + + const char* commentBytes = (scoreComment != nullptr) ? scoreComment->utf8Comment : nullptr; + const size_t commentLen = (scoreComment != nullptr) ? strnlen(scoreComment->utf8Comment, + ORBIS_NP_SCORE_COMMENT_MAXLEN) + : 0; + const u8* gameInfoBytes = (gameInfo != nullptr) ? gameInfo->data : nullptr; + const size_t gameInfoLen = (gameInfo != nullptr) ? gameInfo->infoSize : 0; + + const s32 dispatch_err = NpHandler::GetInstance().RecordScore( + req->userId, ServiceLabelForRequest(req), boardId, req->pcId, score, commentBytes, + commentLen, gameInfoBytes, gameInfoLen, req); + if (dispatch_err != ORBIS_OK) { + // Dispatch failed synchronously (user signed out, not authed, etc.). + // Complete the request with the error so PollAsync/WaitAsync observe it. + req->SetResult(dispatch_err); + return dispatch_err; + } return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreCensorCommentAsync(s32 reqId, const char* comment, void* option) { - LOG_ERROR(Lib_NpScore, "(STUBBED) called reqId={}, comment={}, option={}", reqId, - comment ? comment : "null", PTR(option)); - return ORBIS_OK; +//*********************************** +// RankingByRage functions +//*********************************** +static int GetRankingByRangeImpl(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpScoreRankNumber startSerialRank, + OrbisNpScoreRankData* rankArray, + OrbisNpScoreRankDataA* aRankArrayIn, u64 rankArraySize, + OrbisNpScoreComment* commentArray, u64 commentArraySize, + OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, void* option, bool is_async) { + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "option argument is not supported"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (arrayNum > ORBIS_NP_SCORE_MAX_RANGE_NUM_PER_REQUEST) { + LOG_ERROR(Lib_NpScore, "arrayNum {} exceeds max {}", arrayNum, + ORBIS_NP_SCORE_MAX_RANGE_NUM_PER_REQUEST); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (startSerialRank == 0) { + LOG_ERROR(Lib_NpScore, "startSerialRank must be 1 or higher"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + const bool is_a_variant = (aRankArrayIn != nullptr); + if (is_a_variant == (rankArray != nullptr)) { + LOG_ERROR(Lib_NpScore, + "GetRankingByRangeImpl: exactly one of rankArray ({}) and aRankArray ({}) " + "must be non-null", + PTR(rankArray), PTR(aRankArrayIn)); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + if (arrayNum == 0) { + LOG_ERROR(Lib_NpScore, "arrayNum must be > 0"); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + + const u64 expected_size = + arrayNum * (is_a_variant ? sizeof(OrbisNpScoreRankDataA) : sizeof(OrbisNpScoreRankData)); + if (rankArraySize != expected_size) { + LOG_ERROR(Lib_NpScore, + "Invalid alignment for rankArray (got {}, expected {} for {} variant)", + rankArraySize, expected_size, is_a_variant ? "A" : "non-A"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + + // Zero whichever output buffer the caller supplied. + if (is_a_variant) { + std::memset(aRankArrayIn, 0, rankArraySize); + } else { + std::memset(rankArray, 0, rankArraySize); + } + if (commentArray != nullptr) { + if (commentArraySize != arrayNum * sizeof(OrbisNpScoreComment)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment for commentArray"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(commentArray, 0, commentArraySize); + } + if (infoArray != nullptr) { + if (infoArraySize != arrayNum * sizeof(OrbisNpScoreGameInfo)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment for infoArray"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(infoArray, 0, infoArraySize); + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + s32 dispatch_err; + if (is_a_variant) { + dispatch_err = NpHandler::GetInstance().GetRankingByRangeA( + req->userId, ServiceLabelForRequest(req), boardId, startSerialRank, + static_cast(arrayNum), aRankArrayIn, commentArray, infoArray, lastSortDate, + totalRecord, req); + } else { + dispatch_err = NpHandler::GetInstance().GetRankingByRange( + req->userId, ServiceLabelForRequest(req), boardId, startSerialRank, + static_cast(arrayNum), rankArray, commentArray, infoArray, lastSortDate, + totalRecord, req); + } + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + if (is_async) { + return ORBIS_OK; + } + return req->Wait(); +} +s32 PS4_SYSV_ABI sceNpScoreGetRankingByRange( + s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreRankNumber startSerialRank, + OrbisNpScoreRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, + u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_INFO(Lib_NpScore, "called reqId={}, boardId={}, startSerialRank={}, arrayNum={}", reqId, + boardId, startSerialRank, arrayNum); + return GetRankingByRangeImpl(reqId, boardId, startSerialRank, rankArray, + /*aRankArrayIn=*/nullptr, rankArraySize, commentArray, + commentArraySize, infoArray, infoArraySize, arrayNum, lastSortDate, + totalRecord, option, false); } -s32 PS4_SYSV_ABI sceNpScoreChangeModeForOtherSaveDataOwners() { - LOG_ERROR(Lib_NpScore, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpScoreGetRankingByRangeAsync( + s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreRankNumber startSerialRank, + OrbisNpScoreRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, + u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, startSerialRank={}, " + "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " + "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", + reqId, boardId, startSerialRank, PTR(rankArray), rankArraySize, PTR(commentArray), + commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), + PTR(totalRecord), PTR(option)); + return GetRankingByRangeImpl(reqId, boardId, startSerialRank, rankArray, + /*aRankArrayIn=*/nullptr, rankArraySize, commentArray, + commentArraySize, infoArray, infoArraySize, arrayNum, lastSortDate, + totalRecord, option, true); } -s32 PS4_SYSV_ABI sceNpScoreCreateTitleCtx() { - LOG_ERROR(Lib_NpScore, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpScoreGetRankingByRangeA( + s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreRankNumber startSerialRank, + OrbisNpScoreRankDataA* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, + u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, startSerialRank={}, " + "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " + "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", + reqId, boardId, startSerialRank, PTR(rankArray), rankArraySize, PTR(commentArray), + commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), + PTR(totalRecord), PTR(option)); + return GetRankingByRangeImpl(reqId, boardId, startSerialRank, /*rankArray=*/nullptr, rankArray, + rankArraySize, commentArray, commentArraySize, infoArray, + infoArraySize, arrayNum, lastSortDate, totalRecord, option, false); } -s32 PS4_SYSV_ABI sceNpScoreGetBoardInfo(s32 reqId, OrbisNpScoreBoardId boardId, - OrbisNpScoreBoardInfo* boardInfo, void* option) { - LOG_ERROR(Lib_NpScore, "(STUBBED) called reqId={}, boardId={}, boardInfo={}, option={}", reqId, - boardId, PTR(boardInfo), PTR(option)); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpScoreGetRankingByRangeAAsync( + s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreRankNumber startSerialRank, + OrbisNpScoreRankDataA* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, + u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, startSerialRank={}, " + "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " + "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", + reqId, boardId, startSerialRank, PTR(rankArray), rankArraySize, PTR(commentArray), + commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), + PTR(totalRecord), PTR(option)); + return GetRankingByRangeImpl(reqId, boardId, startSerialRank, /*rankArray=*/nullptr, rankArray, + rankArraySize, commentArray, commentArraySize, infoArray, + infoArraySize, arrayNum, lastSortDate, totalRecord, option, true); } -s32 PS4_SYSV_ABI sceNpScoreGetBoardInfoAsync(s32 reqId, OrbisNpScoreBoardId boardId, - OrbisNpScoreBoardInfo* boardInfo, void* option) { - LOG_ERROR(Lib_NpScore, "(STUBBED) called reqId={}, boardId={}, boardInfo={}, option={}", reqId, - boardId, PTR(boardInfo), PTR(option)); - return ORBIS_OK; +//*********************************** +// Ranking by NpId functions +//*********************************** +static int GetRankingByNpIdImpl(s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpId* npIdArray, + u64 npIdArraySize, OrbisNpScorePlayerRankData* rankArray, + u64 rankArraySize, OrbisNpScoreComment* commentArray, + u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, + u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, void* option, bool is_async) { + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "Invalid argument"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (arrayNum > ORBIS_NP_SCORE_MAX_NPID_NUM_PER_REQUEST) { + LOG_ERROR(Lib_NpScore, "Too many npIds requested: {}", arrayNum); + return ORBIS_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + } + if (npIdArray == nullptr || rankArray == nullptr) { + LOG_ERROR(Lib_NpScore, "insufficient argument: npIdArray={}, rankArray={}", PTR(npIdArray), + PTR(rankArray)); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + if (npIdArraySize != arrayNum * sizeof(OrbisNpId) || + rankArraySize != arrayNum * sizeof(OrbisNpScorePlayerRankData)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment: npIdArraySize={}, rankArraySize={}", + npIdArraySize, rankArraySize); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(rankArray, 0, rankArraySize); + if (commentArray != nullptr) { + if (commentArraySize != arrayNum * sizeof(OrbisNpScoreComment)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment: commentArraySize={}", commentArraySize); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(commentArray, 0, commentArraySize); + } + if (infoArray != nullptr) { + if (infoArraySize != arrayNum * sizeof(OrbisNpScoreGameInfo)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment: infoArraySize={}", infoArraySize); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(infoArray, 0, infoArraySize); + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + std::vector npIds; + npIds.reserve(arrayNum); + for (u64 i = 0; i < arrayNum; ++i) { + const char* data = npIdArray[i].handle.data; + const size_t len = strnlen(data, sizeof(npIdArray[i].handle.data)); + npIds.emplace_back(data, len); + } + + const s32 dispatch_err = NpHandler::GetInstance().GetRankingByNpId( + req->userId, ServiceLabelForRequest(req), boardId, npIds, std::vector{}, rankArray, + commentArray, infoArray, lastSortDate, totalRecord, req); + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + if (is_async) { + return ORBIS_OK; + } + return req->Wait(); +} + +s32 PS4_SYSV_ABI sceNpScoreGetRankingByNpId( + s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpId* npIdArray, u64 npIdArraySize, + OrbisNpScorePlayerRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, + u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, npIdArray={}, npIdArraySize={}, " + "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, " + "infoArray={}, infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, " + "option={}", + reqId, boardId, PTR(npIdArray), npIdArraySize, PTR(rankArray), rankArraySize, + PTR(commentArray), commentArraySize, PTR(infoArray), infoArraySize, arrayNum, + PTR(lastSortDate), PTR(totalRecord), PTR(option)); + return GetRankingByNpIdImpl(reqId, boardId, npIdArray, npIdArraySize, rankArray, rankArraySize, + commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, + lastSortDate, totalRecord, option, false); +} + +s32 PS4_SYSV_ABI sceNpScoreGetRankingByNpIdAsync( + s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpId* npIdArray, u64 npIdArraySize, + OrbisNpScorePlayerRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, + u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_INFO(Lib_NpScore, "called reqId={}, boardId={}, arrayNum={}", reqId, boardId, arrayNum); + return GetRankingByNpIdImpl(reqId, boardId, npIdArray, npIdArraySize, rankArray, rankArraySize, + commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, + lastSortDate, totalRecord, option, true); +} + +//*********************************** +// Ranking by AccountId functions +//*********************************** +static int GetRankingByAccountIdImpl(s32 reqId, OrbisNpScoreBoardId boardId, + const OrbisNpAccountId* accountIdArray, u64 accountIdArraySize, + OrbisNpScorePlayerRankDataA* rankArray, u64 rankArraySize, + OrbisNpScoreComment* commentArray, u64 commentArraySize, + OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, + u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, void* option, + bool is_async) { + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "Invalid argument"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (arrayNum > ORBIS_NP_SCORE_MAX_ID_NUM_PER_REQUEST) { + LOG_ERROR(Lib_NpScore, "Too many accountIds requested: {}", arrayNum); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (arrayNum == 0 || rankArray == nullptr || accountIdArray == nullptr) { + LOG_ERROR(Lib_NpScore, + "Insufficient arguments: arrayNum={}, rankArray={}, accountIdArray={}", arrayNum, + PTR(rankArray), PTR(accountIdArray)); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + if (accountIdArraySize != arrayNum * sizeof(OrbisNpAccountId)) { + LOG_ERROR(Lib_NpScore, + "Invalid alignment for accountIdArray: size {} does not match expected {}", + accountIdArraySize, arrayNum * sizeof(OrbisNpAccountId)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + if (rankArraySize != arrayNum * sizeof(OrbisNpScorePlayerRankDataA)) { + LOG_ERROR(Lib_NpScore, + "Invalid alignment for rankArray: size {} does not match expected {}", + rankArraySize, arrayNum * sizeof(OrbisNpScorePlayerRankDataA)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(rankArray, 0, rankArraySize); + if (commentArray != nullptr) { + if (commentArraySize != arrayNum * sizeof(OrbisNpScoreComment)) { + LOG_ERROR(Lib_NpScore, + "Invalid alignment for commentArray: size {} does not match expected {}", + commentArraySize, arrayNum * sizeof(OrbisNpScoreComment)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(commentArray, 0, commentArraySize); + } + if (infoArray != nullptr) { + if (infoArraySize != arrayNum * sizeof(OrbisNpScoreGameInfo)) { + LOG_ERROR(Lib_NpScore, + "Invalid alignment for infoArray: size {} does not match expected {}", + infoArraySize, arrayNum * sizeof(OrbisNpScoreGameInfo)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(infoArray, 0, infoArraySize); + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + if (IsRequestAborted(req)) + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + + std::vector accountIds; + accountIds.reserve(arrayNum); + for (u64 i = 0; i < arrayNum; ++i) { + accountIds.push_back(static_cast(accountIdArray[i])); + } + + const s32 dispatch_err = NpHandler::GetInstance().GetRankingByAccountId( + req->userId, ServiceLabelForRequest(req), boardId, accountIds, + /*pcIds=*/{}, rankArray, commentArray, infoArray, lastSortDate, totalRecord, req); + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + if (is_async) + return ORBIS_OK; + return req->Wait(); +} + +s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountId( + s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpAccountId* accountIdArray, + u64 accountIdArraySize, OrbisNpScorePlayerRankDataA* rankArray, u64 rankArraySize, + OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, + u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, accountIdArray={}, " + "accountIdArraySize={}, rankArray={}, rankArraySize={}, commentArray={}, " + "commentArraySize={}, infoArray={}, infoArraySize={}, arrayNum={}, lastSortDate={}, " + "totalRecord={}, option={}", + reqId, boardId, PTR(accountIdArray), accountIdArraySize, PTR(rankArray), rankArraySize, + PTR(commentArray), commentArraySize, PTR(infoArray), infoArraySize, arrayNum, + PTR(lastSortDate), PTR(totalRecord), PTR(option)); + return GetRankingByAccountIdImpl(reqId, boardId, accountIdArray, accountIdArraySize, rankArray, + rankArraySize, commentArray, commentArraySize, infoArray, + infoArraySize, arrayNum, lastSortDate, totalRecord, option, + false); +} + +s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdAsync( + s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpAccountId* accountIdArray, + u64 accountIdArraySize, OrbisNpScorePlayerRankDataA* rankArray, u64 rankArraySize, + OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, + u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, " + "accountIdArray={}, accountIdArraySize={}, rankArray={}, rankArraySize={}, " + "commentArray={}, commentArraySize={}, infoArray={}, infoArraySize={}, arrayNum={}, " + "lastSortDate={}, totalRecord={}, option={}", + reqId, boardId, PTR(accountIdArray), accountIdArraySize, PTR(rankArray), rankArraySize, + PTR(commentArray), commentArraySize, PTR(infoArray), infoArraySize, arrayNum, + PTR(lastSortDate), PTR(totalRecord), PTR(option)); + LOG_INFO(Lib_NpScore, + "reqId={} boardId={} accountIdArray={} accountIdArraySize={} arrayNum={} option={}", + reqId, boardId, PTR(accountIdArray), accountIdArraySize, arrayNum, PTR(option)); + return GetRankingByAccountIdImpl(reqId, boardId, accountIdArray, accountIdArraySize, rankArray, + rankArraySize, commentArray, commentArraySize, infoArray, + infoArraySize, arrayNum, lastSortDate, totalRecord, option, + true); +} +//*********************************** +// Friends Ranking functions +//*********************************** +static int GetFriendsRankingImpl(s32 reqId, OrbisNpScoreBoardId boardId, s32 includeSelf, + OrbisNpScoreRankData* rankArray, + OrbisNpScoreRankDataA* aRankArrayIn, u64 rankArraySize, + OrbisNpScoreComment* commentArray, u64 commentArraySize, + OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, void* option, bool is_async) { + const bool is_a_variant = (aRankArrayIn != nullptr); + if (is_a_variant == (rankArray != nullptr)) { + LOG_ERROR(Lib_NpScore, + "GetFriendsRankingImpl: exactly one of rankArray ({}) and aRankArray ({}) " + "must be non-null", + PTR(rankArray), PTR(aRankArrayIn)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + const u64 expected_size = + arrayNum * (is_a_variant ? sizeof(OrbisNpScoreRankDataA) : sizeof(OrbisNpScoreRankData)); + if (rankArraySize != expected_size) { + LOG_ERROR(Lib_NpScore, + "Invalid alignment for rankArray (got {}, expected {} for {} variant)", + rankArraySize, expected_size, is_a_variant ? "A" : "non-A"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + auto* opt = static_cast(option); + u32 startSerialRank = 1; + if (includeSelf == 0) { + if (opt != nullptr) { + LOG_ERROR(Lib_NpScore, "includeSelf is 0 but option struct is not null"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + } else if (opt != nullptr) { + if (opt->size != sizeof(OrbisNpScoreGetFriendRankingOptParam)) { + LOG_ERROR(Lib_NpScore, "Invalid size for option struct: {}", opt->size); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (opt->startSerialRank != nullptr) { + startSerialRank = *opt->startSerialRank; + } + } + if (arrayNum > ORBIS_NP_SCORE_MAX_RANGE_NUM_PER_REQUEST) { + LOG_ERROR(Lib_NpScore, "arrayNum {} exceeds max {}", arrayNum, + ORBIS_NP_SCORE_MAX_RANGE_NUM_PER_REQUEST); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (startSerialRank == 0) { + LOG_ERROR(Lib_NpScore, "startSerialRank must be 1 or higher"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (arrayNum == 0) { + LOG_ERROR(Lib_NpScore, "arrayNum must be greater than 0"); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + + if (is_a_variant) { + std::memset(aRankArrayIn, 0, rankArraySize); + } else { + std::memset(rankArray, 0, rankArraySize); + } + if (commentArray != nullptr) { + if (commentArraySize != arrayNum * sizeof(OrbisNpScoreComment)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment for commentArray"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(commentArray, 0, commentArraySize); + } + if (infoArray != nullptr) { + if (infoArraySize != arrayNum * sizeof(OrbisNpScoreGameInfo)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment for infoArray"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(infoArray, 0, infoArraySize); + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + s32 dispatch_err; + if (is_a_variant) { + dispatch_err = NpHandler::GetInstance().GetFriendsRankingA( + req->userId, ServiceLabelForRequest(req), boardId, includeSelf != 0, + static_cast(arrayNum), aRankArrayIn, commentArray, infoArray, lastSortDate, + totalRecord, req); + } else { + // todo support startSerialRank and opt->hits , currently we return the full friend set + // capped at max in score order without rank-slice semantics + dispatch_err = NpHandler::GetInstance().GetFriendsRanking( + req->userId, ServiceLabelForRequest(req), boardId, includeSelf != 0, + static_cast(arrayNum), rankArray, commentArray, infoArray, lastSortDate, + totalRecord, req); + } + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + if (is_async) { + return ORBIS_OK; + } + return req->Wait(); } s32 PS4_SYSV_ABI sceNpScoreGetFriendsRanking(s32 reqId, OrbisNpScoreBoardId boardId, @@ -217,59 +861,16 @@ s32 PS4_SYSV_ABI sceNpScoreGetFriendsRanking(s32 reqId, OrbisNpScoreBoardId boar u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, includeSelf={}, " - "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " - "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", - reqId, boardId, includeSelf, PTR(rankArray), rankArraySize, PTR(commentArray), - commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), - PTR(totalRecord), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByRange( - s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreRankNumber startSerialRank, - OrbisNpScoreRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, - u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, - Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, "called reqId={}, boardId={}, startSerialRank={}, arrayNum={}", reqId, - boardId, startSerialRank, arrayNum); - - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetFriendsRankingA(s32 reqId, OrbisNpScoreBoardId boardId, - s32 includeSelf, OrbisNpScoreRankDataA* rankArray, - u64 rankArraySize, OrbisNpScoreComment* commentArray, - u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, - u64 infoArraySize, u64 arrayNum, - Rtc::OrbisRtcTick* lastSortDate, - OrbisNpScoreRankNumber* totalRecord, - OrbisNpScoreGetFriendRankingOptParam* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, includeSelf={}, " - "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " - "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", - reqId, boardId, includeSelf, PTR(rankArray), rankArraySize, PTR(commentArray), - commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), - PTR(totalRecord), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetFriendsRankingAAsync( - s32 reqId, OrbisNpScoreBoardId boardId, s32 includeSelf, OrbisNpScoreRankDataA* rankArray, - u64 rankArraySize, OrbisNpScoreComment* commentArray, u64 commentArraySize, - OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, - Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, - OrbisNpScoreGetFriendRankingOptParam* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, includeSelf={}, " - "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " - "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", - reqId, boardId, includeSelf, PTR(rankArray), rankArraySize, PTR(commentArray), - commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), - PTR(totalRecord), PTR(option)); - return ORBIS_OK; + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, includeSelf={}, " + "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " + "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", + reqId, boardId, includeSelf, PTR(rankArray), rankArraySize, PTR(commentArray), + commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), + PTR(totalRecord), PTR(option)); + return GetFriendsRankingImpl(reqId, boardId, includeSelf, rankArray, /*aRankArrayIn=*/nullptr, + rankArraySize, commentArray, commentArraySize, infoArray, + infoArraySize, arrayNum, lastSortDate, totalRecord, option, false); } s32 PS4_SYSV_ABI sceNpScoreGetFriendsRankingAsync( @@ -278,11 +879,718 @@ s32 PS4_SYSV_ABI sceNpScoreGetFriendsRankingAsync( OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, OrbisNpScoreGetFriendRankingOptParam* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, includeSelf={}, " + "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " + "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", + reqId, boardId, includeSelf, PTR(rankArray), rankArraySize, PTR(commentArray), + commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), + PTR(totalRecord), PTR(option)); + return GetFriendsRankingImpl(reqId, boardId, includeSelf, rankArray, /*aRankArrayIn=*/nullptr, + rankArraySize, commentArray, commentArraySize, infoArray, + infoArraySize, arrayNum, lastSortDate, totalRecord, option, true); +} +s32 PS4_SYSV_ABI sceNpScoreGetFriendsRankingA(s32 reqId, OrbisNpScoreBoardId boardId, + s32 includeSelf, OrbisNpScoreRankDataA* rankArray, + u64 rankArraySize, OrbisNpScoreComment* commentArray, + u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, + u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, + OrbisNpScoreGetFriendRankingOptParam* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, includeSelf={}, " + "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " + "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", + reqId, boardId, includeSelf, PTR(rankArray), rankArraySize, PTR(commentArray), + commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), + PTR(totalRecord), PTR(option)); + return GetFriendsRankingImpl(reqId, boardId, includeSelf, /*rankArray=*/nullptr, rankArray, + rankArraySize, commentArray, commentArraySize, infoArray, + infoArraySize, arrayNum, lastSortDate, totalRecord, option, false); +} + +s32 PS4_SYSV_ABI sceNpScoreGetFriendsRankingAAsync( + s32 reqId, OrbisNpScoreBoardId boardId, s32 includeSelf, OrbisNpScoreRankDataA* rankArray, + u64 rankArraySize, OrbisNpScoreComment* commentArray, u64 commentArraySize, + OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, + OrbisNpScoreGetFriendRankingOptParam* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, includeSelf={}, " + "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " + "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", + reqId, boardId, includeSelf, PTR(rankArray), rankArraySize, PTR(commentArray), + commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), + PTR(totalRecord), PTR(option)); + return GetFriendsRankingImpl(reqId, boardId, includeSelf, /*rankArray=*/nullptr, rankArray, + rankArraySize, commentArray, commentArraySize, infoArray, + infoArraySize, arrayNum, lastSortDate, totalRecord, option, true); +} +//*********************************** +// Ranking ByNpIdPcId functions functions +//*********************************** +static int GetRankingByNpIdPcIdImpl(s32 reqId, OrbisNpScoreBoardId boardId, + const OrbisNpScoreNpIdPcId* idArray, u64 idArraySize, + OrbisNpScorePlayerRankData* rankArray, u64 rankArraySize, + OrbisNpScoreComment* commentArray, u64 commentArraySize, + OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, + u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, void* option, + bool is_async) { + + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "Invalid argument: option must be NULL"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + if (idArray == nullptr || arrayNum == 0 || rankArray == nullptr) { + LOG_ERROR(Lib_NpScore, "insufficient argument: idArray={}, arrayNum={}, rankArray={}", + PTR(idArray), arrayNum, PTR(rankArray)); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + if (arrayNum > ORBIS_NP_SCORE_MAX_NPID_NUM_PER_REQUEST) { + LOG_ERROR(Lib_NpScore, "Too many npIds requested: {}", arrayNum); + return ORBIS_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + } + if (idArraySize != arrayNum * sizeof(OrbisNpScoreNpIdPcId) || + rankArraySize != arrayNum * sizeof(OrbisNpScorePlayerRankData)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment: idArraySize={}, rankArraySize={}", idArraySize, + rankArraySize); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(rankArray, 0, rankArraySize); + if (commentArray != nullptr) { + if (commentArraySize != arrayNum * sizeof(OrbisNpScoreComment)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment: commentArraySize={}", commentArraySize); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(commentArray, 0, commentArraySize); + } + if (infoArray != nullptr) { + if (infoArraySize != arrayNum * sizeof(OrbisNpScoreGameInfo)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment: infoArraySize={}", infoArraySize); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(infoArray, 0, infoArraySize); + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + std::vector npIds; + std::vector pcIds; + npIds.reserve(arrayNum); + pcIds.reserve(arrayNum); + for (u64 i = 0; i < arrayNum; ++i) { + const char* data = idArray[i].npId.handle.data; + const size_t len = strnlen(data, sizeof(idArray[i].npId.handle.data)); + npIds.emplace_back(data, len); + pcIds.push_back(idArray[i].pcId); + } + + const s32 dispatch_err = NpHandler::GetInstance().GetRankingByNpId( + req->userId, ServiceLabelForRequest(req), boardId, npIds, pcIds, rankArray, commentArray, + infoArray, lastSortDate, totalRecord, req); + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + if (is_async) { + return ORBIS_OK; + } + return req->Wait(); +} + +s32 PS4_SYSV_ABI sceNpScoreGetRankingByNpIdPcId( + s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpScoreNpIdPcId* idArray, u64 idArraySize, + OrbisNpScorePlayerRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, + u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_ERROR(Lib_NpScore, + "called reqId={}, boardId={}, idArray={}, idArraySize={}, " + "rankArray={}, rankArraySize={}, arrayNum={}", + reqId, boardId, PTR(idArray), idArraySize, PTR(rankArray), rankArraySize, arrayNum); + return GetRankingByNpIdPcIdImpl(reqId, boardId, idArray, idArraySize, rankArray, rankArraySize, + commentArray, commentArraySize, infoArray, infoArraySize, + arrayNum, lastSortDate, totalRecord, option, false); +} + +s32 PS4_SYSV_ABI sceNpScoreGetRankingByNpIdPcIdAsync( + s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpScoreNpIdPcId* idArray, u64 idArraySize, + OrbisNpScorePlayerRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, + u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, + Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_ERROR(Lib_NpScore, "called reqId={}, boardId={}, arrayNum={}", reqId, boardId, arrayNum); + return GetRankingByNpIdPcIdImpl(reqId, boardId, idArray, idArraySize, rankArray, rankArraySize, + commentArray, commentArraySize, infoArray, infoArraySize, + arrayNum, lastSortDate, totalRecord, option, true); +} + +//*********************************** +// Ranking ByAccountIdPcId functions +//*********************************** +static int GetRankingByAccountIdPcIdImpl(s32 reqId, OrbisNpScoreBoardId boardId, + const OrbisNpScoreAccountIdPcId* idArray, u64 idArraySize, + OrbisNpScorePlayerRankDataA* rankArray, u64 rankArraySize, + OrbisNpScoreComment* commentArray, u64 commentArraySize, + OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, + u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, void* option, + bool is_async) { + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "Invalid argument: option must be NULL"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (arrayNum > ORBIS_NP_SCORE_MAX_ID_NUM_PER_REQUEST) { + LOG_ERROR(Lib_NpScore, "Too many accountIds requested: {}", arrayNum); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (arrayNum == 0 || rankArray == nullptr || idArray == nullptr) { + LOG_ERROR(Lib_NpScore, "Insufficient arguments: arrayNum={}, rankArray={}, idArray={}", + arrayNum, PTR(rankArray), PTR(idArray)); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + if (idArraySize != arrayNum * sizeof(OrbisNpScoreAccountIdPcId)) { + LOG_ERROR(Lib_NpScore, "Invalid alignment for idArray: size {} does not match expected {}", + idArraySize, arrayNum * sizeof(OrbisNpScoreAccountIdPcId)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + if (rankArraySize != arrayNum * sizeof(OrbisNpScorePlayerRankDataA)) { + LOG_ERROR(Lib_NpScore, + "Invalid alignment for rankArray: size {} does not match expected {}", + rankArraySize, arrayNum * sizeof(OrbisNpScorePlayerRankDataA)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(rankArray, 0, rankArraySize); + if (commentArray != nullptr) { + if (commentArraySize != arrayNum * sizeof(OrbisNpScoreComment)) { + LOG_ERROR(Lib_NpScore, + "Invalid alignment for commentArray: size {} does not match expected {}", + commentArraySize, arrayNum * sizeof(OrbisNpScoreComment)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(commentArray, 0, commentArraySize); + } + if (infoArray != nullptr) { + if (infoArraySize != arrayNum * sizeof(OrbisNpScoreGameInfo)) { + LOG_ERROR(Lib_NpScore, + "Invalid alignment for infoArray: size {} does not match expected {}", + infoArraySize, arrayNum * sizeof(OrbisNpScoreGameInfo)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + std::memset(infoArray, 0, infoArraySize); + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + if (IsRequestAborted(req)) + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + + std::vector accountIds; + std::vector pcIds; + accountIds.reserve(arrayNum); + pcIds.reserve(arrayNum); + for (u64 i = 0; i < arrayNum; ++i) { + accountIds.push_back(static_cast(idArray[i].accountId)); + pcIds.push_back(idArray[i].pcId); + } + + const s32 dispatch_err = NpHandler::GetInstance().GetRankingByAccountId( + req->userId, ServiceLabelForRequest(req), boardId, accountIds, pcIds, rankArray, + commentArray, infoArray, lastSortDate, totalRecord, req); + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + if (is_async) + return ORBIS_OK; + return req->Wait(); +} + +s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdPcId( + s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpScoreAccountIdPcId* idArray, + u64 idArraySize, OrbisNpScorePlayerRankDataA* rankArray, u64 rankArraySize, + OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, + u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_INFO( + Lib_NpScore, + "called reqId={}, boardId={}, idArray={}, " + "idArraySize={}, rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, " + "infoArray={}, infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", + reqId, boardId, PTR(idArray), idArraySize, PTR(rankArray), rankArraySize, PTR(commentArray), + commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), + PTR(totalRecord), PTR(option)); + return GetRankingByAccountIdPcIdImpl(reqId, boardId, idArray, idArraySize, rankArray, + rankArraySize, commentArray, commentArraySize, infoArray, + infoArraySize, arrayNum, lastSortDate, totalRecord, option, + false); +} + +s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdPcIdAsync( + s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpScoreAccountIdPcId* idArray, + u64 idArraySize, OrbisNpScorePlayerRankDataA* rankArray, u64 rankArraySize, + OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, + u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, + OrbisNpScoreRankNumber* totalRecord, void* option) { + LOG_INFO( + Lib_NpScore, + "called reqId={}, boardId={}, idArray={}, " + "idArraySize={}, rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, " + "infoArray={}, infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", + reqId, boardId, PTR(idArray), idArraySize, PTR(rankArray), rankArraySize, PTR(commentArray), + commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), + PTR(totalRecord), PTR(option)); + return GetRankingByAccountIdPcIdImpl(reqId, boardId, idArray, idArraySize, rankArray, + rankArraySize, commentArray, commentArraySize, infoArray, + infoArraySize, arrayNum, lastSortDate, totalRecord, option, + true); +} +//*********************************** +// BoardInfo functions +//*********************************** +static int GetBoardInfoImpl(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpScoreBoardInfo* boardInfo, void* option, bool is_async) { + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "GetBoardInfo: option must be null, got {}", PTR(option)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (g_firmware_version >= 0 && g_firmware_version >= Common::ElfInfo::FW_250 && + boardInfo == nullptr) { + LOG_ERROR(Lib_NpScore, "GetBoardInfo: boardInfo must be non-null"); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + if (boardInfo == nullptr) { + LOG_INFO(Lib_NpScore, "GetBoardInfo: legacy SDK with null boardInfo — no-op success"); + return ORBIS_OK; + } + + std::memset(boardInfo, 0, sizeof(OrbisNpScoreBoardInfo)); + + const s32 dispatch_err = NpHandler::GetInstance().GetBoardInfo( + req->userId, ServiceLabelForRequest(req), boardId, boardInfo, req); + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + if (is_async) { + return ORBIS_OK; + } + return req->Wait(); +} + +s32 PS4_SYSV_ABI sceNpScoreGetBoardInfo(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpScoreBoardInfo* boardInfo, void* option) { + LOG_INFO(Lib_NpScore, "called reqId={}, boardId={}, boardInfo={}, option={}", reqId, boardId, + PTR(boardInfo), PTR(option)); + return GetBoardInfoImpl(reqId, boardId, boardInfo, option, false); +} + +s32 PS4_SYSV_ABI sceNpScoreGetBoardInfoAsync(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpScoreBoardInfo* boardInfo, void* option) { + LOG_INFO(Lib_NpScore, "called reqId={}, boardId={}, boardInfo={}, option={}", reqId, boardId, + PTR(boardInfo), PTR(option)); + return GetBoardInfoImpl(reqId, boardId, boardInfo, option, true); +} + +//*********************************** +// GetGameData functions +//*********************************** +static int GetGameDataImpl(s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpId* npId, + u64* totalSize, u64 recvSize, void* data, void* option, bool is_async) { + if (npId == nullptr) { + LOG_ERROR(Lib_NpScore, "GetGameData: npId must be non-null"); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "GetGameData: option must be null, got {}", PTR(option)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + const std::string npIdStr(npId->handle.data, + strnlen(npId->handle.data, sizeof(npId->handle.data))); + if (npIdStr.empty()) { + LOG_ERROR(Lib_NpScore, "GetGameData: npId.handle.data is empty"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + const s32 pcId = req->pcId; + + const s32 dispatch_err = + NpHandler::GetInstance().GetGameData(req->userId, ServiceLabelForRequest(req), boardId, + npIdStr, pcId, data, recvSize, totalSize, req); + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + if (is_async) { + return ORBIS_OK; + } + return req->Wait(); +} + +static int GetGameDataByAccountIdImpl(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpAccountId accountId, u64* totalSize, u64 recvSize, + void* data, void* option, bool is_async) { + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "GetGameDataByAccountId: option must be null, got {}", PTR(option)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (accountId == 0) { + LOG_ERROR(Lib_NpScore, "GetGameDataByAccountId: accountId must be non-zero"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + const s32 pcId = req->pcId; + + const s32 dispatch_err = NpHandler::GetInstance().GetGameDataByAccountId( + req->userId, ServiceLabelForRequest(req), boardId, accountId, pcId, data, recvSize, + totalSize, req); + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + if (is_async) { + return ORBIS_OK; + } + return req->Wait(); +} + +s32 PS4_SYSV_ABI sceNpScoreGetGameData(s32 reqId, OrbisNpScoreBoardId boardId, + const OrbisNpId* npId, u64* totalSize, u64 recvSize, + void* data, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, npId={}, totalSize={}, recvSize={}, data={}, option={}", + reqId, boardId, PTR(npId), PTR(totalSize), recvSize, PTR(data), PTR(option)); + return GetGameDataImpl(reqId, boardId, npId, totalSize, recvSize, data, option, false); +} + +s32 PS4_SYSV_ABI sceNpScoreGetGameDataAsync(s32 reqId, OrbisNpScoreBoardId boardId, + const OrbisNpId* npId, u64* totalSize, u64 recvSize, + void* data, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, npId={}, totalSize={}, recvSize={}, data={}, option={}", + reqId, boardId, PTR(npId), PTR(totalSize), recvSize, PTR(data), PTR(option)); + return GetGameDataImpl(reqId, boardId, npId, totalSize, recvSize, data, option, true); +} + +s32 PS4_SYSV_ABI sceNpScoreGetGameDataByAccountId(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpAccountId accountId, u64* totalSize, + u64 recvSize, void* data, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, accountId={}, totalSize={}, recvSize={}, data={}, " + "option={}", + reqId, boardId, accountId, PTR(totalSize), recvSize, PTR(data), PTR(option)); + return GetGameDataByAccountIdImpl(reqId, boardId, accountId, totalSize, recvSize, data, option, + false); +} + +s32 PS4_SYSV_ABI sceNpScoreGetGameDataByAccountIdAsync(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpAccountId accountId, u64* totalSize, + u64 recvSize, void* data, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, accountId={}, totalSize={}, recvSize={}, data={}, " + "option={}", + reqId, boardId, accountId, PTR(totalSize), recvSize, PTR(data), PTR(option)); + return GetGameDataByAccountIdImpl(reqId, boardId, accountId, totalSize, recvSize, data, option, + true); +} + +//*********************************** +// Record Game Data functions +//*********************************** +static int RecordGameDataImpl(s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreValue score, + u64 totalSize, u64 sendSize, const void* data, void* option, + bool is_async) { + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "RecordGameData: option must be null, got {}", PTR(option)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (data == nullptr) { + LOG_ERROR(Lib_NpScore, "RecordGameData: data must be non-null"); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + if (sendSize == 0) { + LOG_ERROR(Lib_NpScore, "RecordGameData: sendSize must be > 0"); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (totalSize > sendSize) { + // Chunked send not supported on the shadnet wire — the server + // stores whatever bytes arrive in this single command as the + // complete blob. Warn so callers can spot the truncation. + LOG_WARNING(Lib_NpScore, + "RecordGameData: chunked send not supported (totalSize={} sendSize={}); " + "storing first chunk only", + totalSize, sendSize); + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + const s32 pcId = req->pcId; + + const s32 dispatch_err = NpHandler::GetInstance().RecordGameData( + req->userId, ServiceLabelForRequest(req), boardId, pcId, score, + static_cast(data), static_cast(sendSize), req); + if (dispatch_err != ORBIS_OK) { + req->SetResult(dispatch_err); + return dispatch_err; + } + if (is_async) { + return ORBIS_OK; + } + return req->Wait(); +} + +s32 PS4_SYSV_ABI sceNpScoreRecordGameData(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpScoreValue score, u64 totalSize, u64 sendSize, + const void* data, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, score={}, totalSize={}, sendSize={}, data={}, option={}", + reqId, boardId, score, totalSize, sendSize, PTR(data), PTR(option)); + return RecordGameDataImpl(reqId, boardId, score, totalSize, sendSize, data, option, false); +} + +s32 PS4_SYSV_ABI sceNpScoreRecordGameDataAsync(s32 reqId, OrbisNpScoreBoardId boardId, + OrbisNpScoreValue score, u64 totalSize, u64 sendSize, + const void* data, void* option) { + LOG_INFO(Lib_NpScore, + "called reqId={}, boardId={}, score={}, totalSize={}, sendSize={}, data={}, option={}", + reqId, boardId, score, totalSize, sendSize, PTR(data), PTR(option)); + return RecordGameDataImpl(reqId, boardId, score, totalSize, sendSize, data, option, true); +} +//*********************************** +// Censor functions +//*********************************** +static int CensorCommentImpl(s32 reqId, const char* comment, void* option, bool is_async) { + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "CensorComment: option must be null, got {}", PTR(option)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (comment == nullptr) { + LOG_ERROR(Lib_NpScore, "CensorComment: comment must be non-null"); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + constexpr size_t probe = ORBIS_NP_SCORE_CENSOR_COMMENT_MAXLEN + 1; + const size_t len = strnlen(comment, probe); + if (len > ORBIS_NP_SCORE_CENSOR_COMMENT_MAXLEN) { + LOG_ERROR(Lib_NpScore, "CensorComment: comment too long (max {} chars + null)", + ORBIS_NP_SCORE_CENSOR_COMMENT_MAXLEN); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + // Passthrough + LOG_INFO(Lib_NpScore, "CensorComment: reqId={} len={} (passthrough)", reqId, len); + req->SetResult(ORBIS_OK); + if (is_async) { + return ORBIS_OK; + } + return req->Wait(); +} + +s32 PS4_SYSV_ABI sceNpScoreCensorComment(s32 reqId, const char* comment, void* option) { + LOG_INFO(Lib_NpScore, "called reqId={}, comment={}, option={}", reqId, + comment ? comment : "null", PTR(option)); + return CensorCommentImpl(reqId, comment, option, false); +} + +s32 PS4_SYSV_ABI sceNpScoreCensorCommentAsync(s32 reqId, const char* comment, void* option) { + LOG_INFO(Lib_NpScore, "called reqId={}, comment={}, option={}", reqId, + comment ? comment : "null", PTR(option)); + return CensorCommentImpl(reqId, comment, option, true); +} + +//*********************************** +// Sanitize functions +//*********************************** +static int SanitizeCommentImpl(s32 reqId, const char* comment, char* sanitizedComment, void* option, + bool is_async) { + if (option != nullptr) { + LOG_ERROR(Lib_NpScore, "SanitizeComment: option must be null, got {}", PTR(option)); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (comment == nullptr) { + LOG_ERROR(Lib_NpScore, "SanitizeComment: comment must be non-null"); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + + if (sanitizedComment == nullptr) { + LOG_ERROR(Lib_NpScore, "SanitizeComment: sanitizedComment must be non-null"); + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + + std::shared_ptr req; + { + std::lock_guard lock(g_mutex); + req = LookupRequestUnlocked(reqId); + if (!req) { + LOG_ERROR(Lib_NpScore, "invalid reqId {}", reqId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; + } + if (IsRequestAborted(req)) { + return ORBIS_NP_COMMUNITY_ERROR_ABORTED; + } + } + + constexpr size_t probe = ORBIS_NP_SCORE_SANITIZE_COMMENT_MAXLEN + 1; + const size_t len = strnlen(comment, probe); + if (len > ORBIS_NP_SCORE_SANITIZE_COMMENT_MAXLEN) { + LOG_ERROR(Lib_NpScore, "SanitizeComment: comment too long (max {} chars + null)", + ORBIS_NP_SCORE_SANITIZE_COMMENT_MAXLEN); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + // Passthrough copy + std::memcpy(sanitizedComment, comment, len); + sanitizedComment[len] = '\0'; + + LOG_INFO(Lib_NpScore, "SanitizeComment: reqId={} len={} (passthrough)", reqId, len); + req->SetResult(ORBIS_OK); + if (is_async) { + return ORBIS_OK; + } + return req->Wait(); +} + +s32 PS4_SYSV_ABI sceNpScoreSanitizeComment(s32 reqId, const char* comment, char* sanitizedComment, + void* option) { + LOG_INFO(Lib_NpScore, "called reqId={}, comment={}, sanitizedComment={}, option={}", reqId, + comment ? comment : "null", PTR(sanitizedComment), PTR(option)); + return SanitizeCommentImpl(reqId, comment, sanitizedComment, option, false); +} + +s32 PS4_SYSV_ABI sceNpScoreSanitizeCommentAsync(s32 reqId, const char* comment, + char* sanitizedComment, void* option) { + LOG_INFO(Lib_NpScore, "called reqId={}, comment={}, sanitizedComment={}, option={}", reqId, + comment ? comment : "null", PTR(sanitizedComment), PTR(option)); + return SanitizeCommentImpl(reqId, comment, sanitizedComment, option, true); +} + +//*********************************** +// Misc functions +//*********************************** +s32 PS4_SYSV_ABI sceNpScoreSetPlayerCharacterId(s32 ctxId, OrbisNpScorePcId pcId) { + if (pcId < 0) { + LOG_ERROR(Lib_NpScore, "SetPlayerCharacterId: pcId={} < 0", pcId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + std::lock_guard lock(g_mutex); + + // Try title-ctx pool first. Setting it here updates the default that + // subsequent CreateRequest will inherit. + if (auto* tc = LookupTitleCtxUnlocked(ctxId)) { + tc->pcId = pcId; + LOG_INFO(Lib_NpScore, "SetPlayerCharacterId: titleCtxId={} pcId={}", ctxId, pcId); + return ORBIS_OK; + } + + // Fall through to the request pool. Setting it here only affects + // operations dispatched on this specific request + if (auto req = LookupRequestUnlocked(ctxId)) { + req->pcId = pcId; + LOG_INFO(Lib_NpScore, "SetPlayerCharacterId: reqId={} pcId={}", ctxId, pcId); + return ORBIS_OK; + } + + LOG_ERROR(Lib_NpScore, "SetPlayerCharacterId: invalid id {}", ctxId); + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ID; +} + +//*********************************** +// Stubbed functions +//*********************************** + +int PS4_SYSV_ABI sceNpScoreChangeModeForOtherSaveDataOwners() { LOG_ERROR(Lib_NpScore, "(STUBBED) called"); return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreGetFriendsRankingForCrossSave( +int PS4_SYSV_ABI sceNpScoreCreateTitleCtx() { + LOG_ERROR(Lib_NpScore, "(STUBBED) called"); + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceNpScoreGetFriendsRankingForCrossSave( s32 reqId, OrbisNpScoreBoardId boardId, s32 includeSelf, OrbisNpScoreRankDataForCrossSave* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, @@ -298,7 +1606,7 @@ s32 PS4_SYSV_ABI sceNpScoreGetFriendsRankingForCrossSave( return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreGetFriendsRankingForCrossSaveAsync( +int PS4_SYSV_ABI sceNpScoreGetFriendsRankingForCrossSaveAsync( s32 reqId, OrbisNpScoreBoardId boardId, s32 includeSelf, OrbisNpScoreRankDataForCrossSave* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, @@ -315,79 +1623,7 @@ s32 PS4_SYSV_ABI sceNpScoreGetFriendsRankingForCrossSaveAsync( return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreGetGameData(s32 reqId, OrbisNpScoreBoardId boardId, - const OrbisNpId* npId, u64* totalSize, u64 recvSize, - void* data, void* option) { - LOG_ERROR(Lib_NpScore, - "called reqId={}, boardId={}, npId={}, totalSize={}, recvSize={}, data={}, option={}", - reqId, boardId, PTR(npId), PTR(totalSize), recvSize, PTR(data), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetGameDataAsync(s32 reqId, OrbisNpScoreBoardId boardId, - const OrbisNpId* npId, u64* totalSize, u64 recvSize, - void* data, void* option) { - LOG_ERROR(Lib_NpScore, - "called reqId={}, boardId={}, npId={}, totalSize={}, recvSize={}, data={}, option={}", - reqId, boardId, PTR(npId), PTR(totalSize), recvSize, PTR(data), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetGameDataByAccountId(s32 reqId, OrbisNpScoreBoardId boardId, - OrbisNpAccountId accountId, u64* totalSize, - u64 recvSize, void* data, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, accountId={}, " - "totalSize={}, recvSize={}, data={}, option={}", - reqId, boardId, accountId, PTR(totalSize), recvSize, PTR(data), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetGameDataByAccountIdAsync(s32 reqId, OrbisNpScoreBoardId boardId, - OrbisNpAccountId accountId, u64* totalSize, - u64 recvSize, void* data, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, accountId={}, " - "totalSize={}, recvSize={}, data={}, option={}", - reqId, boardId, accountId, PTR(totalSize), recvSize, PTR(data), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountId( - s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpAccountId* accountIdArray, - u64 accountIdArraySize, OrbisNpScorePlayerRankDataA* rankArray, u64 rankArraySize, - OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, - u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, - OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, accountIdArray={}, " - "accountIdArraySize={}, rankArray={}, rankArraySize={}, commentArray={}, " - "commentArraySize={}, infoArray={}, infoArraySize={}, arrayNum={}, lastSortDate={}, " - "totalRecord={}, option={}", - reqId, boardId, PTR(accountIdArray), accountIdArraySize, PTR(rankArray), - rankArraySize, PTR(commentArray), commentArraySize, PTR(infoArray), infoArraySize, - arrayNum, PTR(lastSortDate), PTR(totalRecord), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdAsync( - s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpAccountId* accountIdArray, - u64 accountIdArraySize, OrbisNpScorePlayerRankDataA* rankArray, u64 rankArraySize, - OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, - u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, - OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, " - "accountIdArray={}, accountIdArraySize={}, rankArray={}, rankArraySize={}, " - "commentArray={}, commentArraySize={}, infoArray={}, infoArraySize={}, arrayNum={}, " - "lastSortDate={}, totalRecord={}, option={}", - reqId, boardId, PTR(accountIdArray), accountIdArraySize, PTR(rankArray), - rankArraySize, PTR(commentArray), commentArraySize, PTR(infoArray), infoArraySize, - arrayNum, PTR(lastSortDate), PTR(totalRecord), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdForCrossSave( +int PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdForCrossSave( s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpAccountId* accountIdArray, u64 accountIdArraySize, OrbisNpScorePlayerRankDataForCrossSave* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, @@ -404,7 +1640,7 @@ s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdForCrossSave( return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdForCrossSaveAsync( +int PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdForCrossSaveAsync( s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpAccountId* accountIdArray, u64 accountIdArraySize, OrbisNpScorePlayerRankDataForCrossSave* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, @@ -421,41 +1657,7 @@ s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdForCrossSaveAsync( return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdPcId( - s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpScoreAccountIdPcId* idArray, - u64 idArraySize, OrbisNpScorePlayerRankDataA* rankArray, u64 rankArraySize, - OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, - u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, - OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR( - Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, idArray={}, " - "idArraySize={}, rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, " - "infoArray={}, infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", - reqId, boardId, PTR(idArray), idArraySize, PTR(rankArray), rankArraySize, PTR(commentArray), - commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), - PTR(totalRecord), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdPcIdAsync( - s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpScoreAccountIdPcId* idArray, - u64 idArraySize, OrbisNpScorePlayerRankDataA* rankArray, u64 rankArraySize, - OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, - u64 infoArraySize, u64 arrayNum, Rtc::OrbisRtcTick* lastSortDate, - OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR( - Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, idArray={}, " - "idArraySize={}, rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, " - "infoArray={}, infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", - reqId, boardId, PTR(idArray), idArraySize, PTR(rankArray), rankArraySize, PTR(commentArray), - commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), - PTR(totalRecord), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdPcIdForCrossSave( +int PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdPcIdForCrossSave( s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpScoreAccountIdPcId* idArray, u64 idArraySize, OrbisNpScorePlayerRankDataForCrossSave* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, @@ -472,7 +1674,7 @@ s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdPcIdForCrossSave( return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdPcIdForCrossSaveAsync( +int PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdPcIdForCrossSaveAsync( s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpScoreAccountIdPcId* idArray, u64 idArraySize, OrbisNpScorePlayerRankDataForCrossSave* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, @@ -489,101 +1691,7 @@ s32 PS4_SYSV_ABI sceNpScoreGetRankingByAccountIdPcIdForCrossSaveAsync( return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreGetRankingByNpId( - s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpId* npIdArray, u64 npIdArraySize, - OrbisNpScorePlayerRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, - u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, - Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, - "called reqId={}, boardId={}, npIdArray={}, npIdArraySize={}, " - "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, " - "infoArray={}, infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, " - "option={}", - reqId, boardId, PTR(npIdArray), npIdArraySize, PTR(rankArray), rankArraySize, - PTR(commentArray), commentArraySize, PTR(infoArray), infoArraySize, arrayNum, - PTR(lastSortDate), PTR(totalRecord), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByNpIdAsync( - s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpId* npIdArray, u64 npIdArraySize, - OrbisNpScorePlayerRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, - u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, - Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, "called reqId={}, boardId={}, arrayNum={}", reqId, boardId, arrayNum); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByNpIdPcId( - s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpScoreNpIdPcId* idArray, u64 idArraySize, - OrbisNpScorePlayerRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, - u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, - Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, - "called reqId={}, boardId={}, idArray={}, idArraySize={}, " - "rankArray={}, rankArraySize={}, arrayNum={}", - reqId, boardId, PTR(idArray), idArraySize, PTR(rankArray), rankArraySize, arrayNum); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByNpIdPcIdAsync( - s32 reqId, OrbisNpScoreBoardId boardId, const OrbisNpScoreNpIdPcId* idArray, u64 idArraySize, - OrbisNpScorePlayerRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, - u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, - Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, - "called reqId={}, boardId={}, idArray={}, idArraySize={}, " - "rankArray={}, rankArraySize={}, arrayNum={}", - reqId, boardId, PTR(idArray), idArraySize, PTR(rankArray), rankArraySize, arrayNum); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByRangeA( - s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreRankNumber startSerialRank, - OrbisNpScoreRankDataA* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, - u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, - Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, startSerialRank={}, " - "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " - "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", - reqId, boardId, startSerialRank, PTR(rankArray), rankArraySize, PTR(commentArray), - commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), - PTR(totalRecord), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByRangeAAsync( - s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreRankNumber startSerialRank, - OrbisNpScoreRankDataA* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, - u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, - Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, startSerialRank={}, " - "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " - "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", - reqId, boardId, startSerialRank, PTR(rankArray), rankArraySize, PTR(commentArray), - commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), - PTR(totalRecord), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByRangeAsync( - s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreRankNumber startSerialRank, - OrbisNpScoreRankData* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, - u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, u64 infoArraySize, u64 arrayNum, - Rtc::OrbisRtcTick* lastSortDate, OrbisNpScoreRankNumber* totalRecord, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, startSerialRank={}, " - "rankArray={}, rankArraySize={}, commentArray={}, commentArraySize={}, infoArray={}, " - "infoArraySize={}, arrayNum={}, lastSortDate={}, totalRecord={}, option={}", - reqId, boardId, startSerialRank, PTR(rankArray), rankArraySize, PTR(commentArray), - commentArraySize, PTR(infoArray), infoArraySize, arrayNum, PTR(lastSortDate), - PTR(totalRecord), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreGetRankingByRangeForCrossSave( +int PS4_SYSV_ABI sceNpScoreGetRankingByRangeForCrossSave( s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreRankNumber startSerialRank, OrbisNpScoreRankDataForCrossSave* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, @@ -600,7 +1708,7 @@ s32 PS4_SYSV_ABI sceNpScoreGetRankingByRangeForCrossSave( return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreGetRankingByRangeForCrossSaveAsync( +int PS4_SYSV_ABI sceNpScoreGetRankingByRangeForCrossSaveAsync( s32 reqId, OrbisNpScoreBoardId boardId, OrbisNpScoreRankNumber startSerialRank, OrbisNpScoreRankDataForCrossSave* rankArray, u64 rankArraySize, OrbisNpScoreComment* commentArray, u64 commentArraySize, OrbisNpScoreGameInfo* infoArray, @@ -617,87 +1725,13 @@ s32 PS4_SYSV_ABI sceNpScoreGetRankingByRangeForCrossSaveAsync( return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScorePollAsync(s32 reqId, s32* result) { - LOG_ERROR(Lib_NpScore, "(STUBBED) called reqId={}, result={}", reqId, PTR(result)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreRecordGameData(s32 reqId, OrbisNpScoreBoardId boardId, - OrbisNpScoreValue score, u64 totalSize, u64 sendSize, - const void* data, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, score={}, totalSize={}, " - "sendSize={}, data={}, option={}", - reqId, boardId, score, totalSize, sendSize, PTR(data), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreRecordGameDataAsync(s32 reqId, OrbisNpScoreBoardId boardId, - OrbisNpScoreValue score, u64 totalSize, u64 sendSize, - const void* data, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, score={}, " - "totalSize={}, sendSize={}, data={}, option={}", - reqId, boardId, score, totalSize, sendSize, PTR(data), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreRecordScore(s32 reqId, OrbisNpScoreBoardId boardId, - OrbisNpScoreValue score, - const OrbisNpScoreComment* scoreComment, - const OrbisNpScoreGameInfo* gameInfo, - OrbisNpScoreRankNumber* tmpRank, - const Rtc::OrbisRtcTick* compareDate, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, score={}, scoreComment={}, " - "gameInfo={}, tmpRank={}, compareDate={}, option={}", - reqId, boardId, score, PTR(scoreComment), PTR(gameInfo), PTR(tmpRank), - PTR(compareDate), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreRecordScoreAsync(s32 reqId, OrbisNpScoreBoardId boardId, - OrbisNpScoreValue score, - const OrbisNpScoreComment* scoreComment, - const OrbisNpScoreGameInfo* gameInfo, - OrbisNpScoreRankNumber* tmpRank, - const Rtc::OrbisRtcTick* compareDate, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, boardId={}, score={}, " - "scoreComment={}, gameInfo={}, tmpRank={}, compareDate={}, option={}", - reqId, boardId, score, PTR(scoreComment), PTR(gameInfo), PTR(tmpRank), - PTR(compareDate), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreSanitizeComment(s32 reqId, const char* comment, char* sanitizedComment, - void* option) { - LOG_ERROR(Lib_NpScore, "(STUBBED) called reqId={}, comment={}, sanitizedComment={}, option={}", - reqId, comment ? comment : "null", PTR(sanitizedComment), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreSanitizeCommentAsync(s32 reqId, const char* comment, - char* sanitizedComment, void* option) { - LOG_ERROR(Lib_NpScore, - "(STUBBED) called reqId={}, comment={}, sanitizedComment={}, " - "option={}", - reqId, comment ? comment : "null", PTR(sanitizedComment), PTR(option)); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreSetPlayerCharacterId(s32 ctxId, OrbisNpScorePcId pcId) { - LOG_ERROR(Lib_NpScore, "(STUBBED) called ctxId={}, pcId={}", ctxId, pcId); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpScoreSetThreadParam(s32 threadPriority, u64 cpuAffinityMask) { +int PS4_SYSV_ABI sceNpScoreSetThreadParam(s32 threadPriority, u64 cpuAffinityMask) { LOG_ERROR(Lib_NpScore, "(STUBBED) called threadPriority={}, cpuAffinityMask={:#x}", threadPriority, cpuAffinityMask); return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreSetTimeout(s32 id, s32 resolveRetry, s32 resolveTimeout, s32 connTimeout, +int PS4_SYSV_ABI sceNpScoreSetTimeout(s32 id, s32 resolveRetry, s32 resolveTimeout, s32 connTimeout, s32 sendTimeout, s32 recvTimeout) { LOG_ERROR(Lib_NpScore, "(STUBBED) called id={}, resolveRetry={}, resolveTimeout={}, " @@ -706,13 +1740,10 @@ s32 PS4_SYSV_ABI sceNpScoreSetTimeout(s32 id, s32 resolveRetry, s32 resolveTimeo return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpScoreWaitAsync(s32 reqId, s32* result) { - LOG_ERROR(Lib_NpScore, "(STUBBED) sceNpScoreWaitAsync(reqId={}, result={})", reqId, - PTR(result)); - return ORBIS_OK; -} - void RegisterLib(Core::Loader::SymbolsResolver* sym) { + ASSERT_MSG(Libraries::Kernel::sceKernelGetCompiledSdkVersion(&g_firmware_version) == ORBIS_OK, + "Failed to get compiled SDK version."); + LIB_FUNCTION("1i7kmKbX6hk", "libSceNpScore", 1, "libSceNpScore", sceNpScoreAbortRequest); LIB_FUNCTION("2b3TI0mDYiI", "libSceNpScore", 1, "libSceNpScore", sceNpScoreCensorComment); LIB_FUNCTION("4eOvDyN-aZc", "libSceNpScore", 1, "libSceNpScore", sceNpScoreCensorCommentAsync); diff --git a/src/core/libraries/np/np_score/np_score.h b/src/core/libraries/np/np_score/np_score.h index 72dab339b..3ac5dc689 100644 --- a/src/core/libraries/np/np_score/np_score.h +++ b/src/core/libraries/np/np_score/np_score.h @@ -161,9 +161,9 @@ struct OrbisNpScorePlayerRankDataForCrossSave { //*********************************** // Title context management functions //*********************************** -s32 PS4_SYSV_ABI sceNpScoreCreateNpTitleCtx(OrbisNpServiceLabel serviceLabel, +int PS4_SYSV_ABI sceNpScoreCreateNpTitleCtx(OrbisNpServiceLabel serviceLabel, const OrbisNpId* selfNpId); -s32 PS4_SYSV_ABI sceNpScoreCreateNpTitleCtxA(OrbisNpServiceLabel npServiceLabel, +int PS4_SYSV_ABI sceNpScoreCreateNpTitleCtxA(OrbisNpServiceLabel npServiceLabel, UserService::OrbisUserServiceUserId selfId); s32 PS4_SYSV_ABI sceNpScoreDeleteNpTitleCtx(s32 titleCtxId); //*********************************** diff --git a/src/core/libraries/np/np_web_api/np_web_api.cpp b/src/core/libraries/np/np_web_api/np_web_api.cpp index faf495775..13665c203 100644 --- a/src/core/libraries/np/np_web_api/np_web_api.cpp +++ b/src/core/libraries/np/np_web_api/np_web_api.cpp @@ -315,7 +315,7 @@ s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValueLength(s64 requestId, cons } s32 PS4_SYSV_ABI sceNpWebApiGetHttpStatusCode(s64 requestId, s32* out_status_code) { - LOG_ERROR(Lib_NpWebApi, "called : requestId = {:#x}", requestId); + LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}", requestId); // On newer SDKs, NULL output pointer is invalid if (getCompiledSdkVersion() > Common::ElfInfo::FW_100 && out_status_code == nullptr) return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; @@ -462,8 +462,8 @@ s32 PS4_SYSV_ABI sceNpWebApiIntRegisterServicePushEventCallbackA( } s32 PS4_SYSV_ABI sceNpWebApiReadData(s64 requestId, void* pData, u64 size) { - LOG_ERROR(Lib_NpWebApi, "called : requestId = {:#x}, pData = {}, size = {:#x}", requestId, - fmt::ptr(pData), size); + LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}, pData = {}, size = {:#x}", requestId, + fmt::ptr(pData), size); if (pData == nullptr || size == 0) return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; 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 6bf67a850..f5d49c166 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 @@ -8,6 +8,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 "np_web_api_internal.h" namespace Libraries::Np::NpWebApi { @@ -613,11 +614,120 @@ s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s return ORBIS_NP_WEBAPI_ERROR_NOT_SIGNED_IN; } - LOG_ERROR(Lib_NpWebApi, - "(STUBBED) called, requestId = {:#x}, pApiGroup = '{}', pPath = '{}', pContentType = " - "'{}', method = {}, multipart = {}", - requestId, request->userApiGroup, request->userPath, request->userContentType, - magic_enum::enum_name(request->userMethod), request->multipart); + if (request->http_request_id == 0) { + std::string base_url = EmulatorSettings.GetShadNetWebApiServer(); + // sceHttpCreateConnectionWithURL expects a template id, not the raw libhttp + // context id that NpWebApi was initialized with. Create a template from the + // context first, then open the connection against it. + const s32 tmpl_id = Libraries::Http::sceHttpCreateTemplate( + context->libHttpCtxId, "libhttp", /*httpVer=*/2, /*isAutoProxyConf=*/0); + if (tmpl_id < 0) { + LOG_ERROR(Lib_NpWebApi, "sendRequest: sceHttpCreateTemplate failed: {:#x}", tmpl_id); + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return tmpl_id; + } + request->http_template_id = tmpl_id; + const int conn_id = Libraries::Http::sceHttpCreateConnectionWithURL( + tmpl_id, base_url.c_str(), /*enableKeepalive=*/true); + if (conn_id < 0) { + LOG_ERROR(Lib_NpWebApi, "sendRequest: sceHttpCreateConnectionWithURL failed: {:#x}", + conn_id); + Libraries::Http::sceHttpDeleteTemplate(tmpl_id); + request->http_template_id = 0; + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return conn_id; + } + request->http_connection_id = conn_id; + + // Convert the WebAPI method enum to libhttp's SceHttpMethods + // enum. The two enums have different orderings: + // + // OrbisNpWebApiHttpMethod: GET=0, POST=1, PUT=2, DELETE=3, PATCH=4 + // SceHttpMethods: GET=0, POST=1, HEAD=2, OPTIONS=3, + // PUT=4, DELETE=5, TRACE=6, CONNECT=7 + s32 sceMethod; + switch (request->userMethod) { + case ORBIS_NP_WEBAPI_HTTP_METHOD_GET: + sceMethod = 0; + break; + case ORBIS_NP_WEBAPI_HTTP_METHOD_POST: + sceMethod = 1; + break; + case ORBIS_NP_WEBAPI_HTTP_METHOD_PUT: + sceMethod = 4; + break; + case ORBIS_NP_WEBAPI_HTTP_METHOD_DELETE: + sceMethod = 5; + break; + case ORBIS_NP_WEBAPI_HTTP_METHOD_PATCH: + sceMethod = 8; // out-of-band PATCH marker recognised by libhttp + break; + default: + LOG_ERROR(Lib_NpWebApi, "sendRequest: unknown method enum value {}", + static_cast(request->userMethod)); + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + const std::string full_url = base_url + request->userPath; + const int req_id = Libraries::Http::sceHttpCreateRequestWithURL( + conn_id, sceMethod, full_url.c_str(), request->userContentLength); + if (req_id < 0) { + LOG_ERROR(Lib_NpWebApi, "sendRequest: sceHttpCreateRequestWithURL failed: {:#x}", + req_id); + Libraries::Http::sceHttpDeleteConnection(conn_id); + request->http_connection_id = 0; + Libraries::Http::sceHttpDeleteTemplate(tmpl_id); + request->http_template_id = 0; + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return req_id; + } + request->http_request_id = req_id; + + // Add Content-Type if the caller specified one. + // adds Accept-Encoding, User-Agent, OAuth Authorization , etc .... + // TODO check if we need to add them + if (!request->userContentType.empty()) { + Libraries::Http::sceHttpAddRequestHeader(req_id, "Content-Type", + request->userContentType.c_str(), /*mode=*/0); + } + + const std::string bearer = NpHandler::GetInstance().GetBearerToken(user_context->userId); + if (!bearer.empty()) { + const std::string auth_value = "Bearer " + bearer; + Libraries::Http::sceHttpAddRequestHeader(req_id, "Authorization", auth_value.c_str(), + /*mode=*/0); + } else { + LOG_WARNING(Lib_NpWebApi, + "sendRequest: no bearer token for user_id={}; request to '{}' will " + "be unauthenticated (expect 401 from server)", + user_context->userId, request->userPath); + } + } + + setRequestState(request, 4); + + const s32 send_err = + Libraries::Http::sceHttpSendRequest(request->http_request_id, pData, dataSize); + if (send_err < 0) { + LOG_ERROR(Lib_NpWebApi, "sendRequest: sceHttpSendRequest failed: {:#x}", send_err); + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return send_err; + } + + LOG_INFO(Lib_NpWebApi, + "sendRequest OK requestId={:#x} apiGroup='{}' path='{}' method={} httpReqId={}", + requestId, request->userApiGroup, request->userPath, + magic_enum::enum_name(request->userMethod), request->http_request_id); releaseRequest(request); releaseUserContext(user_context); @@ -709,6 +819,18 @@ s32 deleteRequest(s64 requestId) { } releaseRequest(request); + if (request->http_request_id != 0) { + Libraries::Http::sceHttpDeleteRequest(request->http_request_id); + request->http_request_id = 0; + } + if (request->http_connection_id != 0) { + Libraries::Http::sceHttpDeleteConnection(request->http_connection_id); + request->http_connection_id = 0; + } + if (request->http_template_id != 0) { + Libraries::Http::sceHttpDeleteTemplate(request->http_template_id); + request->http_template_id = 0; + } user_context->requests.erase(request->requestId); releaseUserContext(user_context); @@ -1374,7 +1496,7 @@ s32 unregisterExtdPushEventCallback(s32 titleUserCtxId, s32 callbackId) { s32 PS4_SYSV_ABI getHttpRequestIdFromRequest(OrbisNpWebApiRequest* request) { - return request->requestId; + return request->http_request_id; } s32 PS4_SYSV_ABI getHttpStatusCodeInternal(s64 requestId, s32* out_status_code) { diff --git a/src/core/libraries/np/np_web_api/np_web_api_internal.h b/src/core/libraries/np/np_web_api/np_web_api_internal.h index 73cfa8f2f..44d960d30 100644 --- a/src/core/libraries/np/np_web_api/np_web_api_internal.h +++ b/src/core/libraries/np/np_web_api/np_web_api_internal.h @@ -74,6 +74,9 @@ struct OrbisNpWebApiRequest { u64 remainingData; u32 readOffset; char data[64]; + s32 http_connection_id = 0; + s32 http_request_id = 0; + s32 http_template_id = 0; }; struct OrbisNpWebApiHandle { diff --git a/src/emulator.cpp b/src/emulator.cpp index da03ef8ca..661210d1e 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -124,6 +124,8 @@ std::map ExtractTrophies(const std::filesystem::path& npbind_p LOG_WARNING(Common_Filesystem, "No NPCommIDs in npbind.dat"); return trophy_index_map; } + auto& game_info = Common::ElfInfo::Instance(); + game_info.SetNpCommIds(np_comm_ids); if (!std::filesystem::exists(trophy_dir)) { LOG_WARNING(Common_Filesystem, "Game does not contain a trophy directory"); diff --git a/src/imgui/friends_layer.cpp b/src/imgui/friends_layer.cpp new file mode 100644 index 000000000..27aed77a9 --- /dev/null +++ b/src/imgui/friends_layer.cpp @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include + +#include "common/types.h" +#include "core/libraries/np/np_handler.h" +#include "imgui/friends_layer.h" +#include "imgui/imgui_layer.h" +#include "imgui/shadnet_notifications_layer.h" + +namespace ImGui::Friends { + +bool g_open = false; +int g_user_index = 0; +char g_add_buf[64] = {}; + +class FriendsLayer final : public ImGui::Layer { +public: + void Draw() override; +}; + +FriendsLayer g_layer; + +void DrawFriendsTab() { + auto& np = Libraries::Np::NpHandler::GetInstance(); + const std::vector users = np.GetConnectedUsers(); + + if (users.empty()) { + ImGui::TextUnformatted("No user is signed in to shadNet."); + return; + } + + if (g_user_index >= static_cast(users.size())) { + g_user_index = 0; + } + if (users.size() > 1) { + const std::string preview = "user " + std::to_string(users[g_user_index]); + if (ImGui::BeginCombo("User", preview.c_str())) { + for (int i = 0; i < static_cast(users.size()); ++i) { + const std::string label = "user " + std::to_string(users[i]); + if (ImGui::Selectable(label.c_str(), i == g_user_index)) { + g_user_index = i; + } + } + ImGui::EndCombo(); + } + } + const s32 user_id = users[g_user_index]; + const Libraries::Np::NpHandler::FriendListSnapshot fl = np.GetFriendList(user_id); + + ImGui::SeparatorText("Add friend"); + ImGui::InputTextWithHint("##addnpid", "Online ID", g_add_buf, sizeof(g_add_buf)); + ImGui::SameLine(); + if (ImGui::Button("Send request") && g_add_buf[0] != '\0') { + np.SendFriendRequest(user_id, g_add_buf); + g_add_buf[0] = '\0'; + } + + if (!fl.requests_received.empty()) { + ImGui::SeparatorText("Requests received"); + for (const auto& npid : fl.requests_received) { + ImGui::PushID(npid.c_str()); + ImGui::TextUnformatted(npid.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Accept")) { + np.SendFriendRequest(user_id, npid); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Decline")) { + np.RemoveFriend(user_id, npid); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Block")) { + np.BlockUser(user_id, npid); + } + ImGui::PopID(); + } + } + + if (!fl.requests_sent.empty()) { + ImGui::SeparatorText("Requests sent"); + for (const auto& npid : fl.requests_sent) { + ImGui::PushID(npid.c_str()); + ImGui::TextUnformatted(npid.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Cancel")) { + np.RemoveFriend(user_id, npid); + } + ImGui::PopID(); + } + } + + ImGui::SeparatorText(("Friends (" + std::to_string(fl.friends.size()) + ")").c_str()); + for (const auto& f : fl.friends) { + ImGui::PushID(f.npid.c_str()); + const ImVec4 col = + f.online ? ImVec4(0.30f, 0.85f, 0.35f, 1.0f) : ImVec4(0.60f, 0.60f, 0.60f, 1.0f); + ImGui::TextColored(col, "%s", f.online ? "[on] " : "[off]"); + ImGui::SameLine(); + ImGui::TextUnformatted(f.npid.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Remove")) { + np.RemoveFriend(user_id, f.npid); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Block")) { + np.BlockUser(user_id, f.npid); + } + ImGui::PopID(); + } + + if (!fl.blocked.empty()) { + ImGui::SeparatorText("Blocked"); + for (const auto& npid : fl.blocked) { + ImGui::PushID(npid.c_str()); + ImGui::TextUnformatted(npid.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Unblock")) { + np.UnblockUser(user_id, npid); + } + ImGui::PopID(); + } + } +} + +void DrawNotificationsTab() { + if (ImGui::SmallButton("Clear")) { + ImGui::ShadNetNotify::ClearHistory(); + } + const std::vector history = + ImGui::ShadNetNotify::GetHistory(); + ImGui::SameLine(); + ImGui::TextDisabled("%zu event(s)", history.size()); + ImGui::Separator(); + + ImGui::BeginChild("##notif_history", ImVec2(0.0f, 0.0f)); + if (history.empty()) { + ImGui::TextDisabled("No shadNet notifications yet."); + } + // Newest first. + for (auto it = history.rbegin(); it != history.rend(); ++it) { + const ImGui::ShadNetNotify::KindDisplay d = ImGui::ShadNetNotify::DisplayOf(it->kind); + const ImVec4 col(d.color[0], d.color[1], d.color[2], d.color[3]); + if (!it->time.empty()) { + ImGui::TextDisabled("%s", it->time.c_str()); + ImGui::SameLine(); + } + ImGui::TextColored(col, "[%s]", d.tag); + ImGui::SameLine(); + ImGui::TextWrapped("%s", it->text.c_str()); + } + ImGui::EndChild(); +} + +void Register() { + ImGui::Layer::AddLayer(&g_layer); +} +void Unregister() { + ImGui::Layer::RemoveLayer(&g_layer); +} +void Toggle() { + g_open = !g_open; +} +bool IsOpen() { + return g_open; +} + +void FriendsLayer::Draw() { + if (!g_open) { + return; + } + + const std::size_t unread = ImGui::ShadNetNotify::UnreadCount(); + const std::string title = + unread > 0 ? "shadNet Friends (" + std::to_string(unread) + ")###shadnet_friends" + : "shadNet Friends###shadnet_friends"; + + ImGui::SetNextWindowSize(ImVec2(440.0f, 560.0f), ImGuiCond_FirstUseEver); + if (!ImGui::Begin(title.c_str(), &g_open)) { + ImGui::End(); + return; + } + + if (ImGui::BeginTabBar("##shadnet_tabs")) { + if (ImGui::BeginTabItem("Friends")) { + DrawFriendsTab(); + ImGui::EndTabItem(); + } + const std::string notif_label = + unread > 0 ? "Notifications (" + std::to_string(unread) + ")###notif" + : "Notifications###notif"; + if (ImGui::BeginTabItem(notif_label.c_str())) { + ImGui::ShadNetNotify::MarkRead(); + DrawNotificationsTab(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + + ImGui::End(); +} + +} // namespace ImGui::Friends diff --git a/src/imgui/friends_layer.h b/src/imgui/friends_layer.h new file mode 100644 index 000000000..6241077d2 --- /dev/null +++ b/src/imgui/friends_layer.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +namespace ImGui::Friends { + +// Add/remove the Friends overlay layer. Call Register() once after ImGui is initialized +void Register(); +void Unregister(); + +// Show/hide the Friends window. Bind Toggle() to a hotkey or a menu entry. +void Toggle(); +bool IsOpen(); + +} // namespace ImGui::Friends diff --git a/src/imgui/shadnet_notifications_layer.cpp b/src/imgui/shadnet_notifications_layer.cpp new file mode 100644 index 000000000..afe2e2bf3 --- /dev/null +++ b/src/imgui/shadnet_notifications_layer.cpp @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include + +#include + +#include "common/types.h" +#include "imgui/imgui_layer.h" +#include "imgui/shadnet_notifications_layer.h" + +namespace ImGui::ShadNetNotify { + +constexpr float life_time = 6.0f; // seconds a toast stays before fading out +constexpr float fade_in = 0.25f; // fade-in duration +constexpr float fade_out = 0.5f; // fade-out duration +constexpr std::size_t kMaxToasts = 6; // oldest dropped beyond this + +struct Toast { + Kind kind; + std::string text; + float age = 0.0f; +}; + +constexpr std::size_t max_history = 100; + +std::mutex g_mutex; +std::deque g_toasts; +std::deque g_history; +std::size_t g_total = 0; // monotonic count of all pushes +std::size_t g_seen = 0; // value of g_total at last MarkRead() + +std::string NowHMS() { + const std::time_t t = std::time(nullptr); + std::tm tm{}; +#ifdef _WIN32 + localtime_s(&tm, &t); +#else + localtime_r(&t, &tm); +#endif + char buf[16]; + if (std::strftime(buf, sizeof(buf), "%H:%M:%S", &tm) == 0) { + return {}; + } + return buf; +} + +class ShadNetNotificationsUI final : public ImGui::Layer { +public: + void Draw() override; +}; + +ShadNetNotificationsUI g_layer; + +KindDisplay DisplayOf(Kind kind) { + switch (kind) { + case Kind::FriendRequest: + return {"Friend request", {0.45f, 0.70f, 1.00f, 1.0f}}; + case Kind::FriendNew: + return {"Friend", {0.35f, 0.85f, 0.40f, 1.0f}}; + case Kind::FriendLost: + return {"Friend", {0.85f, 0.55f, 0.35f, 1.0f}}; + case Kind::Online: + return {"Online", {0.35f, 0.85f, 0.40f, 1.0f}}; + case Kind::Info: + default: + return {"shadNet", {0.75f, 0.75f, 0.78f, 1.0f}}; + } +} + +void Push(Kind kind, std::string text) { + std::lock_guard lock(g_mutex); + g_toasts.push_back({kind, text, 0.0f}); + while (g_toasts.size() > kMaxToasts) { + g_toasts.pop_front(); + } + g_history.push_back({kind, std::move(text), NowHMS()}); + while (g_history.size() > max_history) { + g_history.pop_front(); + } + ++g_total; +} + +std::vector GetHistory() { + std::lock_guard lock(g_mutex); + return {g_history.begin(), g_history.end()}; +} + +void ClearHistory() { + std::lock_guard lock(g_mutex); + g_history.clear(); +} + +std::size_t UnreadCount() { + std::lock_guard lock(g_mutex); + return g_total - g_seen; +} + +void MarkRead() { + std::lock_guard lock(g_mutex); + g_seen = g_total; +} + +void Register() { + ImGui::Layer::AddLayer(&g_layer); +} +void Unregister() { + ImGui::Layer::RemoveLayer(&g_layer); +} + +void ShadNetNotificationsUI::Draw() { + auto& io = ImGui::GetIO(); + + std::vector snapshot; + { + std::lock_guard lock(g_mutex); + for (auto& t : g_toasts) { + t.age += io.DeltaTime; + } + while (!g_toasts.empty() && g_toasts.front().age >= life_time) { + g_toasts.pop_front(); + } + snapshot.assign(g_toasts.begin(), g_toasts.end()); + } + if (snapshot.empty()) { + return; + } + + const float scale = io.DisplaySize.x / 1920.0f; + const float margin = 20.0f * scale; + const float width = 320.0f * scale; + + ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - margin, margin), ImGuiCond_Always, + ImVec2(1.0f, 0.0f)); + ImGui::SetNextWindowSize(ImVec2(width, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.85f); + + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_AlwaysAutoResize; + + if (ImGui::Begin("##shadnet_notifications", nullptr, flags)) { + // Newest first. + for (auto it = snapshot.rbegin(); it != snapshot.rend(); ++it) { + float alpha = 1.0f; + const float remaining = life_time - it->age; + if (it->age < fade_in) { + alpha = it->age / fade_in; + } else if (remaining < fade_out) { + alpha = std::max(0.0f, remaining / fade_out); + } + + const KindDisplay ks = DisplayOf(it->kind); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); + const ImVec4 col(ks.color[0], ks.color[1], ks.color[2], ks.color[3]); + ImGui::TextColored(col, "[%s]", ks.tag); + ImGui::PushTextWrapPos(0.0f); + ImGui::TextUnformatted(it->text.c_str()); + ImGui::PopTextWrapPos(); + ImGui::PopStyleVar(); + + if (it + 1 != snapshot.rend()) { + ImGui::Separator(); + } + } + } + ImGui::End(); +} + +} // namespace ImGui::ShadNetNotify diff --git a/src/imgui/shadnet_notifications_layer.h b/src/imgui/shadnet_notifications_layer.h new file mode 100644 index 000000000..ffe89629e --- /dev/null +++ b/src/imgui/shadnet_notifications_layer.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +namespace ImGui::ShadNetNotify { + +// Kind drives the colored tag shown on the toast and in the history list. +enum class Kind { + FriendRequest, + FriendNew, + FriendLost, + Online, + Info, +}; + +// A retained notification entry +struct HistoryEntry { + Kind kind; + std::string text; + std::string time; // local "HH:MM:SS" captured when pushed +}; + +// Display style for a kind: short tag + RGBA color +struct KindDisplay { + const char* tag; + float color[4]; +}; +KindDisplay DisplayOf(Kind kind); + +// Queue a shadNet notification: shows a transient toast AND appends to history. +void Push(Kind kind, std::string text); + +// History access for UI (newest entries last) +std::vector GetHistory(); +void ClearHistory(); + +// Unread tracking for a UI badge: number of events pushed since the last MarkRead(). +// Call MarkRead() when the user is actually viewing the notifications list. +std::size_t UnreadCount(); +void MarkRead(); + +// Add/remove the toast overlay layer. Call Register() once after ImGui init and +// Unregister() on teardown. +void Register(); +void Unregister(); + +} // namespace ImGui::ShadNetNotify diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp index 7a8256b94..099388673 100644 --- a/src/input/input_handler.cpp +++ b/src/input/input_handler.cpp @@ -182,6 +182,7 @@ std::filesystem::path GetInputConfigFile(const std::string& game_id) { {"hotkey_volume_up", "kpplus"}, {"hotkey_volume_down", "kpminus"}, {"hotkey_emulator_settings", "f3"}, + {"hotkey_toggle_friends", "f2"}, }; std::string legacy_capture_binding; bool legacy_capture_binding_found = false; @@ -805,6 +806,9 @@ void ControllerOutput::FinalizeUpdate(u8 gamepad_index) { case HOTKEY_OPEN_EMULATOR_SETTINGS: ImGuiEmuSettings::OpenInGameSettingsDialog(); break; + case HOTKEY_TOGGLE_FRIENDS: + PushSDLEvent(SDL_EVENT_TOGGLE_FRIENDS); + break; case KEY_TOGGLE: // noop break; diff --git a/src/input/input_handler.h b/src/input/input_handler.h index 80f03eb01..8e31da059 100644 --- a/src/input/input_handler.h +++ b/src/input/input_handler.h @@ -42,6 +42,7 @@ #define SDL_EVENT_REMOVE_VIRTUAL_USER SDL_EVENT_USER + 12 #define SDL_EVENT_RDOC_CAPTURE SDL_EVENT_USER + 13 #define SDL_EVENT_SCREENSHOT_WITH_OVERLAYS SDL_EVENT_USER + 14 +#define SDL_EVENT_TOGGLE_FRIENDS SDL_EVENT_USER + 15 #define LEFTJOYSTICK_HALFMODE 0x00010000 #define RIGHTJOYSTICK_HALFMODE 0x00020000 @@ -65,6 +66,7 @@ #define HOTKEY_REMOVE_VIRTUAL_USER 0xf000000d #define HOTKEY_SCREENSHOT_WITH_OVERLAYS 0xf000000e #define HOTKEY_OPEN_EMULATOR_SETTINGS 0xf000000f +#define HOTKEY_TOGGLE_FRIENDS 0xf0000010 #define SDL_UNMAPPED UINT32_MAX - 1 @@ -172,6 +174,7 @@ const std::map string_to_hotkey_map = { {"hotkey_volume_up", HOTKEY_VOLUME_UP}, {"hotkey_volume_down", HOTKEY_VOLUME_DOWN}, {"hotkey_emulator_settings", HOTKEY_OPEN_EMULATOR_SETTINGS}, + {"hotkey_toggle_friends", HOTKEY_TOGGLE_FRIENDS}, }; const std::map string_to_axis_map = { @@ -537,7 +540,7 @@ public: class ControllerAllOutputs { public: - static constexpr u64 output_count = 42; + static constexpr u64 output_count = 43; std::array data = { // Important: these have to be the first, or else they will update in the wrong order ControllerOutput(LEFTJOYSTICK_HALFMODE), @@ -591,6 +594,7 @@ public: ControllerOutput(HOTKEY_VOLUME_UP), ControllerOutput(HOTKEY_VOLUME_DOWN), ControllerOutput(HOTKEY_OPEN_EMULATOR_SETTINGS), + ControllerOutput(HOTKEY_TOGGLE_FRIENDS), ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID), }; diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index b909796c4..c7f4d72ce 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -22,6 +22,7 @@ #include "core/libraries/pad/pad.h" #include "core/libraries/system/userservice.h" #include "core/user_settings.h" +#include "imgui/friends_layer.h" #include "imgui/renderer/imgui_core.h" #include "input/controller.h" #include "input/input_handler.h" @@ -288,6 +289,9 @@ void WindowSDL::WaitEvent() { case SDL_EVENT_TOGGLE_SIMPLE_FPS: Overlay::ToggleSimpleFps(); break; + case SDL_EVENT_TOGGLE_FRIENDS: + ImGui::Friends::Toggle(); + break; case SDL_EVENT_RELOAD_INPUTS: Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial())); break; diff --git a/src/shadnet/client.cpp b/src/shadnet/client.cpp new file mode 100644 index 000000000..f6c62a975 --- /dev/null +++ b/src/shadnet/client.cpp @@ -0,0 +1,751 @@ +// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "client.h" +#include "common/logging/log.h" +#include "shadnet.pb.h" + +#ifdef _WIN32 +#pragma comment(lib, "Ws2_32.lib") +static void PlatformInit() { + static bool done = false; + if (!done) { + WSADATA wd; + WSAStartup(MAKEWORD(2, 2), &wd); + done = true; + } +} +#else +static void PlatformInit() {} +#endif + +namespace ShadNet { + +// Build a u32-LE-prefixed proto blob payload for a request packet. +template +static std::vector MakeProtoPayload(const T& msg) { + const std::string serialised = msg.SerializeAsString(); + const u32 len = static_cast(serialised.size()); + std::vector out(4); + out[0] = static_cast(len); + out[1] = static_cast(len >> 8); + out[2] = static_cast(len >> 16); + out[3] = static_cast(len >> 24); + out.insert(out.end(), serialised.begin(), serialised.end()); + return out; +} + +// Read a u32-LE-prefixed proto blob starting at pos in p. +// Returns the raw bytes ready for ParseFromString. +std::string ShadNetClient::ExtractBlob(const std::vector& p, int pos) { + if (pos + 4 > static_cast(p.size())) + return {}; + const u32 len = GetLE32(p.data() + pos); + pos += 4; + if (pos + static_cast(len) > static_cast(p.size())) + return {}; + return std::string(reinterpret_cast(p.data() + pos), len); +} + +ShadNetClient::ShadNetClient() { + PlatformInit(); +} + +ShadNetClient::~ShadNetClient() { + Stop(); +} + +void ShadNetClient::Start(const std::string& host, u16 port, const std::string& npid, + const std::string& password, const std::string& token) { + m_host = host; + m_port = port; + m_npid = npid; + m_password = password; + m_token = token; + m_terminate = false; + m_thread_connect = std::thread(&ShadNetClient::ConnectThread, this); +} + +void ShadNetClient::Stop() { + m_terminate = true; + try { + m_sem_connected.release(); + } catch (...) { + } + try { + m_sem_authenticated.release(); + } catch (...) { + } + { + std::lock_guard lock(m_mutex_send_queue); + m_cv_send_queue.notify_all(); + } + DoDisconnect(); + if (m_thread_connect.joinable()) + m_thread_connect.join(); + if (m_thread_reader.joinable()) + m_thread_reader.join(); + if (m_thread_writer.joinable()) + m_thread_writer.join(); +} + +ShadNetState ShadNetClient::WaitForConnection() { + { + std::lock_guard lock(m_mutex_connected); + if (m_connected) + return ShadNetState::Ok; + } + m_sem_connected.acquire(); + return m_connected ? ShadNetState::Ok : m_state.load(); +} + +ShadNetState ShadNetClient::WaitForAuthenticated() { + { + std::lock_guard lock(m_mutex_authenticated); + if (m_authenticated) + return ShadNetState::Ok; + } + m_sem_authenticated.acquire(); + return m_authenticated ? ShadNetState::Ok : m_state.load(); +} + +bool ShadNetClient::IsConnected() const { + return m_connected.load(); +} +bool ShadNetClient::IsAuthenticated() const { + return m_authenticated.load(); +} +ShadNetState ShadNetClient::GetState() const { + return m_state.load(); +} +const std::string& ShadNetClient::GetAvatarUrl() const { + return m_avatar_url; +} +u64 ShadNetClient::GetUserId() const { + return m_user_id; +} +u32 ShadNetClient::GetAddrLocal() const { + return m_addr_local.load(); +} + +u32 ShadNetClient::GetNumFriends() const { + std::lock_guard lock(m_mutex_friends); + return static_cast(m_friends.size()); +} + +std::optional ShadNetClient::GetFriendNpid(u32 index) const { + std::lock_guard lock(m_mutex_friends); + if (index >= m_friends.size()) + return std::nullopt; + return m_friends[index].npid; +} + +std::string ShadNetClient::GetBearerToken() const { + std::lock_guard lock(m_mutex_bearer); + return m_bearer_token; +} + +// Threading + +void ShadNetClient::ConnectThread() { + bool connected = false; + u32 backoff_ms = SHAD_CONNECT_RETRY_BACKOFF_MS; + for (u32 attempt = 1; attempt <= SHAD_CONNECT_MAX_ATTEMPTS && !m_terminate; ++attempt) { + if (DoConnect()) { + connected = true; + break; + } + if (m_terminate || attempt == SHAD_CONNECT_MAX_ATTEMPTS) + break; + LOG_WARNING(ShadNet, "ShadNet: connect attempt {}/{} to {}:{} failed, retrying in {} ms", + attempt, SHAD_CONNECT_MAX_ATTEMPTS, m_host, m_port, backoff_ms); + // Interruptible backoff: poll m_terminate so Stop() wakes us promptly. + for (u32 waited = 0; waited < backoff_ms && !m_terminate; waited += 100) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + backoff_ms = std::min(backoff_ms * 2, 8000); + } + + if (!connected) { + // m_state holds the last failure reason from DoConnect. + m_sem_connected.release(); + m_sem_authenticated.release(); + return; + } + + m_thread_reader = std::thread(&ShadNetClient::ReaderThread, this); + m_thread_writer = std::thread(&ShadNetClient::WriterThread, this); + m_sem_connected.release(); + + // Build Login request as protobuf + shadnet::LoginRequest req; + req.set_npid(m_npid); + req.set_password(m_password); + if (!m_token.empty()) + req.set_token(m_token); + + const u64 id = m_pkt_counter.fetch_add(1); + if (!SendAll(BuildPacket(CommandType::Login, id, MakeProtoPayload(req)))) { + LOG_ERROR(ShadNet, "ShadNet: Failed to send Login packet"); + m_state = ShadNetState::FailureOther; + return; + } + LOG_INFO(ShadNet, "Login packet sent for '{}'", m_npid); +} + +void ShadNetClient::ReaderThread() { + while (!m_terminate) { + u8 hdr[SHAD_HEADER_SIZE]; + if (!RecvN(hdr, SHAD_HEADER_SIZE)) { + if (!m_terminate) + LOG_WARNING(ShadNet, "Reader header recv failed, disconnecting"); + break; + } + const auto ptype = static_cast(hdr[0]); + const u16 cmd_raw = GetLE16(hdr + 1); + const u32 total_sz = GetLE32(hdr + 3); + const u64 pkt_id = GetLE64(hdr + 7); + + if (total_sz < SHAD_HEADER_SIZE || total_sz > SHAD_MAX_PACKET_SIZE) { + LOG_ERROR(ShadNet, "Corrupt packet (total_sz={})", total_sz); + m_state = ShadNetState::FailureProtocol; + break; + } + std::vector payload; + const u32 payload_sz = total_sz - static_cast(SHAD_HEADER_SIZE); + if (payload_sz > 0) { + payload.resize(payload_sz); + if (!RecvN(payload.data(), payload_sz)) { + if (!m_terminate) + LOG_WARNING(ShadNet, "Reader payload recv failed"); + break; + } + } + DispatchPacket(ptype, cmd_raw, pkt_id, payload); + } + + if (!m_authenticated) { + if (m_state == ShadNetState::Ok) + m_state = ShadNetState::FailureOther; + m_sem_authenticated.release(); + } + m_connected = false; + m_authenticated = false; + LOG_INFO(ShadNet, "ReaderThread exiting"); +} + +void ShadNetClient::WriterThread() { + while (!m_terminate) { + std::unique_lock lock(m_mutex_send_queue); + m_cv_send_queue.wait(lock, [&] { return m_terminate.load() || !m_send_queue.empty(); }); + if (m_terminate) + break; + std::vector> batch; + std::swap(batch, m_send_queue); + lock.unlock(); + for (auto& pkt : batch) { + if (!m_connected) + break; + if (!SendAll(pkt)) { + LOG_ERROR(ShadNet, "WriterThread send failed"); + return; + } + } + } +} + +// Connect / Disconnect + +bool ShadNetClient::DoConnect() { + m_state = ShadNetState::Ok; // reset; this attempt sets a failure code only on error + struct addrinfo hints{}, *res_list = nullptr; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + if (::getaddrinfo(m_host.c_str(), std::to_string(m_port).c_str(), &hints, &res_list) != 0 || + !res_list) { + LOG_ERROR(ShadNet, "DNS resolution failed for '{}'", m_host); + m_state = ShadNetState::FailureResolve; + return false; + } + + m_sock = ::socket(res_list->ai_family, SOCK_STREAM, IPPROTO_TCP); + if (m_sock == SHAD_INVALID_SOCK) { + ::freeaddrinfo(res_list); + m_state = ShadNetState::FailureConnect; + return false; + } + + // Use non-blocking connect + select() so we time out instead of hanging + // for the OS default TCP timeout (75 s on Linux, >20 s on Windows). +#ifdef _WIN32 + { + u_long nb = 1; + ::ioctlsocket(m_sock, FIONBIO, &nb); + } +#else + { + int fl = ::fcntl(m_sock, F_GETFL, 0); + ::fcntl(m_sock, F_SETFL, fl | O_NONBLOCK); + } +#endif + + const int cr = ::connect(m_sock, res_list->ai_addr, static_cast(res_list->ai_addrlen)); + ::freeaddrinfo(res_list); + + bool connected = false; +#ifdef _WIN32 + const bool in_progress = (cr < 0 && WSAGetLastError() == WSAEWOULDBLOCK); +#else + const bool in_progress = (cr < 0 && errno == EINPROGRESS); +#endif + if (cr == 0 || in_progress) { + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(m_sock, &wfds); + struct timeval tv{static_cast(SHAD_CONNECT_TIMEOUT_MS / 1000), + static_cast((SHAD_CONNECT_TIMEOUT_MS % 1000) * 1000)}; + if (::select(static_cast(m_sock) + 1, nullptr, &wfds, nullptr, &tv) > 0) { + int err = 0; + socklen_t len = sizeof(err); + ::getsockopt(m_sock, SOL_SOCKET, SO_ERROR, reinterpret_cast(&err), &len); + connected = (err == 0); + } + } + if (!connected) { + LOG_ERROR(ShadNet, "connect() timed out or failed for {}:{}", m_host, m_port); + SHAD_CLOSE(m_sock); + m_sock = SHAD_INVALID_SOCK; + m_state = ShadNetState::FailureConnect; + return false; + } + + // Restore blocking mode for RecvN / SendAll +#ifdef _WIN32 + { + u_long nb = 0; + ::ioctlsocket(m_sock, FIONBIO, &nb); + } +#else + { + int fl = ::fcntl(m_sock, F_GETFL, 0); + ::fcntl(m_sock, F_SETFL, fl & ~O_NONBLOCK); + } +#endif + + struct sockaddr_in local{}; + socklen_t alen = sizeof(local); + if (::getsockname(m_sock, reinterpret_cast(&local), &alen) == 0) + m_addr_local.store(local.sin_addr.s_addr); + + LOG_INFO(ShadNet, "TCP connected to {}:{}", m_host, m_port); + + // Apply receive timeout for the ServerInfo handshake. + // Cleared after success so ReaderThread's RecvN blocks indefinitely as intended. +#ifdef _WIN32 + DWORD so_rcv = static_cast(SHAD_CONNECT_TIMEOUT_MS); +#else + struct timeval so_rcv{static_cast(SHAD_CONNECT_TIMEOUT_MS / 1000), + static_cast((SHAD_CONNECT_TIMEOUT_MS % 1000) * 1000)}; +#endif + ::setsockopt(m_sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&so_rcv), + sizeof(so_rcv)); + + // ServerInfo handshake + u8 hdr[SHAD_HEADER_SIZE]; + if (!RecvN(hdr, SHAD_HEADER_SIZE)) { + LOG_ERROR(ShadNet, "Timeout reading ServerInfo header"); + DoDisconnect(); + m_state = ShadNetState::FailureServerInfo; + return false; + } + if (static_cast(hdr[0]) != PacketType::ServerInfo) { + LOG_ERROR(ShadNet, "Expected ServerInfo, got packet type {:02x}", hdr[0]); + DoDisconnect(); + m_state = ShadNetState::FailureServerInfo; + return false; + } + const u32 total_sz = GetLE32(hdr + 3); + const u32 payload_sz = (total_sz > SHAD_HEADER_SIZE) ? total_sz - SHAD_HEADER_SIZE : 0; + std::vector si_payload(payload_sz); + if (payload_sz > 0 && !RecvN(si_payload.data(), payload_sz)) { + LOG_ERROR(ShadNet, "Timeout reading ServerInfo payload"); + DoDisconnect(); + m_state = ShadNetState::FailureServerInfo; + return false; + } + if (payload_sz >= 4) { + const u32 server_ver = GetLE32(si_payload.data()); + if (server_ver != SHAD_PROTOCOL_VERSION) { + LOG_ERROR(ShadNet, "Protocol version mismatch server={} client={}", server_ver, + SHAD_PROTOCOL_VERSION); + DoDisconnect(); + m_state = ShadNetState::FailureServerInfo; + return false; + } + } + LOG_INFO(ShadNet, "ServerInfo OK (protocol v{})", SHAD_PROTOCOL_VERSION); + + // Clear the receive timeout ReaderThread handles the socket from here. +#ifdef _WIN32 + DWORD no_timeout = 0; +#else + struct timeval no_timeout{0, 0}; +#endif + ::setsockopt(m_sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&no_timeout), + sizeof(no_timeout)); + + m_connected = true; + return true; +} + +void ShadNetClient::DoDisconnect() { + if (m_sock != SHAD_INVALID_SOCK) { +#ifdef _WIN32 + ::shutdown(m_sock, SD_BOTH); +#else + ::shutdown(m_sock, SHUT_RDWR); +#endif + SHAD_CLOSE(m_sock); + m_sock = SHAD_INVALID_SOCK; + } +} + +bool ShadNetClient::RecvN(u8* buf, u32 n) { + u32 received = 0; + while (received < n) { + if (m_terminate) + return false; + const int r = static_cast(::recv(m_sock, reinterpret_cast(buf + received), + static_cast(n - received), 0)); + if (r <= 0) + return false; + received += static_cast(r); + } + return true; +} + +bool ShadNetClient::SendAll(const std::vector& data) { + std::lock_guard lock(m_mutex_send_direct); + int sent = 0; + const int total = static_cast(data.size()); + while (sent < total) { + const int r = static_cast( + ::send(m_sock, reinterpret_cast(data.data() + sent), total - sent, 0)); + if (r < 0) { + LOG_ERROR(ShadNet, "send() failed"); + return false; + } + sent += r; + } + return true; +} + +std::vector ShadNetClient::BuildPacket(CommandType cmd, u64 id, + const std::vector& payload) const { + const u32 total = static_cast(SHAD_HEADER_SIZE + payload.size()); + std::vector out(SHAD_HEADER_SIZE); + out[0] = static_cast(PacketType::Request); + PutLE16(out, 1, static_cast(cmd)); + PutLE32(out, 3, total); + PutLE64(out, 7, id); + out.insert(out.end(), payload.begin(), payload.end()); + return out; +} + +u64 ShadNetClient::SubmitRequest(CommandType cmd, const std::vector& payload) { + const u64 pkt_id = m_pkt_counter.fetch_add(1); + auto pkt = BuildPacket(cmd, pkt_id, payload); + { + std::lock_guard lock(m_mutex_send_queue); + m_send_queue.push_back(std::move(pkt)); + } + m_cv_send_queue.notify_all(); + return pkt_id; +} + +u64 ShadNetClient::AddFriend(const std::string& npid) { + shadnet::FriendCommandRequest req; + req.set_npid(npid); + return SubmitRequest(CommandType::AddFriend, MakeProtoPayload(req)); +} + +u64 ShadNetClient::RemoveFriend(const std::string& npid) { + shadnet::FriendCommandRequest req; + req.set_npid(npid); + return SubmitRequest(CommandType::RemoveFriend, MakeProtoPayload(req)); +} + +u64 ShadNetClient::AddBlock(const std::string& npid) { + shadnet::FriendCommandRequest req; + req.set_npid(npid); + return SubmitRequest(CommandType::AddBlock, MakeProtoPayload(req)); +} + +u64 ShadNetClient::RemoveBlock(const std::string& npid) { + shadnet::FriendCommandRequest req; + req.set_npid(npid); + return SubmitRequest(CommandType::RemoveBlock, MakeProtoPayload(req)); +} + +// Packet dispatch + +void ShadNetClient::DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id, + const std::vector& payload) { + switch (type) { + case PacketType::Reply: + switch (static_cast(cmd_raw)) { + case CommandType::Login: + HandleLoginReply(payload); + break; + case CommandType::GetToken: + HandleGetTokenReply(payload); + break; + default: + if (onAsyncReply) { + // Every reply body starts with an ErrorType byte. + ErrorType err = + payload.empty() ? ErrorType::Malformed : static_cast(payload[0]); + std::vector body; + if (payload.size() > 1) { + body.assign(payload.begin() + 1, payload.end()); + } + onAsyncReply(static_cast(cmd_raw), pkt_id, err, body); + } else { + LOG_DEBUG(ShadNet, "Unhandled reply cmd={} pkt_id={}", cmd_raw, pkt_id); + } + break; + } + break; + case PacketType::Notification: + HandleNotification(cmd_raw, payload); + break; + case PacketType::ServerInfo: + LOG_DEBUG(ShadNet, "ServerInfo update received"); + break; + case PacketType::Request: + LOG_WARNING(ShadNet, "Unexpected Request from server"); + break; + } +} + +// Login reply + +void ShadNetClient::HandleLoginReply(const std::vector& payload) { + LoginResult res; + + if (payload.empty()) { + res.error = ErrorType::Malformed; + LOG_ERROR(ShadNet, "Empty Login reply"); + } else { + res.error = static_cast(payload[0]); + + if (res.error == ErrorType::NoError) { + // payload[0] = ErrorType byte + // payload[1..] = u32 LE blob size + LoginReply proto bytes + shadnet::LoginReply pb; + const std::string blob = ExtractBlob(payload, 1); + if (!blob.empty() && pb.ParseFromString(blob)) { + res.avatarUrl = pb.avatar_url(); + res.userId = pb.user_id(); + for (const auto& f : pb.friends()) { + FriendEntry fe; + fe.npid = f.npid(); + fe.online = f.online(); + res.friends.push_back(std::move(fe)); + } + for (const auto& n : pb.friend_requests_sent()) + res.requestsSent.push_back(n); + for (const auto& n : pb.friend_requests_received()) + res.requestsReceived.push_back(n); + for (const auto& n : pb.blocked()) + res.blocked.push_back(n); + + m_avatar_url = res.avatarUrl; + m_user_id = res.userId; + { + std::lock_guard lock(m_mutex_friends); + m_friends = res.friends; + } + m_authenticated = true; + LOG_INFO(ShadNet, "Logged in npid='{}' userId={} friends={}", m_npid, m_user_id, + m_friends.size()); + + const u64 pkt_id = m_pkt_counter.fetch_add(1); + std::vector empty_payload; + std::vector pkt = BuildPacket(CommandType::GetToken, pkt_id, empty_payload); + { + std::lock_guard lock(m_mutex_send_queue); + m_send_queue.push_back(std::move(pkt)); + } + m_cv_send_queue.notify_one(); + LOG_DEBUG(ShadNet, "GetToken request fired pkt_id={}", pkt_id); + } else { + res.error = ErrorType::Malformed; + LOG_ERROR(ShadNet, "Failed to parse LoginReply proto"); + m_state = ShadNetState::FailureProtocol; + m_sem_authenticated.release(); + DoDisconnect(); + } + } else { + switch (res.error) { + case ErrorType::LoginAlreadyLoggedIn: + m_state = ShadNetState::FailureAlreadyIn; + break; + case ErrorType::LoginInvalidUsername: + m_state = ShadNetState::FailureUsername; + break; + case ErrorType::LoginInvalidPassword: + m_state = ShadNetState::FailurePassword; + break; + case ErrorType::LoginInvalidToken: + m_state = ShadNetState::FailureToken; + break; + default: + m_state = ShadNetState::FailureAuth; + break; + } + LOG_ERROR(ShadNet, "Login rejected error code {}", static_cast(res.error)); + m_sem_authenticated.release(); + DoDisconnect(); + } + } + + if (onLoginResult) + onLoginResult(res); +} + +void ShadNetClient::HandleGetTokenReply(const std::vector& payload) { + if (payload.empty()) { + LOG_ERROR(ShadNet, "Empty GetToken reply"); + m_sem_authenticated.release(); + return; + } + const ErrorType err = static_cast(payload[0]); + if (err != ErrorType::NoError) { + LOG_WARNING(ShadNet, "GetToken returned error={} — WebAPI calls will be unauthenticated", + static_cast(err)); + m_sem_authenticated.release(); + return; + } + shadnet::GetTokenReply pb; + const std::string blob = ExtractBlob(payload, 1); + if (blob.empty() || !pb.ParseFromString(blob)) { + LOG_ERROR(ShadNet, "Failed to parse GetTokenReply proto"); + m_sem_authenticated.release(); + return; + } + { + std::lock_guard lock(m_mutex_bearer); + m_bearer_token = pb.token(); + } + LOG_INFO(ShadNet, "Bearer token captured ({} chars) for accountID={} canonical npid='{}'", + pb.token().size(), pb.user_id(), pb.npid()); + m_sem_authenticated.release(); +} + +// Notifications + +void ShadNetClient::HandleNotification(u16 cmd_raw, const std::vector& payload) { + // Notification payload = u32 LE blob size + proto bytes + const std::string blob = ExtractBlob(payload, 0); + if (blob.empty()) { + LOG_WARNING(ShadNet, "Empty notification payload type={}", cmd_raw); + return; + } + + switch (static_cast(cmd_raw)) { + case NotificationType::FriendQuery: { + shadnet::NotifyFriendQuery pb; + if (!pb.ParseFromString(blob)) { + LOG_WARNING(ShadNet, "FriendQuery parse error"); + break; + } + NotifyFriendQuery n; + n.fromNpid = pb.from_npid(); + LOG_DEBUG(ShadNet, "FriendQuery from '{}'", n.fromNpid); + if (onFriendQuery) + onFriendQuery(n); + break; + } + case NotificationType::FriendNew: { + shadnet::NotifyFriendNew pb; + if (!pb.ParseFromString(blob)) { + LOG_WARNING(ShadNet, "FriendNew parse error"); + break; + } + NotifyFriendNew n; + n.npid = pb.npid(); + n.online = pb.online(); + LOG_DEBUG(ShadNet, "FriendNew '{}' ({})", n.npid, n.online ? "online" : "offline"); + if (onFriendNew) + onFriendNew(n); + break; + } + case NotificationType::FriendLost: { + shadnet::NotifyFriendLost pb; + if (!pb.ParseFromString(blob)) { + LOG_WARNING(ShadNet, "FriendLost parse error"); + break; + } + NotifyFriendLost n; + n.npid = pb.npid(); + LOG_DEBUG(ShadNet, "FriendLost '{}'", n.npid); + if (onFriendLost) + onFriendLost(n); + break; + } + case NotificationType::FriendStatus: { + shadnet::NotifyFriendStatus pb; + if (!pb.ParseFromString(blob)) { + LOG_WARNING(ShadNet, "FriendStatus parse error"); + break; + } + NotifyFriendStatus n; + n.npid = pb.npid(); + n.online = pb.online(); + n.timestamp = pb.timestamp(); + LOG_DEBUG(ShadNet, "FriendStatus '{}' is {}", n.npid, n.online ? "online" : "offline"); + if (onFriendStatus) + onFriendStatus(n); + break; + } + default: + LOG_DEBUG(ShadNet, "Unknown notification type {}", cmd_raw); + break; + } +} + +void ShadNetClient::PutLE16(std::vector& b, size_t off, u16 v) { + b[off] = static_cast(v); + b[off + 1] = static_cast(v >> 8); +} +void ShadNetClient::PutLE32(std::vector& b, size_t off, u32 v) { + b[off] = static_cast(v); + b[off + 1] = static_cast(v >> 8); + b[off + 2] = static_cast(v >> 16); + b[off + 3] = static_cast(v >> 24); +} +void ShadNetClient::PutLE64(std::vector& b, size_t off, u64 v) { + for (int i = 0; i < 8; ++i) + b[off + i] = static_cast(v >> (8 * i)); +} +u16 ShadNetClient::GetLE16(const u8* p) { + return static_cast(p[0]) | (static_cast(p[1]) << 8); +} +u32 ShadNetClient::GetLE32(const u8* p) { + return static_cast(p[0]) | (static_cast(p[1]) << 8) | (static_cast(p[2]) << 16) | + (static_cast(p[3]) << 24); +} +u64 ShadNetClient::GetLE64(const u8* p) { + u64 v = 0; + for (int i = 0; i < 8; ++i) + v |= static_cast(p[i]) << (8 * i); + return v; +} + +} // namespace ShadNet diff --git a/src/shadnet/client.h b/src/shadnet/client.h new file mode 100644 index 000000000..747fff56d --- /dev/null +++ b/src/shadnet/client.h @@ -0,0 +1,298 @@ +// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/types.h" +#ifdef _WIN32 +#include +#include +using ShadSocketHandle = SOCKET; +static constexpr ShadSocketHandle SHAD_INVALID_SOCK = INVALID_SOCKET; +#define SHAD_CLOSE(s) ::closesocket(s) +#else +#include +#include +#include +#include +#include +#include +#include +using ShadSocketHandle = int; +static constexpr ShadSocketHandle SHAD_INVALID_SOCK = -1; +#define SHAD_CLOSE(s) ::close(s) +#endif + +namespace ShadNet { + +// Protocol constants +static constexpr u32 SHAD_HEADER_SIZE = 15; +static constexpr u32 SHAD_CONNECT_TIMEOUT_MS = 10000; // 10 second connect/handshake timeout +// MAX_ATTEMPTS * CONNECT_TIMEOUT + sum(backoffs). +static constexpr u32 SHAD_CONNECT_MAX_ATTEMPTS = 4; +static constexpr u32 SHAD_CONNECT_RETRY_BACKOFF_MS = + 1000; // base backoff, doubled per retry (cap 8s) +static constexpr u32 SHAD_PROTOCOL_VERSION = 1; +static constexpr u32 SHAD_MAX_PACKET_SIZE = 0x800000; // 8 MiB + +// Protocol enumerations (must match shadnet server protocol.h) +enum class PacketType : u8 { + Request = 0, + Reply = 1, + Notification = 2, + ServerInfo = 3, +}; + +enum class CommandType : u16 { + Login = 0, + Terminate = 1, + Create = 2, + Delete = 3, + SendToken = 4, + SendResetToken = 5, + ResetPassword = 6, + ResetState = 7, + AddFriend = 8, + RemoveFriend = 9, + AddBlock = 10, + RemoveBlock = 11, + // Matchmaking + RegisterHandlers = 12, + CreateRoom = 13, + JoinRoom = 14, + LeaveRoom = 15, + GetRoomList = 16, + RequestSignalingInfos = 17, + SetRoomDataInternal = 20, + SetRoomDataExternal = 21, + KickoutRoomMember = 22, + // 23-29 reserved for future matchmaking commands + GetBoardInfos = 30, + RecordScore = 31, + RecordScoreData = 32, + GetScoreData = 33, + GetScoreRange = 34, + GetScoreFriends = 35, + GetScoreNpid = 36, + GetScoreAccountId = 37, + GetScoreGameDataByAccId = 38, + GetToken = 39, +}; + +enum class NotificationType : u16 { + FriendQuery = 5, + FriendNew = 6, + FriendLost = 7, + FriendStatus = 8, +}; + +enum class ErrorType : uint8_t { + NoError = 0, + Malformed = 1, + Invalid = 2, + InvalidInput = 3, + TooSoon = 4, + LoginError = 5, + LoginAlreadyLoggedIn = 6, + LoginInvalidUsername = 7, + LoginInvalidPassword = 8, + LoginInvalidToken = 9, + CreationError = 10, + CreationExistingUsername = 11, + CreationBannedEmailProvider = 12, + CreationExistingEmail = 13, + RoomMissing = 14, + RoomAlreadyJoined = 15, + RoomFull = 16, + RoomPasswordMismatch = 17, + RoomPasswordMissing = 18, + RoomGroupNoJoinLabel = 19, + RoomGroupFull = 20, + RoomGroupJoinLabelNotFound = 21, + RoomGroupMaxSlotMismatch = 22, + Unauthorized = 23, + DbFail = 24, + EmailFail = 25, + NotFound = 26, + Blocked = 27, + AlreadyFriend = 28, + ScoreNotBest = 29, + ScoreInvalid = 30, + ScoreHasData = 31, + CondFail = 32, + Unsupported = 33, +}; + +enum class ShadNetState { + Ok, + FailureInput, + FailureResolve, + FailureConnect, + FailureServerInfo, + FailureAuth, + FailureAlreadyIn, + FailureUsername, + FailurePassword, + FailureToken, + FailureProtocol, + FailureOther, +}; + +// Callback data structures + +struct FriendEntry { + std::string npid; + bool online = false; +}; + +struct LoginResult { + ErrorType error = ErrorType::Malformed; + std::string avatarUrl; + u64 userId = 0; + std::vector friends; + std::vector requestsSent; + std::vector requestsReceived; + std::vector blocked; +}; + +struct NotifyFriendQuery { + std::string fromNpid; +}; +struct NotifyFriendNew { + std::string npid; + bool online = false; +}; +struct NotifyFriendLost { + std::string npid; +}; +struct NotifyFriendStatus { + std::string npid; + bool online = false; + u64 timestamp = 0; +}; + +// ShadNetClient + +class ShadNetClient { +public: + ShadNetClient(); + ~ShadNetClient(); + ShadNetClient(const ShadNetClient&) = delete; + ShadNetClient& operator=(const ShadNetClient&) = delete; + + void Start(const std::string& host, u16 port, const std::string& npid, + const std::string& password, const std::string& token = {}); + void Stop(); + + ShadNetState WaitForConnection(); + ShadNetState WaitForAuthenticated(); + + bool IsConnected() const; + bool IsAuthenticated() const; + ShadNetState GetState() const; + + const std::string& GetAvatarUrl() const; + u64 GetUserId() const; + u32 GetAddrLocal() const; + u32 GetNumFriends() const; + std::optional GetFriendNpid(u32 index) const; + + std::string GetBearerToken() const; + + // Callbacks + std::function onLoginResult; + std::function onFriendQuery; + std::function onFriendNew; + std::function onFriendLost; + std::function onFriendStatus; + // Async reply callback. + // cmd —command this reply is for (matches the request's cmd) + // pkt_id —packet id echoed back from the original request header + // error —ErrorType byte that prefixes every reply body + // body —reply payload AFTER the error byte (may be empty) + std::function& body)> + onAsyncReply; + + // Submit a Request packet for async processing. + // Allocates a packet id, builds the packet, pushes it to the writer queue. + // Returns the packet id so callers can correlate the eventual reply. + u64 SubmitRequest(CommandType cmd, const std::vector& payload); + + // Friend / block commands. + u64 AddFriend(const std::string& npid); + u64 RemoveFriend(const std::string& npid); + u64 AddBlock(const std::string& npid); + u64 RemoveBlock(const std::string& npid); + +private: + void ConnectThread(); + void ReaderThread(); + void WriterThread(); + bool DoConnect(); + void DoDisconnect(); + bool RecvN(u8* buf, u32 n); + bool SendAll(const std::vector& data); + std::vector BuildPacket(CommandType cmd, u64 id, const std::vector& payload) const; + void DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id, const std::vector& payload); + void HandleLoginReply(const std::vector& payload); + void HandleGetTokenReply(const std::vector& payload); + void HandleNotification(u16 cmd_raw, const std::vector& payload); + + // Helper: read a u32-LE-prefixed proto blob from a byte vector at pos. + static std::string ExtractBlob(const std::vector& p, int pos); + + static void PutLE16(std::vector& b, size_t off, u16 v); + static void PutLE32(std::vector& b, size_t off, u32 v); + static void PutLE64(std::vector& b, size_t off, u64 v); + static u16 GetLE16(const u8* p); + static u32 GetLE32(const u8* p); + static u64 GetLE64(const u8* p); + + ShadSocketHandle m_sock = SHAD_INVALID_SOCK; + std::string m_host; + u16 m_port = 31313; + std::string m_npid; + std::string m_password; + std::string m_token; + + std::atomic m_terminate{false}; + std::atomic m_connected{false}; + std::atomic m_authenticated{false}; + std::atomic m_state{ShadNetState::Ok}; + + std::binary_semaphore m_sem_connected{0}; + std::binary_semaphore m_sem_authenticated{0}; + std::mutex m_mutex_connected; + std::mutex m_mutex_authenticated; + + std::thread m_thread_connect; + std::thread m_thread_reader; + std::thread m_thread_writer; + + std::mutex m_mutex_send_direct; + std::mutex m_mutex_send_queue; + std::condition_variable m_cv_send_queue; + std::vector> m_send_queue; + + std::string m_avatar_url; + u64 m_user_id = 0; + mutable std::mutex m_mutex_bearer; + std::string m_bearer_token; + std::atomic m_addr_local{0}; + + mutable std::mutex m_mutex_friends; + std::vector m_friends; + + std::atomic m_pkt_counter{1}; +}; + +} // namespace ShadNet diff --git a/src/shadnet/shadnet.proto b/src/shadnet/shadnet.proto new file mode 100644 index 000000000..0888eb342 --- /dev/null +++ b/src/shadnet/shadnet.proto @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: Copyright 2019-2026 rpcsn Project +// SPDX-FileCopyrightText: Copyright 2026 shadNet Project +// SPDX-License-Identifier: GPL-2.0-or-later + +syntax = "proto3"; + +package shadnet; + +option optimize_for = LITE_RUNTIME; + +// Account + +message LoginRequest { + string npid = 1; + string password = 2; + string token = 3; +} + +message FriendEntry { + string npid = 1; + bool online = 2; + bytes presence = 3; +} + +message LoginReply { + string avatar_url = 1; + uint64 user_id = 2; + repeated FriendEntry friends = 3; + repeated string friend_requests_sent = 4; + repeated string friend_requests_received = 5; + repeated string blocked = 6; +} + +message GetTokenReply { + string token = 1; + uint64 user_id = 2; + string npid = 3; +} + +message RegistrationRequest { + string npid = 1; + string password = 2; + string avatar_url = 3; // optional; server uses default if empty + string email = 4; + string secret_key = 5; // optional; must match server config if set +} + +// Friend / block commands +// Shared by AddFriend, RemoveFriend, AddBlock, RemoveBlock. + +message FriendCommandRequest { + string npid = 1; // target Online ID +} + +// Notifications + +// NotificationType::FriendQuery (5) +message NotifyFriendQuery { + string from_npid = 1; +} + +// NotificationType::FriendNew (6) +message NotifyFriendNew { + string npid = 1; + bool online = 2; +} + +// NotificationType::FriendLost (7) +message NotifyFriendLost { + string npid = 1; +} + +// NotificationType::FriendStatus (8) +message NotifyFriendStatus { + string npid = 1; + bool online = 2; + uint64 timestamp = 3; // nanoseconds since Unix epoch +} + +// Score: board configuration + +message BoardInfo { + uint32 rankLimit = 1; + uint32 updateMode = 2; // 0=NORMAL_UPDATE, 1=FORCE_UPDATE + uint32 sortMode = 3; // 0=DESCENDING, 1=ASCENDING + uint32 uploadNumLimit = 4; + uint64 uploadSizeLimit = 5; +} + +// Score: requests + +message RecordScoreRequest { + uint32 boardId = 1; + int32 pcId = 2; + int64 score = 3; + string comment = 4; + bytes data = 5; // optional inline game-info blob +} + +message RecordScoreGameDataRequest { + uint32 boardId = 1; + int32 pcId = 2; + int64 score = 3; +} + +message GetScoreGameDataRequest { + uint32 boardId = 1; + string npId = 2; + int32 pcId = 3; +} + +message GetScoreRangeRequest { + uint32 boardId = 1; + uint32 startRank = 2; + uint32 numRanks = 3; + bool withComment = 4; + bool withGameInfo = 5; +} + +message ScoreNpIdPcId { + string npid = 1; + int32 pcId = 2; +} + +message GetScoreNpIdRequest { + uint32 boardId = 1; + repeated ScoreNpIdPcId npids = 2; + bool withComment = 3; + bool withGameInfo = 4; +} + +message GetScoreFriendsRequest { + uint32 boardId = 1; + bool includeSelf = 2; + uint32 max = 3; + bool withComment = 4; + bool withGameInfo = 5; +} + +message ScoreAccountIdPcId { + int64 accountId = 1; + int32 pcId = 2; +} + +message GetScoreAccountIdRequest { + uint32 boardId = 1; + repeated ScoreAccountIdPcId ids = 2; + bool withComment = 3; + bool withGameInfo = 4; +} + +message GetScoreGameDataByAccountIdRequest { + uint32 boardId = 1; + int64 accountId = 2; + int32 pcId = 3; +} + +// Score: responses + +message ScoreRankData { + string npId = 1; + int32 pcId = 2; + uint32 rank = 3; + int64 score = 4; + bool hasGameData = 5; + uint64 recordDate = 6; + int64 accountId = 7; +} + +message ScoreInfo { + bytes data = 1; +} + +message GetScoreResponse { + repeated ScoreRankData rankArray = 1; + repeated string commentArray = 2; + repeated ScoreInfo infoArray = 3; + uint64 lastSortDate = 4; + uint32 totalRecord = 5; +} diff --git a/src/video_core/renderer_vulkan/vk_presenter.cpp b/src/video_core/renderer_vulkan/vk_presenter.cpp index a98a5ce23..584cba904 100644 --- a/src/video_core/renderer_vulkan/vk_presenter.cpp +++ b/src/video_core/renderer_vulkan/vk_presenter.cpp @@ -10,9 +10,11 @@ #include "core/devtools/layer.h" #include "core/emulator_settings.h" #include "core/libraries/system/systemservice.h" +#include "imgui/friends_layer.h" #include "imgui/notifications_layer.h" #include "imgui/renderer/imgui_core.h" #include "imgui/renderer/imgui_impl_vulkan.h" +#include "imgui/shadnet_notifications_layer.h" #include "sdl_window.h" #include "video_core/buffer_cache/buffer.h" #include "video_core/renderdoc.h" @@ -520,9 +522,13 @@ Presenter::Presenter(Frontend::WindowSDL& window_, AmdGpu::Liverpool* liverpool_ pp_pass.Create(device, swapchain.GetSurfaceFormat().format); ImGui::Layer::AddLayer(Common::Singleton::Instance()); + ImGui::Friends::Register(); + ImGui::ShadNetNotify::Register(); } Presenter::~Presenter() { + ImGui::ShadNetNotify::Unregister(); + ImGui::Friends::Unregister(); ImGui::Layer::RemoveLayer(Common::Singleton::Instance()); draw_scheduler.Finish();