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
This commit is contained in:
georgemoralis 2026-06-24 23:57:43 +03:00 committed by GitHub
parent 8651fd78d8
commit aff387e8a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 5749 additions and 487 deletions

3
.gitmodules vendored
View File

@ -155,3 +155,6 @@
path = externals/vulkan-loader path = externals/vulkan-loader
url = https://github.com/KhronosGroup/Vulkan-Loader url = https://github.com/KhronosGroup/Vulkan-Loader
shallow = true shallow = true
[submodule "externals/protobuf"]
path = externals/protobuf
url = https://github.com/shadexternals/protobuf.git

View File

@ -1129,6 +1129,10 @@ set(IMGUI src/imgui/imgui_config.h
src/imgui/imgui_texture.h src/imgui/imgui_texture.h
src/imgui/imgui_translations.cpp src/imgui/imgui_translations.cpp
src/imgui/imgui_translations.h 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.cpp
src/imgui/notifications_layer.h src/imgui/notifications_layer.h
src/imgui/renderer/imgui_core.cpp src/imgui/renderer/imgui_core.cpp
@ -1169,6 +1173,27 @@ set(EMULATOR src/emulator.cpp
src/sdl_window.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) if (APPLE)
list(APPEND EMULATOR src/sdl_window_apple.mm) list(APPEND EMULATOR src/sdl_window_apple.mm)
endif() endif()
@ -1184,6 +1209,7 @@ add_executable(shadps4
${SHADER_RECOMPILER} ${SHADER_RECOMPILER}
${VIDEO_CORE} ${VIDEO_CORE}
${EMULATOR} ${EMULATOR}
${SHADNET}
src/main.cpp src/main.cpp
src/emulator.cpp src/emulator.cpp
src/emulator.h 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 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 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 Freetype::Freetype)
target_link_libraries(shadps4 PRIVATE libprotobuf)
target_include_directories(shadps4 PRIVATE "${SHADNET_PROTO_OUT}")
if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
target_link_libraries(shadps4 PRIVATE "/usr/lib/libusb.so") target_link_libraries(shadps4 PRIVATE "/usr/lib/libusb.so")

View File

@ -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_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) set(VULKAN_ICD_PATH "${CMAKE_CURRENT_BINARY_DIR}/mesa-kosmickrisp/outputs/kosmickrisp_mesa_icd.json" PARENT_SCOPE)
endif() 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)

1
externals/protobuf vendored Submodule

@ -0,0 +1 @@
Subproject commit 5917ee224a7e47383a78acc129628511fe210dff

View File

@ -75,6 +75,7 @@ class ElfInfo {
std::filesystem::path splash_path{}; std::filesystem::path splash_path{};
std::filesystem::path game_folder{}; std::filesystem::path game_folder{};
std::vector<std::string> npCommIds{};
std::map<s32, std::string> trophy_index_map{}; std::map<s32, std::string> trophy_index_map{};
public: public:
@ -148,6 +149,14 @@ public:
[[nodiscard]] const std::map<s32, std::string>& GetTrophyIndexMap() const { [[nodiscard]] const std::map<s32, std::string>& GetTrophyIndexMap() const {
return trophy_index_map; return trophy_index_map;
} }
[[nodiscard]] const std::vector<std::string>& GetNpCommIds() const {
return npCommIds;
}
void SetNpCommIds(std::vector<std::string> ids) {
npCommIds = std::move(ids);
}
}; };
} // namespace Common } // namespace Common

View File

@ -107,9 +107,11 @@ constexpr auto Lib_WebBrowserDialog = "Lib.WebBrowserDialog"; ///< The Lib
constexpr auto Lib_Zlib = "Lib.Zlib"; ///< The LibSceZlib implementation. constexpr auto Lib_Zlib = "Lib.Zlib"; ///< The LibSceZlib implementation.
constexpr auto Loader = "Loader"; ///< ROM loader constexpr auto Loader = "Loader"; ///< ROM loader
constexpr auto Log = "Log"; ///< Messages about the log system itself 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 = "Render"; ///< Video Core
constexpr auto Render_Recompiler = "Render.Recompiler"; ///< Shader recompiler constexpr auto Render_Recompiler = "Render.Recompiler"; ///< Shader recompiler
constexpr auto Render_Vulkan = "Render.Vulkan"; ///< Vulkan backend constexpr auto Render_Vulkan = "Render.Vulkan"; ///< Vulkan backend
constexpr auto ShadNet = "ShadNet"; ///< shadNet binary protocol client
constexpr auto Tty = "Tty"; ///< Debug output from emu constexpr auto Tty = "Tty"; ///< Debug output from emu
// clang-format on // clang-format on
} // namespace Common::Log::Class } // namespace Common::Log::Class

View File

@ -131,9 +131,11 @@ std::unordered_map<std::string_view, std::shared_ptr<spdlog::logger>> ALL_LOGGER
{Class::Lib_Zlib, nullptr}, {Class::Lib_Zlib, nullptr},
{Class::Loader, nullptr}, {Class::Loader, nullptr},
{Class::Log, nullptr}, {Class::Log, nullptr},
{Class::NpHandler, nullptr},
{Class::Render, nullptr}, {Class::Render, nullptr},
{Class::Render_Recompiler, nullptr}, {Class::Render_Recompiler, nullptr},
{Class::Render_Vulkan, nullptr}, {Class::Render_Vulkan, nullptr},
{Class::ShadNet, nullptr},
{Class::Tty, nullptr}, {Class::Tty, nullptr},
}; };

View File

@ -183,9 +183,9 @@ struct GeneralSettings {
Setting<bool> show_fps_counter{false}; Setting<bool> show_fps_counter{false};
Setting<int> console_language{1}; Setting<int> console_language{1};
Setting<int> big_picture_scale{1000}; Setting<int> big_picture_scale{1000};
Setting<std::string> shadnet_server{""}; Setting<std::string> shadnet_server{"srv.shadps4.net:31313"};
Setting<std::string> signaling_addr{""}; Setting<std::string> shadnet_webapi_server{"http://srv.shadps4.net:31315"};
Setting<u16> signaling_port{}; Setting<std::string> signaling_info{};
Setting<bool> enable_upnp{true}; Setting<bool> enable_upnp{true};
// return a vector of override descriptors (runtime, but tiny) // return a vector of override descriptors (runtime, but tiny)
@ -205,10 +205,7 @@ struct GeneralSettings {
make_override<GeneralSettings>("trophy_notification_side", make_override<GeneralSettings>("trophy_notification_side",
&GeneralSettings::trophy_notification_side), &GeneralSettings::trophy_notification_side),
make_override<GeneralSettings>("connected_to_network", make_override<GeneralSettings>("connected_to_network",
&GeneralSettings::connected_to_network), &GeneralSettings::connected_to_network)};
make_override<GeneralSettings>("signaling_addr", &GeneralSettings::signaling_addr),
make_override<GeneralSettings>("signaling_port", &GeneralSettings::signaling_port),
make_override<GeneralSettings>("enable_upnp", &GeneralSettings::enable_upnp)};
} }
}; };
@ -218,8 +215,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GeneralSettings, install_dirs, addon_install_
trophy_notification_duration, show_splash, trophy_notification_duration, show_splash,
trophy_notification_side, connected_to_network, trophy_notification_side, connected_to_network,
discord_rpc_enabled, show_fps_counter, console_language, discord_rpc_enabled, show_fps_counter, console_language,
big_picture_scale, shadnet_server, signaling_addr, big_picture_scale, shadnet_server, shadnet_webapi_server,
signaling_port, enable_upnp) signaling_info, enable_upnp)
// ------------------------------- // -------------------------------
// Log settings // Log settings
@ -605,8 +602,8 @@ public:
SETTING_FORWARD(m_general, ConsoleLanguage, console_language) SETTING_FORWARD(m_general, ConsoleLanguage, console_language)
SETTING_FORWARD(m_general, BigPictureScale, big_picture_scale) SETTING_FORWARD(m_general, BigPictureScale, big_picture_scale)
SETTING_FORWARD(m_general, ShadNetServer, shadnet_server) SETTING_FORWARD(m_general, ShadNetServer, shadnet_server)
SETTING_FORWARD(m_general, SignalingAddr, signaling_addr) SETTING_FORWARD(m_general, ShadNetWebApiServer, shadnet_webapi_server)
SETTING_FORWARD(m_general, SignalingPort, signaling_port) SETTING_FORWARD(m_general, SignalingInfo, signaling_info)
SETTING_FORWARD_BOOL(m_general, UPnPEnabled, enable_upnp) SETTING_FORWARD_BOOL(m_general, UPnPEnabled, enable_upnp)
// Log settings // Log settings

