mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-07-10 01:44:42 -06:00
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:
parent
8651fd78d8
commit
aff387e8a9
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -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
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
12
externals/CMakeLists.txt
vendored
12
externals/CMakeLists.txt
vendored
@ -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
1
externals/protobuf
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 5917ee224a7e47383a78acc129628511fe210dff
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
@ -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
|
||||||
@ -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
@ -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);
|
||||||
//***********************************
|
//***********************************
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
208
src/imgui/friends_layer.cpp
Normal 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
16
src/imgui/friends_layer.h
Normal 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
|
||||||
175
src/imgui/shadnet_notifications_layer.cpp
Normal file
175
src/imgui/shadnet_notifications_layer.cpp
Normal 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
|
||||||
52
src/imgui/shadnet_notifications_layer.h
Normal file
52
src/imgui/shadnet_notifications_layer.h
Normal 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
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
751
src/shadnet/client.cpp
Normal 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
298
src/shadnet/client.h
Normal 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
180
src/shadnet/shadnet.proto
Normal 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;
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user