View File

@ -1522,6 +1522,11 @@ int PS4_SYSV_ABI sceHttpSendRequest(int reqId, const void* postData, u64 size) {
plan.method = req.method; plan.method = req.method;
plan.method_str = req.method_str; 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.path = ExtractPathFromUrl(req.url);
plan.settings = req.settings; plan.settings = req.settings;
if (auto conn_it = g_state.connections.find(req.conn_id); 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; auto& req = *it->second;
int wr = WaitForResponseReady(req, lock); int wr = WaitForResponseReady(req, lock);
if (wr == ORBIS_HTTP_ERROR_EAGAIN) { if (wr == ORBIS_HTTP_ERROR_EAGAIN) {
LOG_DEBUG(Lib_Http, "reqId={}: response not yet ready, returning BEFORE_SEND", reqId); LOG_DEBUG(Lib_Http, "reqId={}: response not yet ready, returning EAGAIN", reqId);
return ORBIS_HTTP_ERROR_BEFORE_SEND; return ORBIS_HTTP_ERROR_EAGAIN;
} }
if (wr != ORBIS_OK) { if (wr != ORBIS_OK) {
LOG_ERROR(Lib_Http, "Wait failed for reqId={}: {:#x}", reqId, static_cast<u32>(wr)); LOG_ERROR(Lib_Http, "Wait failed for reqId={}: {:#x}", reqId, static_cast<u32>(wr));

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,28 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include <common/types.h>
#include "np_types.h" #include <atomic>
#include <chrono>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#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 { namespace Libraries::Np {
@ -15,17 +35,248 @@ public:
NpHandler(const NpHandler&) = delete; NpHandler(const NpHandler&) = delete;
NpHandler& operator=(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. // True if this specific user is authenticated to the shadNet server.
bool IsPsnSignedIn(s32 user_id) const; 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. // Reverse lookup: OrbisNpOnlineId to local user_id.
// Scans m_np_ids for a matching handle.data string. // Scans m_np_ids for a matching handle.data string.
// Returns -1 if no connected user has that Online ID. // Returns -1 if no connected user has that Online ID.
s32 GetUserIdByOnlineId(const OrbisNpOnlineId& online_id) const; s32 GetUserIdByOnlineId(const OrbisNpOnlineId& online_id) const;
// Friend list
u32 GetNumFriends(s32 user_id) const;
std::optional<std::string> GetFriendNpid(s32 user_id, u32 index) const;
// ---- Friend list / requests / blocked (shadNet) ----
struct FriendInfo {
std::string npid;
bool online = false;
};
struct FriendListSnapshot {
std::vector<FriendInfo> friends;
std::vector<std::string> requests_received;
std::vector<std::string> requests_sent;
std::vector<std::string> blocked;
};
// Local users that currently have an authenticated shadNet session.
std::vector<s32> 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<NpScore::ScoreRequestCtx> req);
s32 RecordGameData(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score,
const u8* data, size_t size, std::shared_ptr<NpScore::ScoreRequestCtx> 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<NpScore::ScoreRequestCtx> req);
s32 GetGameDataByAccountId(s32 user_id, s32 service_label, u32 boardId, u64 accountId, s32 pcId,
void* dataOut, u64 recvSize, u64* totalSizeOut,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 GetBoardInfo(s32 user_id, s32 service_label, u32 boardId,
NpScore::OrbisNpScoreBoardInfo* boardInfo,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// Submit a GetRankingByNpId request to the shadNet server.
s32 GetRankingByNpId(s32 user_id, s32 service_label, u32 boardId,
const std::vector<std::string>& npIds, const std::vector<s32>& pcIds,
NpScore::OrbisNpScorePlayerRankData* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> 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<NpScore::ScoreRequestCtx> 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<NpScore::ScoreRequestCtx> req);
s32 GetRankingByAccountId(s32 user_id, s32 service_label, u32 boardId,
const std::vector<u64>& accountIds, const std::vector<s32>& pcIds,
NpScore::OrbisNpScorePlayerRankDataA* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> 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<NpScore::ScoreRequestCtx> 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<NpScore::ScoreRequestCtx> req);
// State callbacks
using StateCallback = std::function<void(Libraries::UserService::OrbisUserServiceUserId user_id,
NpManager::OrbisNpState state)>;
s32 RegisterStateCallback(StateCallback cb, void* userdata);
void UnregisterStateCallback(s32 handle);
private: private:
NpHandler() = default; NpHandler() = default;
~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<std::string, u16> 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<u8>& 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<s32, std::shared_ptr<ShadNet::ShadNetClient>> m_clients;
// Per-user NP ID built once from shadnet_npid after login.
std::map<s32, OrbisNpId> m_np_ids;
// Score requests awaiting a reply, keyed by the submit packet id.
struct PendingScoreRequest {
std::shared_ptr<NpScore::ScoreRequestCtx> req;
ShadNet::CommandType cmd;
std::vector<std::string> 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<u64, PendingScoreRequest> m_pending_score;
// Worker thread
std::atomic<bool> m_initialized{false};
std::atomic<bool> 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<s32, ReconnectState> m_reconnect;
// State callbacks
struct CbEntry {
s32 handle;
StateCallback cb;
void* userdata;
};
mutable std::mutex m_mutex_cbs;
std::vector<CbEntry> 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<s32, FriendListSnapshot> m_friend_state;
}; };
} // namespace Libraries::Np } // namespace Libraries::Np

View File

@ -6,7 +6,7 @@
#include <deque> #include <deque>
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <variant> #include <vector>
#include <core/user_settings.h> #include <core/user_settings.h>
#include "common/elf_info.h" #include "common/elf_info.h"
@ -366,6 +366,39 @@ s32 PS4_SYSV_ABI sceNpAbortRequest(s32 req_id) {
return ORBIS_OK; 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<s32>(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) { s32 PS4_SYSV_ABI sceNpWaitAsync(s32 req_id, s32* result) {
if (result == nullptr) { if (result == nullptr) {
return ORBIS_NP_ERROR_INVALID_ARGUMENT; 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, s32 PS4_SYSV_ABI sceNpGetGamePresenceStatusA(Libraries::UserService::OrbisUserServiceUserId user_id,
OrbisNpGamePresenseStatus* game_status) { 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; return ORBIS_NP_ERROR_INVALID_ARGUMENT;
} }
*game_status = *game_status =
@ -529,65 +563,78 @@ s32 PS4_SYSV_ABI sceNpGetAccountId(OrbisNpOnlineId* online_id, u64* account_id)
if (online_id == nullptr || account_id == nullptr) { if (online_id == nullptr || account_id == nullptr) {
return ORBIS_NP_ERROR_INVALID_ARGUMENT; 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; *account_id = 0;
return ORBIS_NP_ERROR_SIGNED_OUT; return ORBIS_NP_ERROR_SIGNED_OUT;
} }
*account_id = 0xFEEDFACE; *account_id = Libraries::Np::NpHandler::GetInstance().GetAccountId(user_id);
return ORBIS_OK; return ORBIS_OK;
} }
s32 PS4_SYSV_ABI sceNpGetAccountIdA(Libraries::UserService::OrbisUserServiceUserId user_id, s32 PS4_SYSV_ABI sceNpGetAccountIdA(Libraries::UserService::OrbisUserServiceUserId user_id,
u64* account_id) { u64* account_id) {
LOG_DEBUG(Lib_NpManager, "user_id {}", user_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; return ORBIS_NP_ERROR_INVALID_ARGUMENT;
} }
if (UserManagement.GetUserByID(user_id) == nullptr) { if (!g_shadnet_enabled || !Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id)) {
*account_id = 0;
return ORBIS_NP_ERROR_USER_NOT_FOUND;
}
if (!g_shadnet_enabled) {
*account_id = 0; *account_id = 0;
return ORBIS_NP_ERROR_SIGNED_OUT; return ORBIS_NP_ERROR_SIGNED_OUT;
} }
*account_id = 0xFEEDFACE; *account_id = Libraries::Np::NpHandler::GetInstance().GetAccountId(user_id);
return ORBIS_OK; return ORBIS_OK;
} }
s32 PS4_SYSV_ABI sceNpGetNpId(Libraries::UserService::OrbisUserServiceUserId user_id, s32 PS4_SYSV_ABI sceNpGetNpId(Libraries::UserService::OrbisUserServiceUserId user_id,
OrbisNpId* np_id) { OrbisNpId* np_id) {
LOG_DEBUG(Lib_NpManager, "user_id {}", user_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) { if (np_id == nullptr) {
return ORBIS_NP_ERROR_INVALID_ARGUMENT; return ORBIS_NP_ERROR_INVALID_ARGUMENT;
} }
const auto* user = UserManagement.GetUserByID(user_id); if (!g_shadnet_enabled || !Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id)) {
if (user == nullptr) { LOG_WARNING(Lib_NpManager,
return ORBIS_NP_ERROR_USER_NOT_FOUND; "sceNpGetNpId: SIGNED_OUT (user_id={} shadnet_enabled={} signed_in={})",
} user_id, g_shadnet_enabled,
if (!g_shadnet_enabled) { Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id));
return ORBIS_NP_ERROR_SIGNED_OUT; return ORBIS_NP_ERROR_SIGNED_OUT;
} }
memset(np_id, 0, sizeof(OrbisNpId)); *np_id = Libraries::Np::NpHandler::GetInstance().GetNpId(user_id);
strncpy(np_id->handle.data, user->user_name.c_str(), sizeof(np_id->handle.data) - 1); 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; return ORBIS_OK;
} }
s32 PS4_SYSV_ABI sceNpGetOnlineId(Libraries::UserService::OrbisUserServiceUserId user_id, s32 PS4_SYSV_ABI sceNpGetOnlineId(Libraries::UserService::OrbisUserServiceUserId user_id,
OrbisNpOnlineId* online_id) { OrbisNpOnlineId* online_id) {
LOG_DEBUG(Lib_NpManager, "user_id {}", user_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) { if (online_id == nullptr) {
LOG_ERROR(Lib_NpManager, "invalid argument: online_id is null");
return ORBIS_NP_ERROR_INVALID_ARGUMENT; return ORBIS_NP_ERROR_INVALID_ARGUMENT;
} }
const auto* user = UserManagement.GetUserByID(user_id); if (!g_shadnet_enabled || !Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id)) {
if (user == nullptr) { LOG_INFO(Lib_NpManager,
return ORBIS_NP_ERROR_USER_NOT_FOUND; "sceNpGetOnlineId: SIGNED_OUT (user_id={} shadnet_enabled={} signed_in={})",
} user_id, g_shadnet_enabled,
if (!g_shadnet_enabled) { Libraries::Np::NpHandler::GetInstance().IsPsnSignedIn(user_id));
return ORBIS_NP_ERROR_SIGNED_OUT; return ORBIS_NP_ERROR_SIGNED_OUT;
} }
memset(online_id, 0, sizeof(OrbisNpOnlineId)); *online_id = Libraries::Np::NpHandler::GetInstance().GetOnlineId(user_id);
strncpy(online_id->data, user->user_name.c_str(), sizeof(online_id->data) - 1);
return ORBIS_OK; return ORBIS_OK;
} }
@ -596,10 +643,6 @@ s32 PS4_SYSV_ABI sceNpGetNpReachabilityState(Libraries::UserService::OrbisUserSe
if (state == nullptr) { if (state == nullptr) {
return ORBIS_NP_ERROR_INVALID_ARGUMENT; 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 (user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) {
if (g_firmware_version < 0 || g_firmware_version >= Common::ElfInfo::FW_400) { if (g_firmware_version < 0 || g_firmware_version >= Common::ElfInfo::FW_400) {
return ORBIS_NP_ERROR_INVALID_ARGUMENT; return ORBIS_NP_ERROR_INVALID_ARGUMENT;
@ -616,9 +659,6 @@ s32 PS4_SYSV_ABI sceNpGetState(Libraries::UserService::OrbisUserServiceUserId us
if (state == nullptr) { if (state == nullptr) {
return ORBIS_NP_ERROR_INVALID_ARGUMENT; 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 (user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) {
if (g_firmware_version < 0 || g_firmware_version >= Common::ElfInfo::FW_900) { if (g_firmware_version < 0 || g_firmware_version >= Common::ElfInfo::FW_900) {
return ORBIS_NP_ERROR_INVALID_ARGUMENT; return ORBIS_NP_ERROR_INVALID_ARGUMENT;
@ -639,14 +679,47 @@ s32 PS4_SYSV_ABI sceNpGetState(Libraries::UserService::OrbisUserServiceUserId us
s32 PS4_SYSV_ABI s32 PS4_SYSV_ABI
sceNpGetUserIdByAccountId(u64 account_id, Libraries::UserService::OrbisUserServiceUserId* user_id) { 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; return ORBIS_NP_ERROR_INVALID_ARGUMENT;
} }
if (!g_shadnet_enabled) { if (!g_shadnet_enabled)
return ORBIS_NP_ERROR_SIGNED_OUT; 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; if (!g_shadnet_enabled)
LOG_DEBUG(Lib_NpManager, "userid({}) = {}", account_id, *user_id); 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; return ORBIS_OK;
} }
@ -657,15 +730,15 @@ s32 PS4_SYSV_ABI sceNpHasSignedUp(Libraries::UserService::OrbisUserServiceUserId
return ORBIS_NP_ERROR_INVALID_ARGUMENT; return ORBIS_NP_ERROR_INVALID_ARGUMENT;
} }
*has_signed_up = false; *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 (user_id == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) {
if (g_firmware_version < 0 || g_firmware_version >= Common::ElfInfo::FW_900) { if (g_firmware_version < 0 || g_firmware_version >= Common::ElfInfo::FW_900) {
return ORBIS_NP_ERROR_INVALID_ARGUMENT; 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. // A user has signed up if they have a shadNet npid configured.
// This is independent of shadnet_enabled and current connection state. // This is independent of shadnet_enabled and current connection state.
*has_signed_up = !u->shadnet_npid.empty(); *has_signed_up = !u->shadnet_npid.empty();
@ -721,6 +794,18 @@ struct NpStateCallbackAEntry {
static std::array<NpStateCallbackAEntry, ORBIS_NP_STATE_CALLBACK_MAX> g_np_state_callbacks{}; static std::array<NpStateCallbackAEntry, ORBIS_NP_STATE_CALLBACK_MAX> 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<Libraries::UserService::OrbisUserServiceUserId, OrbisNpReachabilityState>
g_np_reachability_last;
struct PendingNpStateEvent { struct PendingNpStateEvent {
Libraries::UserService::OrbisUserServiceUserId user_id; Libraries::UserService::OrbisUserServiceUserId user_id;
OrbisNpState state; OrbisNpState state;
@ -732,18 +817,13 @@ static std::deque<PendingNpStateEvent> g_np_state_events;
static void QueueNpStateEvent(Libraries::UserService::OrbisUserServiceUserId user_id, static void QueueNpStateEvent(Libraries::UserService::OrbisUserServiceUserId user_id,
OrbisNpState state) { OrbisNpState state) {
const auto* user = UserManagement.GetUserByID(user_id);
if (user == nullptr) {
return;
}
PendingNpStateEvent event{}; PendingNpStateEvent event{};
event.user_id = user_id; event.user_id = user_id;
event.state = state; event.state = state;
event.has_np_id = state == OrbisNpState::SignedIn; event.has_np_id = state == OrbisNpState::SignedIn;
if (event.has_np_id) { if (event.has_np_id) {
std::strncpy(event.np_id.handle.data, user->user_name.c_str(), // NpId is built from the user's shadnet_npid at login; GetNpId returns by value.
sizeof(event.np_id.handle.data) - 1); event.np_id = Libraries::Np::NpHandler::GetInstance().GetNpId(user_id);
} }
std::scoped_lock lk{g_np_state_events_mutex}; std::scoped_lock lk{g_np_state_events_mutex};
@ -754,11 +834,20 @@ void NotifyNpStateFromUserServiceEvent(Libraries::UserService::OrbisUserServiceE
Libraries::UserService::OrbisUserServiceUserId user_id) { Libraries::UserService::OrbisUserServiceUserId user_id) {
switch (event_type) { switch (event_type) {
case Libraries::UserService::OrbisUserServiceEventType::Login: case Libraries::UserService::OrbisUserServiceEventType::Login:
QueueNpStateEvent(user_id, if (g_shadnet_enabled) {
g_shadnet_enabled ? OrbisNpState::SignedIn : OrbisNpState::SignedOut); // 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; break;
case Libraries::UserService::OrbisUserServiceEventType::Logout: 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; break;
default: default:
break; break;
@ -814,7 +903,11 @@ static s32 UnregisterStateCallbackAById(s32 callback_id) {
static void DispatchPendingNpStateCallbacks() { static void DispatchPendingNpStateCallbacks() {
std::deque<PendingNpStateEvent> pending_events; std::deque<PendingNpStateEvent> pending_events;
LegacyNpStateCallback legacy_callback{}; LegacyNpStateCallback legacy_callback{};
NpStateCallbackForNpToolkit toolkit_callback{};
NpReachabilityStateCallback reachability_callback{};
std::array<NpStateCallbackAEntry, ORBIS_NP_STATE_CALLBACK_MAX> callbacks; std::array<NpStateCallbackAEntry, ORBIS_NP_STATE_CALLBACK_MAX> callbacks;
std::vector<std::pair<Libraries::UserService::OrbisUserServiceUserId, OrbisNpReachabilityState>>
reachability_changes;
{ {
std::scoped_lock lk{g_np_state_events_mutex, g_np_state_callbacks_mutex}; std::scoped_lock lk{g_np_state_events_mutex, g_np_state_callbacks_mutex};
if (g_np_state_events.empty()) { if (g_np_state_events.empty()) {
@ -823,6 +916,21 @@ static void DispatchPendingNpStateCallbacks() {
pending_events.swap(g_np_state_events); pending_events.swap(g_np_state_events);
legacy_callback = LegacyNpStateCb; legacy_callback = LegacyNpStateCb;
callbacks = g_np_state_callbacks; 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) { for (auto& event : pending_events) {
@ -842,22 +950,25 @@ static void DispatchPendingNpStateCallbacks() {
} }
} }
if (NpStateCbForNp.func != nullptr) { if (toolkit_callback.func != nullptr) {
NpStateCbForNp.func(event.user_id, event.state, NpStateCbForNp.userdata); 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() { s32 PS4_SYSV_ABI sceNpCheckCallback() {
LOG_DEBUG(Lib_NpManager, "(STUBBED) called"); LOG_DEBUG(Lib_NpManager, "called");
DispatchPendingNpStateCallbacks(); DispatchPendingNpStateCallbacks();
std::scoped_lock lk{g_np_callbacks_mutex}; std::scoped_lock lk{g_np_callbacks_mutex};
for (auto& [key, cb] : g_np_callbacks) {
for (auto i : g_np_callbacks) { cb();
(i.second)();
} }
return ORBIS_OK; return ORBIS_OK;
} }
@ -883,18 +994,13 @@ s32 PS4_SYSV_ABI sceNpRegisterStateCallback(OrbisNpStateCallback callback, void*
return ORBIS_OK; return ORBIS_OK;
} }
s32 PS4_SYSV_ABI sceNpUnregisterStateCallback(s32 callback_id) { s32 PS4_SYSV_ABI sceNpUnregisterStateCallback() {
LOG_INFO(Lib_NpManager, "called, callback_id = {}", callback_id);
if (callback_id != 0) {
return ORBIS_NP_ERROR_INVALID_ARGUMENT;
}
std::scoped_lock lk{g_np_state_callbacks_mutex}; std::scoped_lock lk{g_np_state_callbacks_mutex};
if (LegacyNpStateCb.func == nullptr) { if (LegacyNpStateCb.func == nullptr) {
return ORBIS_NP_ERROR_CALLBACK_NOT_REGISTERED; return ORBIS_NP_ERROR_CALLBACK_NOT_REGISTERED;
} }
LegacyNpStateCb.func = nullptr;
LegacyNpStateCb = {}; LegacyNpStateCb.userdata = nullptr;
return ORBIS_OK; return ORBIS_OK;
} }
@ -908,59 +1014,191 @@ s32 PS4_SYSV_ABI sceNpUnregisterStateCallbackA(s32 callback_id) {
return UnregisterStateCallbackAById(callback_id); return UnregisterStateCallbackAById(callback_id);
} }
struct NpReachabilityStateCallback {
OrbisNpReachabilityStateCallback func;
void* userdata;
};
NpReachabilityStateCallback NpReachabilityCb;
s32 PS4_SYSV_ABI sceNpRegisterNpReachabilityStateCallback(OrbisNpReachabilityStateCallback callback, s32 PS4_SYSV_ABI sceNpRegisterNpReachabilityStateCallback(OrbisNpReachabilityStateCallback callback,
void* userdata) { void* userdata) {
if (callback == nullptr) { if (callback == nullptr) {
LOG_ERROR(Lib_NpManager, "callback is nullptr");
return ORBIS_NP_ERROR_INVALID_ARGUMENT; return ORBIS_NP_ERROR_INVALID_ARGUMENT;
} }
std::scoped_lock lk{g_np_state_callbacks_mutex};
if (NpReachabilityCb.func != nullptr) { if (NpReachabilityCb.func != nullptr) {
LOG_ERROR(Lib_NpManager, "callback already registered, cannot register multiple");
return ORBIS_NP_ERROR_CALLBACK_ALREADY_REGISTERED; return ORBIS_NP_ERROR_CALLBACK_ALREADY_REGISTERED;
} }
LOG_INFO(Lib_NpManager, "called"); LOG_INFO(Lib_NpManager, "called");
NpReachabilityCb.func = callback; NpReachabilityCb.func = callback;
NpReachabilityCb.userdata = userdata; NpReachabilityCb.userdata = userdata;
// Reset the per-user cache so the next state transition reports fresh.
g_np_reachability_last.clear();
return ORBIS_OK; return ORBIS_OK;
} }
s32 PS4_SYSV_ABI sceNpUnregisterNpReachabilityStateCallback() { s32 PS4_SYSV_ABI sceNpUnregisterNpReachabilityStateCallback() {
std::scoped_lock lk{g_np_state_callbacks_mutex};
if (NpReachabilityCb.func == nullptr) { if (NpReachabilityCb.func == nullptr) {
LOG_ERROR(Lib_NpManager, "callback not registered");
return ORBIS_NP_ERROR_CALLBACK_NOT_REGISTERED; return ORBIS_NP_ERROR_CALLBACK_NOT_REGISTERED;
} }
NpReachabilityCb.func = nullptr;
NpReachabilityCb = {}; NpReachabilityCb.userdata = nullptr;
g_np_reachability_last.clear();
return ORBIS_OK; return ORBIS_OK;
} }
s32 PS4_SYSV_ABI sceNpRegisterStateCallbackForToolkit(OrbisNpStateCallbackForNpToolkit callback, s32 PS4_SYSV_ABI sceNpRegisterStateCallbackForToolkit(OrbisNpStateCallbackForNpToolkit callback,
void* userdata) { void* userdata) {
static s32 id = 0;
LOG_ERROR(Lib_NpManager, "(STUBBED) called"); LOG_ERROR(Lib_NpManager, "(STUBBED) called");
std::scoped_lock lk{g_np_state_callbacks_mutex};
NpStateCbForNp.func = callback; NpStateCbForNp.func = callback;
NpStateCbForNp.userdata = userdata; 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<GamePresenceCallbackEntry, ORBIS_NP_STATE_CALLBACK_MAX> 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<s32>(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<void()> cb) { void RegisterNpCallback(std::string key, std::function<void()> cb) {
std::scoped_lock lk{g_np_callbacks_mutex}; std::scoped_lock lk{g_np_callbacks_mutex};
LOG_DEBUG(Lib_NpManager, "registering callback processing for {}", key); LOG_DEBUG(Lib_NpManager, "registering callback processing for {}", key);
g_np_callbacks.emplace(key, cb); g_np_callbacks.emplace(key, cb);
} }
void DeregisterNpCallback(std::string key) { void DeregisterNpCallback(std::string key) {
std::scoped_lock lk{g_np_callbacks_mutex}; std::scoped_lock lk{g_np_callbacks_mutex};
LOG_DEBUG(Lib_NpManager, "deregistering callback processing for {}", key); LOG_DEBUG(Lib_NpManager, "deregistering callback processing for {}", key);
g_np_callbacks.erase(key); g_np_callbacks.erase(key);
} }
@ -969,6 +1207,16 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
"Failed to get compiled SDK version."); "Failed to get compiled SDK version.");
g_shadnet_enabled = EmulatorSettings.IsShadNetEnabled(); 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("GpLQDNKICac", "libSceNpManager", 1, "libSceNpManager", sceNpCreateRequest);
LIB_FUNCTION("eiqMCt9UshI", "libSceNpManager", 1, "libSceNpManager", sceNpCreateAsyncRequest); LIB_FUNCTION("eiqMCt9UshI", "libSceNpManager", 1, "libSceNpManager", sceNpCreateAsyncRequest);
LIB_FUNCTION("2rsFmlGWleQ", "libSceNpManager", 1, "libSceNpManager", sceNpCheckNpAvailability); LIB_FUNCTION("2rsFmlGWleQ", "libSceNpManager", 1, "libSceNpManager", sceNpCheckNpAvailability);
@ -982,6 +1230,7 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("m9L3O6yst-U", "libSceNpManager", 1, "libSceNpManager", LIB_FUNCTION("m9L3O6yst-U", "libSceNpManager", 1, "libSceNpManager",
sceNpGetParentalControlInfoA); sceNpGetParentalControlInfoA);
LIB_FUNCTION("OzKvTvg3ZYU", "libSceNpManager", 1, "libSceNpManager", sceNpAbortRequest); LIB_FUNCTION("OzKvTvg3ZYU", "libSceNpManager", 1, "libSceNpManager", sceNpAbortRequest);
LIB_FUNCTION("-QglDeRr8D8", "libSceNpManager", 1, "libSceNpManager", sceNpSetTimeout);
LIB_FUNCTION("jyi5p9XWUSs", "libSceNpManager", 1, "libSceNpManager", sceNpWaitAsync); LIB_FUNCTION("jyi5p9XWUSs", "libSceNpManager", 1, "libSceNpManager", sceNpWaitAsync);
LIB_FUNCTION("uqcPJLWL08M", "libSceNpManager", 1, "libSceNpManager", sceNpPollAsync); LIB_FUNCTION("uqcPJLWL08M", "libSceNpManager", 1, "libSceNpManager", sceNpPollAsync);
LIB_FUNCTION("S7QTn72PrDw", "libSceNpManager", 1, "libSceNpManager", sceNpDeleteRequest); LIB_FUNCTION("S7QTn72PrDw", "libSceNpManager", 1, "libSceNpManager", sceNpDeleteRequest);
@ -996,6 +1245,21 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
sceNpGetGamePresenceStatus); sceNpGetGamePresenceStatus);
LIB_FUNCTION("oPO9U42YpgI", "libSceNpManager", 1, "libSceNpManager", LIB_FUNCTION("oPO9U42YpgI", "libSceNpManager", 1, "libSceNpManager",
sceNpGetGamePresenceStatusA); 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", LIB_FUNCTION("e-ZuhGEoeC4", "libSceNpManager", 1, "libSceNpManager",
sceNpGetNpReachabilityState); sceNpGetNpReachabilityState);
@ -1005,11 +1269,11 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("XDncXQIJUSk", "libSceNpManager", 1, "libSceNpManager", sceNpGetOnlineId); LIB_FUNCTION("XDncXQIJUSk", "libSceNpManager", 1, "libSceNpManager", sceNpGetOnlineId);
LIB_FUNCTION("eQH7nWPcAgc", "libSceNpManager", 1, "libSceNpManager", sceNpGetState); LIB_FUNCTION("eQH7nWPcAgc", "libSceNpManager", 1, "libSceNpManager", sceNpGetState);
LIB_FUNCTION("VgYczPGB5ss", "libSceNpManager", 1, "libSceNpManager", sceNpGetUserIdByAccountId); 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", LIB_FUNCTION("A2CQ3kgSopQ", "libSceNpManager", 1, "libSceNpManager",
sceNpSetContentRestriction); sceNpSetContentRestriction);
LIB_FUNCTION("Ec63y59l9tw", "libSceNpManager", 1, "libSceNpManager", sceNpSetNpTitleId); LIB_FUNCTION("Ec63y59l9tw", "libSceNpManager", 1, "libSceNpManager", sceNpSetNpTitleId);
LIB_FUNCTION("Oad3rvY-NJQ", "libSceNpManager", 1, "libSceNpManager", sceNpHasSignedUp);
LIB_FUNCTION("3Zl8BePTh9Y", "libSceNpManager", 1, "libSceNpManager", sceNpCheckCallback); LIB_FUNCTION("3Zl8BePTh9Y", "libSceNpManager", 1, "libSceNpManager", sceNpCheckCallback);
LIB_FUNCTION("JELHf4xPufo", "libSceNpManager", 1, "libSceNpManager", sceNpCheckCallbackForLib); LIB_FUNCTION("JELHf4xPufo", "libSceNpManager", 1, "libSceNpManager", sceNpCheckCallbackForLib);
LIB_FUNCTION("VfRSmPmj8Q8", "libSceNpManager", 1, "libSceNpManager", LIB_FUNCTION("VfRSmPmj8Q8", "libSceNpManager", 1, "libSceNpManager",
@ -1028,6 +1292,8 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
sceNpCheckCallbackForLib); sceNpCheckCallbackForLib);
LIB_FUNCTION("0c7HbXRKUt4", "libSceNpManagerForToolkit", 1, "libSceNpManager", LIB_FUNCTION("0c7HbXRKUt4", "libSceNpManagerForToolkit", 1, "libSceNpManager",
sceNpRegisterStateCallbackForToolkit); sceNpRegisterStateCallbackForToolkit);
LIB_FUNCTION("YIvqqvJyjEc", "libSceNpManagerForToolkit", 1, "libSceNpManager",
sceNpUnregisterStateCallbackForToolkit);
LIB_FUNCTION("2rsFmlGWleQ", "libSceNpManagerCompat", 1, "libSceNpManager", LIB_FUNCTION("2rsFmlGWleQ", "libSceNpManagerCompat", 1, "libSceNpManager",
sceNpCheckNpAvailability); sceNpCheckNpAvailability);

File diff suppressed because it is too large Load Diff

View File

@ -161,9 +161,9 @@ struct OrbisNpScorePlayerRankDataForCrossSave {
//*********************************** //***********************************
// Title context management functions // Title context management functions
//*********************************** //***********************************
s32 PS4_SYSV_ABI sceNpScoreCreateNpTitleCtx(OrbisNpServiceLabel serviceLabel, int PS4_SYSV_ABI sceNpScoreCreateNpTitleCtx(OrbisNpServiceLabel serviceLabel,
const OrbisNpId* selfNpId); const OrbisNpId* selfNpId);
s32 PS4_SYSV_ABI sceNpScoreCreateNpTitleCtxA(OrbisNpServiceLabel npServiceLabel, int PS4_SYSV_ABI sceNpScoreCreateNpTitleCtxA(OrbisNpServiceLabel npServiceLabel,
UserService::OrbisUserServiceUserId selfId); UserService::OrbisUserServiceUserId selfId);
s32 PS4_SYSV_ABI sceNpScoreDeleteNpTitleCtx(s32 titleCtxId); s32 PS4_SYSV_ABI sceNpScoreDeleteNpTitleCtx(s32 titleCtxId);
//*********************************** //***********************************

View File

@ -315,7 +315,7 @@ s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValueLength(s64 requestId, cons
} }
s32 PS4_SYSV_ABI sceNpWebApiGetHttpStatusCode(s64 requestId, s32* out_status_code) { 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 // On newer SDKs, NULL output pointer is invalid
if (getCompiledSdkVersion() > Common::ElfInfo::FW_100 && out_status_code == nullptr) if (getCompiledSdkVersion() > Common::ElfInfo::FW_100 && out_status_code == nullptr)
return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; 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) { s32 PS4_SYSV_ABI sceNpWebApiReadData(s64 requestId, void* pData, u64 size) {
LOG_ERROR(Lib_NpWebApi, "called : requestId = {:#x}, pData = {}, size = {:#x}", requestId, LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}, pData = {}, size = {:#x}", requestId,
fmt::ptr(pData), size); fmt::ptr(pData), size);
if (pData == nullptr || size == 0) if (pData == nullptr || size == 0)
return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT;

View File

@ -8,6 +8,7 @@
#include "core/libraries/kernel/time.h" #include "core/libraries/kernel/time.h"
#include "core/libraries/network/http.h" #include "core/libraries/network/http.h"
#include "core/libraries/np/np_error.h" #include "core/libraries/np/np_error.h"
#include "core/libraries/np/np_handler.h"
#include "np_web_api_internal.h" #include "np_web_api_internal.h"
namespace Libraries::Np::NpWebApi { 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; return ORBIS_NP_WEBAPI_ERROR_NOT_SIGNED_IN;
} }
LOG_ERROR(Lib_NpWebApi, if (request->http_request_id == 0) {
"(STUBBED) called, requestId = {:#x}, pApiGroup = '{}', pPath = '{}', pContentType = " std::string base_url = EmulatorSettings.GetShadNetWebApiServer();
"'{}', method = {}, multipart = {}", // sceHttpCreateConnectionWithURL expects a template id, not the raw libhttp
requestId, request->userApiGroup, request->userPath, request->userContentType, // context id that NpWebApi was initialized with. Create a template from the
magic_enum::enum_name(request->userMethod), request->multipart); // 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<int>(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); releaseRequest(request);
releaseUserContext(user_context); releaseUserContext(user_context);
@ -709,6 +819,18 @@ s32 deleteRequest(s64 requestId) {
} }
releaseRequest(request); 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); user_context->requests.erase(request->requestId);
releaseUserContext(user_context); releaseUserContext(user_context);
@ -1374,7 +1496,7 @@ s32 unregisterExtdPushEventCallback(s32 titleUserCtxId, s32 callbackId) {
s32 PS4_SYSV_ABI getHttpRequestIdFromRequest(OrbisNpWebApiRequest* request) 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) { s32 PS4_SYSV_ABI getHttpStatusCodeInternal(s64 requestId, s32* out_status_code) {

View File

@ -74,6 +74,9 @@ struct OrbisNpWebApiRequest {
u64 remainingData; u64 remainingData;
u32 readOffset; u32 readOffset;
char data[64]; char data[64];
s32 http_connection_id = 0;
s32 http_request_id = 0;
s32 http_template_id = 0;
}; };
struct OrbisNpWebApiHandle { struct OrbisNpWebApiHandle {

View File

@ -124,6 +124,8 @@ std::map<s32, std::string> ExtractTrophies(const std::filesystem::path& npbind_p
LOG_WARNING(Common_Filesystem, "No NPCommIDs in npbind.dat"); LOG_WARNING(Common_Filesystem, "No NPCommIDs in npbind.dat");
return trophy_index_map; return trophy_index_map;
} }
auto& game_info = Common::ElfInfo::Instance();
game_info.SetNpCommIds(np_comm_ids);
if (!std::filesystem::exists(trophy_dir)) { if (!std::filesystem::exists(trophy_dir)) {
LOG_WARNING(Common_Filesystem, "Game does not contain a trophy directory"); LOG_WARNING(Common_Filesystem, "Game does not contain a trophy directory");

208
src/imgui/friends_layer.cpp Normal file
View File

@ -0,0 +1,208 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstddef>
#include <string>
#include <vector>
#include <imgui.h>
#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<s32> users = np.GetConnectedUsers();
if (users.empty()) {
ImGui::TextUnformatted("No user is signed in to shadNet.");
return;
}
if (g_user_index >= static_cast<int>(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<int>(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<ImGui::ShadNetNotify::HistoryEntry> 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

16
src/imgui/friends_layer.h Normal file
View File

@ -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

View File

@ -0,0 +1,175 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <ctime>
#include <deque>
#include <mutex>
#include <vector>
#include <imgui.h>
#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<Toast> g_toasts;
std::deque<HistoryEntry> 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<HistoryEntry> 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<Toast> 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

View File

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstddef>
#include <string>
#include <vector>
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<HistoryEntry> 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

View File

@ -182,6 +182,7 @@ std::filesystem::path GetInputConfigFile(const std::string& game_id) {
{"hotkey_volume_up", "kpplus"}, {"hotkey_volume_up", "kpplus"},
{"hotkey_volume_down", "kpminus"}, {"hotkey_volume_down", "kpminus"},
{"hotkey_emulator_settings", "f3"}, {"hotkey_emulator_settings", "f3"},
{"hotkey_toggle_friends", "f2"},
}; };
std::string legacy_capture_binding; std::string legacy_capture_binding;
bool legacy_capture_binding_found = false; bool legacy_capture_binding_found = false;
@ -805,6 +806,9 @@ void ControllerOutput::FinalizeUpdate(u8 gamepad_index) {
case HOTKEY_OPEN_EMULATOR_SETTINGS: case HOTKEY_OPEN_EMULATOR_SETTINGS:
ImGuiEmuSettings::OpenInGameSettingsDialog(); ImGuiEmuSettings::OpenInGameSettingsDialog();
break; break;
case HOTKEY_TOGGLE_FRIENDS:
PushSDLEvent(SDL_EVENT_TOGGLE_FRIENDS);
break;
case KEY_TOGGLE: case KEY_TOGGLE:
// noop // noop
break; break;

View File

@ -42,6 +42,7 @@
#define SDL_EVENT_REMOVE_VIRTUAL_USER SDL_EVENT_USER + 12 #define SDL_EVENT_REMOVE_VIRTUAL_USER SDL_EVENT_USER + 12
#define SDL_EVENT_RDOC_CAPTURE SDL_EVENT_USER + 13 #define SDL_EVENT_RDOC_CAPTURE SDL_EVENT_USER + 13
#define SDL_EVENT_SCREENSHOT_WITH_OVERLAYS SDL_EVENT_USER + 14 #define SDL_EVENT_SCREENSHOT_WITH_OVERLAYS SDL_EVENT_USER + 14
#define SDL_EVENT_TOGGLE_FRIENDS SDL_EVENT_USER + 15
#define LEFTJOYSTICK_HALFMODE 0x00010000 #define LEFTJOYSTICK_HALFMODE 0x00010000
#define RIGHTJOYSTICK_HALFMODE 0x00020000 #define RIGHTJOYSTICK_HALFMODE 0x00020000
@ -65,6 +66,7 @@
#define HOTKEY_REMOVE_VIRTUAL_USER 0xf000000d #define HOTKEY_REMOVE_VIRTUAL_USER 0xf000000d
#define HOTKEY_SCREENSHOT_WITH_OVERLAYS 0xf000000e #define HOTKEY_SCREENSHOT_WITH_OVERLAYS 0xf000000e
#define HOTKEY_OPEN_EMULATOR_SETTINGS 0xf000000f #define HOTKEY_OPEN_EMULATOR_SETTINGS 0xf000000f
#define HOTKEY_TOGGLE_FRIENDS 0xf0000010
#define SDL_UNMAPPED UINT32_MAX - 1 #define SDL_UNMAPPED UINT32_MAX - 1
@ -172,6 +174,7 @@ const std::map<std::string, u32> string_to_hotkey_map = {
{"hotkey_volume_up", HOTKEY_VOLUME_UP}, {"hotkey_volume_up", HOTKEY_VOLUME_UP},
{"hotkey_volume_down", HOTKEY_VOLUME_DOWN}, {"hotkey_volume_down", HOTKEY_VOLUME_DOWN},
{"hotkey_emulator_settings", HOTKEY_OPEN_EMULATOR_SETTINGS}, {"hotkey_emulator_settings", HOTKEY_OPEN_EMULATOR_SETTINGS},
{"hotkey_toggle_friends", HOTKEY_TOGGLE_FRIENDS},
}; };
const std::map<std::string, AxisMapping> string_to_axis_map = { const std::map<std::string, AxisMapping> string_to_axis_map = {
@ -537,7 +540,7 @@ public:
class ControllerAllOutputs { class ControllerAllOutputs {
public: public:
static constexpr u64 output_count = 42; static constexpr u64 output_count = 43;
std::array<ControllerOutput, output_count> data = { std::array<ControllerOutput, output_count> data = {
// Important: these have to be the first, or else they will update in the wrong order // Important: these have to be the first, or else they will update in the wrong order
ControllerOutput(LEFTJOYSTICK_HALFMODE), ControllerOutput(LEFTJOYSTICK_HALFMODE),
@ -591,6 +594,7 @@ public:
ControllerOutput(HOTKEY_VOLUME_UP), ControllerOutput(HOTKEY_VOLUME_UP),
ControllerOutput(HOTKEY_VOLUME_DOWN), ControllerOutput(HOTKEY_VOLUME_DOWN),
ControllerOutput(HOTKEY_OPEN_EMULATOR_SETTINGS), ControllerOutput(HOTKEY_OPEN_EMULATOR_SETTINGS),
ControllerOutput(HOTKEY_TOGGLE_FRIENDS),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID), ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID),
}; };

View File

@ -22,6 +22,7 @@
#include "core/libraries/pad/pad.h" #include "core/libraries/pad/pad.h"
#include "core/libraries/system/userservice.h" #include "core/libraries/system/userservice.h"
#include "core/user_settings.h" #include "core/user_settings.h"
#include "imgui/friends_layer.h"
#include "imgui/renderer/imgui_core.h" #include "imgui/renderer/imgui_core.h"
#include "input/controller.h" #include "input/controller.h"
#include "input/input_handler.h" #include "input/input_handler.h"
@ -288,6 +289,9 @@ void WindowSDL::WaitEvent() {
case SDL_EVENT_TOGGLE_SIMPLE_FPS: case SDL_EVENT_TOGGLE_SIMPLE_FPS:
Overlay::ToggleSimpleFps(); Overlay::ToggleSimpleFps();
break; break;
case SDL_EVENT_TOGGLE_FRIENDS:
ImGui::Friends::Toggle();
break;
case SDL_EVENT_RELOAD_INPUTS: case SDL_EVENT_RELOAD_INPUTS:
Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial())); Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial()));
break; break;

751
src/shadnet/client.cpp Normal file
View File

@ -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 <algorithm>
#include <chrono>
#include <thread>
#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 <typename T>
static std::vector<u8> MakeProtoPayload(const T& msg) {
const std::string serialised = msg.SerializeAsString();
const u32 len = static_cast<u32>(serialised.size());
std::vector<u8> out(4);
out[0] = static_cast<u8>(len);
out[1] = static_cast<u8>(len >> 8);
out[2] = static_cast<u8>(len >> 16);
out[3] = static_cast<u8>(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<u8>& p, int pos) {
if (pos + 4 > static_cast<int>(p.size()))
return {};
const u32 len = GetLE32(p.data() + pos);
pos += 4;
if (pos + static_cast<int>(len) > static_cast<int>(p.size()))
return {};
return std::string(reinterpret_cast<const char*>(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<u32>(m_friends.size());
}
std::optional<std::string> 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<u32>(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<PacketType>(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<u8> payload;
const u32 payload_sz = total_sz - static_cast<u32>(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<std::vector<u8>> 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<int>(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<long>(SHAD_CONNECT_TIMEOUT_MS / 1000),
static_cast<long>((SHAD_CONNECT_TIMEOUT_MS % 1000) * 1000)};
if (::select(static_cast<int>(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<char*>(&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<struct sockaddr*>(&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<DWORD>(SHAD_CONNECT_TIMEOUT_MS);
#else
struct timeval so_rcv{static_cast<long>(SHAD_CONNECT_TIMEOUT_MS / 1000),
static_cast<long>((SHAD_CONNECT_TIMEOUT_MS % 1000) * 1000)};
#endif
::setsockopt(m_sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&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<PacketType>(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<u8> 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<const char*>(&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<int>(::recv(m_sock, reinterpret_cast<char*>(buf + received),
static_cast<int>(n - received), 0));
if (r <= 0)
return false;
received += static_cast<u32>(r);
}
return true;
}
bool ShadNetClient::SendAll(const std::vector<u8>& data) {
std::lock_guard lock(m_mutex_send_direct);
int sent = 0;
const int total = static_cast<int>(data.size());
while (sent < total) {
const int r = static_cast<int>(
::send(m_sock, reinterpret_cast<const char*>(data.data() + sent), total - sent, 0));
if (r < 0) {
LOG_ERROR(ShadNet, "send() failed");
return false;
}
sent += r;
}
return true;
}
std::vector<u8> ShadNetClient::BuildPacket(CommandType cmd, u64 id,
const std::vector<u8>& payload) const {
const u32 total = static_cast<u32>(SHAD_HEADER_SIZE + payload.size());
std::vector<u8> out(SHAD_HEADER_SIZE);
out[0] = static_cast<u8>(PacketType::Request);
PutLE16(out, 1, static_cast<u16>(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<u8>& 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<u8>& payload) {
switch (type) {
case PacketType::Reply:
switch (static_cast<CommandType>(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<ErrorType>(payload[0]);
std::vector<u8> body;
if (payload.size() > 1) {
body.assign(payload.begin() + 1, payload.end());
}
onAsyncReply(static_cast<CommandType>(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<u8>& payload) {
LoginResult res;
if (payload.empty()) {
res.error = ErrorType::Malformed;
LOG_ERROR(ShadNet, "Empty Login reply");
} else {
res.error = static_cast<ErrorType>(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<u8> empty_payload;
std::vector<u8> 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<u8>(res.error));
m_sem_authenticated.release();
DoDisconnect();
}
}
if (onLoginResult)
onLoginResult(res);
}
void ShadNetClient::HandleGetTokenReply(const std::vector<u8>& payload) {
if (payload.empty()) {
LOG_ERROR(ShadNet, "Empty GetToken reply");
m_sem_authenticated.release();
return;
}
const ErrorType err = static_cast<ErrorType>(payload[0]);
if (err != ErrorType::NoError) {
LOG_WARNING(ShadNet, "GetToken returned error={} — WebAPI calls will be unauthenticated",
static_cast<int>(err));
m_sem_authenticated.release();
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<u8>& 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<NotificationType>(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<u8>& b, size_t off, u16 v) {
b[off] = static_cast<u8>(v);
b[off + 1] = static_cast<u8>(v >> 8);
}
void ShadNetClient::PutLE32(std::vector<u8>& b, size_t off, u32 v) {
b[off] = static_cast<u8>(v);
b[off + 1] = static_cast<u8>(v >> 8);
b[off + 2] = static_cast<u8>(v >> 16);
b[off + 3] = static_cast<u8>(v >> 24);
}
void ShadNetClient::PutLE64(std::vector<u8>& b, size_t off, u64 v) {
for (int i = 0; i < 8; ++i)
b[off + i] = static_cast<u8>(v >> (8 * i));
}
u16 ShadNetClient::GetLE16(const u8* p) {
return static_cast<u16>(p[0]) | (static_cast<u16>(p[1]) << 8);
}
u32 ShadNetClient::GetLE32(const u8* p) {
return static_cast<u32>(p[0]) | (static_cast<u32>(p[1]) << 8) | (static_cast<u32>(p[2]) << 16) |
(static_cast<u32>(p[3]) << 24);
}
u64 ShadNetClient::GetLE64(const u8* p) {
u64 v = 0;
for (int i = 0; i < 8; ++i)
v |= static_cast<u64>(p[i]) << (8 * i);
return v;
}
} // namespace ShadNet

298
src/shadnet/client.h Normal file
View File

@ -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 <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <optional>
#include <semaphore>
#include <string>
#include <thread>
#include <vector>
#include "common/types.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
using ShadSocketHandle = SOCKET;
static constexpr ShadSocketHandle SHAD_INVALID_SOCK = INVALID_SOCKET;
#define SHAD_CLOSE(s) ::closesocket(s)
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
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<FriendEntry> friends;
std::vector<std::string> requestsSent;
std::vector<std::string> requestsReceived;
std::vector<std::string> 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<std::string> GetFriendNpid(u32 index) const;
std::string GetBearerToken() const;
// Callbacks
std::function<void(const LoginResult&)> onLoginResult;
std::function<void(const NotifyFriendQuery&)> onFriendQuery;
std::function<void(const NotifyFriendNew&)> onFriendNew;
std::function<void(const NotifyFriendLost&)> onFriendLost;
std::function<void(const NotifyFriendStatus&)> 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<void(CommandType cmd, u64 pkt_id, ErrorType error, const std::vector<u8>& 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<u8>& 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<u8>& data);
std::vector<u8> BuildPacket(CommandType cmd, u64 id, const std::vector<u8>& payload) const;
void DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id, const std::vector<u8>& payload);
void HandleLoginReply(const std::vector<u8>& payload);
void HandleGetTokenReply(const std::vector<u8>& payload);
void HandleNotification(u16 cmd_raw, const std::vector<u8>& payload);
// Helper: read a u32-LE-prefixed proto blob from a byte vector at pos.
static std::string ExtractBlob(const std::vector<u8>& p, int pos);
static void PutLE16(std::vector<u8>& b, size_t off, u16 v);
static void PutLE32(std::vector<u8>& b, size_t off, u32 v);
static void PutLE64(std::vector<u8>& 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<bool> m_terminate{false};
std::atomic<bool> m_connected{false};
std::atomic<bool> m_authenticated{false};
std::atomic<ShadNetState> 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<std::vector<u8>> 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<u32> m_addr_local{0};
mutable std::mutex m_mutex_friends;
std::vector<FriendEntry> m_friends;
std::atomic<u64> m_pkt_counter{1};
};
} // namespace ShadNet

180
src/shadnet/shadnet.proto Normal file
View File

@ -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;
}

View File

@ -10,9 +10,11 @@
#include "core/devtools/layer.h" #include "core/devtools/layer.h"
#include "core/emulator_settings.h" #include "core/emulator_settings.h"
#include "core/libraries/system/systemservice.h" #include "core/libraries/system/systemservice.h"
#include "imgui/friends_layer.h"
#include "imgui/notifications_layer.h" #include "imgui/notifications_layer.h"
#include "imgui/renderer/imgui_core.h" #include "imgui/renderer/imgui_core.h"
#include "imgui/renderer/imgui_impl_vulkan.h" #include "imgui/renderer/imgui_impl_vulkan.h"
#include "imgui/shadnet_notifications_layer.h"
#include "sdl_window.h" #include "sdl_window.h"
#include "video_core/buffer_cache/buffer.h" #include "video_core/buffer_cache/buffer.h"
#include "video_core/renderdoc.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); pp_pass.Create(device, swapchain.GetSurfaceFormat().format);
ImGui::Layer::AddLayer(Common::Singleton<Core::Devtools::Layer>::Instance()); ImGui::Layer::AddLayer(Common::Singleton<Core::Devtools::Layer>::Instance());
ImGui::Friends::Register();
ImGui::ShadNetNotify::Register();
} }
Presenter::~Presenter() { Presenter::~Presenter() {
ImGui::ShadNetNotify::Unregister();
ImGui::Friends::Unregister();
ImGui::Layer::RemoveLayer(Common::Singleton<Core::Devtools::Layer>::Instance()); ImGui::Layer::RemoveLayer(Common::Singleton<Core::Devtools::Layer>::Instance());
draw_scheduler.Finish(); draw_scheduler.Finish();