mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-07-09 17:25:37 -06:00
* 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
1865 lines
77 KiB
C++
1865 lines
77 KiB
C++
// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project
|
|
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include "common/elf_info.h"
|
|
#include "common/logging/log.h"
|
|
#include "core/emulator_settings.h"
|
|
#include "core/libraries/np/np_error.h"
|
|
#include "core/libraries/np/np_manager.h"
|
|
#include "core/libraries/np/np_score/np_score.h"
|
|
#include "core/user_settings.h"
|
|
#include "imgui/shadnet_notifications_layer.h"
|
|
#include "np_handler.h"
|
|
#include "shadnet.pb.h"
|
|
|
|
namespace Libraries::Np {
|
|
|
|
NpHandler& NpHandler::GetInstance() {
|
|
static NpHandler s_instance;
|
|
return s_instance;
|
|
}
|
|
|
|
std::pair<std::string, u16> NpHandler::ParseServerAddress() const {
|
|
const std::string server_str = EmulatorSettings.GetShadNetServer();
|
|
u16 port = 31313; // default port
|
|
if (server_str.empty()) {
|
|
LOG_ERROR(NpHandler,
|
|
"shadNet server address is empty,set the shadNet server field in settings. "
|
|
"Connection will fail.");
|
|
return {std::string{}, port};
|
|
}
|
|
std::string host = server_str;
|
|
const auto colon = server_str.rfind(':');
|
|
if (colon != std::string::npos) {
|
|
host = server_str.substr(0, colon);
|
|
const std::string port_str = server_str.substr(colon + 1);
|
|
try {
|
|
port = static_cast<u16>(std::stoi(port_str));
|
|
} catch (const std::exception&) {
|
|
LOG_WARNING(NpHandler,
|
|
"shadNet server port '{}' is not a valid number; using default {}",
|
|
port_str, port);
|
|
}
|
|
}
|
|
return {host, port};
|
|
}
|
|
|
|
bool NpHandler::ConnectUserById(s32 user_id) {
|
|
if (!EmulatorSettings.IsShadNetEnabled())
|
|
return false;
|
|
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
if (m_clients.find(user_id) != m_clients.end())
|
|
return false; // already connected
|
|
}
|
|
|
|
const User* u = UserManagement.GetUserByID(user_id);
|
|
if (!u)
|
|
return false;
|
|
if (!u->shadnet_enabled) {
|
|
LOG_DEBUG(NpHandler, "user_id={} ('{}') shadNet disabled,skipping", u->user_id,
|
|
u->user_name);
|
|
return false;
|
|
}
|
|
if (u->shadnet_npid.empty() || u->shadnet_password.empty()) {
|
|
LOG_WARNING(NpHandler, "user_id={} ('{}') shadNet enabled but credentials missing,skipping",
|
|
u->user_id, u->user_name);
|
|
return false;
|
|
}
|
|
|
|
const auto [host, port] = ParseServerAddress();
|
|
return ConnectUser(u->user_id, host, port, u->shadnet_npid, u->shadnet_password,
|
|
u->shadnet_token);
|
|
}
|
|
|
|
void NpHandler::StartWorker() {
|
|
bool expected = false;
|
|
if (m_worker_running.compare_exchange_strong(expected, true)) {
|
|
m_worker_thread = std::thread(&NpHandler::WorkerThread, this);
|
|
}
|
|
}
|
|
|
|
void NpHandler::Initialize() {
|
|
if (m_initialized.exchange(true)) {
|
|
LOG_WARNING(NpHandler, "Initialize called more than once");
|
|
return;
|
|
}
|
|
|
|
if (!EmulatorSettings.IsShadNetEnabled()) {
|
|
LOG_INFO(NpHandler, "shadNet disabled globally we are in offline mode");
|
|
return;
|
|
}
|
|
|
|
const auto logged_in = UserManagement.GetLoggedInUsers(); // get all login users
|
|
int connected_count = 0;
|
|
for (int i = 0; i < Libraries::UserService::ORBIS_USER_SERVICE_MAX_LOGIN_USERS; ++i) {
|
|
const User* u = logged_in[i];
|
|
if (!u)
|
|
continue;
|
|
if (ConnectUserById(u->user_id))
|
|
++connected_count;
|
|
}
|
|
|
|
if (connected_count == 0) {
|
|
LOG_WARNING(NpHandler, "no users connected to shadNet");
|
|
return;
|
|
}
|
|
|
|
StartWorker();
|
|
}
|
|
|
|
void NpHandler::Shutdown() {
|
|
if (!m_initialized.exchange(false))
|
|
return;
|
|
|
|
m_worker_running = false;
|
|
|
|
// Stop any pending reconnect retries.
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
m_reconnect.clear();
|
|
}
|
|
|
|
// Collect user IDs to disconnect (avoid holding m_mutex_clients during Stop)
|
|
std::vector<s32> ids;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
for (auto& [uid, _] : m_clients)
|
|
ids.push_back(uid);
|
|
}
|
|
for (s32 uid : ids)
|
|
DisconnectUser(uid);
|
|
|
|
if (m_worker_thread.joinable())
|
|
m_worker_thread.join();
|
|
|
|
LOG_INFO(NpHandler, "Shutdown complete");
|
|
}
|
|
|
|
bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, const std::string& npid,
|
|
const std::string& password, const std::string& token) {
|
|
LOG_INFO(NpHandler, "Connecting user_id={} npid='{}' to {}:{} (timeout {}s)", user_id, npid,
|
|
host, port, ShadNet::SHAD_CONNECT_TIMEOUT_MS / 1000);
|
|
|
|
auto client = std::make_shared<ShadNet::ShadNetClient>();
|
|
|
|
// Wire per-user notification callbacks
|
|
client->onFriendQuery = [this, user_id](const ShadNet::NotifyFriendQuery& n) {
|
|
OnFriendQuery(user_id, n);
|
|
};
|
|
client->onFriendNew = [this, user_id](const ShadNet::NotifyFriendNew& n) {
|
|
OnFriendNew(user_id, n);
|
|
};
|
|
client->onFriendLost = [this, user_id](const ShadNet::NotifyFriendLost& n) {
|
|
OnFriendLost(user_id, n);
|
|
};
|
|
client->onFriendStatus = [this, user_id](const ShadNet::NotifyFriendStatus& n) {
|
|
OnFriendStatus(user_id, n);
|
|
};
|
|
client->onAsyncReply = [this, user_id](ShadNet::CommandType cmd, u64 pkt_id,
|
|
ShadNet::ErrorType err, const std::vector<u8>& body) {
|
|
OnScoreReply(user_id, cmd, pkt_id, err, body);
|
|
};
|
|
client->onLoginResult = [this, user_id](const ShadNet::LoginResult& res) {
|
|
OnLoginResult(user_id, res);
|
|
};
|
|
|
|
client->Start(host, port, npid, password, token);
|
|
|
|
const ShadNet::ShadNetState conn_state = client->WaitForConnection();
|
|
if (conn_state != ShadNet::ShadNetState::Ok) {
|
|
LOG_ERROR(NpHandler, "user_id={} connection failed (state={})", user_id,
|
|
static_cast<int>(conn_state));
|
|
client->Stop();
|
|
return false;
|
|
}
|
|
|
|
const ShadNet::ShadNetState auth_state = client->WaitForAuthenticated();
|
|
if (auth_state != ShadNet::ShadNetState::Ok) {
|
|
LOG_ERROR(NpHandler, "user_id={} authentication failed (state={})", user_id,
|
|
static_cast<int>(auth_state));
|
|
client->Stop();
|
|
return false;
|
|
}
|
|
|
|
LOG_INFO(NpHandler, "user_id={} signed in npid='{}' accountId={}", user_id, npid,
|
|
client->GetUserId());
|
|
|
|
// Build OrbisNpId
|
|
{
|
|
OrbisNpId np_id{};
|
|
strncpy(np_id.handle.data, npid.c_str(), sizeof(np_id.handle.data) - 1);
|
|
std::lock_guard lock(m_mutex_clients);
|
|
m_np_ids[user_id] = np_id;
|
|
m_clients[user_id] = std::move(client);
|
|
}
|
|
|
|
FireStateCallback(user_id, NpManager::OrbisNpState::SignedIn);
|
|
return true;
|
|
}
|
|
|
|
void NpHandler::DisconnectUser(s32 user_id) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end())
|
|
return;
|
|
client = std::move(it->second);
|
|
m_clients.erase(it);
|
|
}
|
|
{
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
m_friend_state.erase(user_id);
|
|
}
|
|
client->Stop();
|
|
FireStateCallback(user_id, NpManager::OrbisNpState::SignedOut);
|
|
LOG_INFO(NpHandler, "user_id={} disconnected", user_id);
|
|
}
|
|
|
|
void NpHandler::OnUserLoggedIn(s32 user_id) {
|
|
if (ConnectUserById(user_id))
|
|
StartWorker();
|
|
}
|
|
|
|
void NpHandler::OnUserLoggedOut(s32 user_id) {
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
m_reconnect.erase(user_id); // do not auto-reconnect
|
|
}
|
|
DisconnectUser(user_id);
|
|
}
|
|
|
|
void NpHandler::WorkerThread() {
|
|
constexpr auto INTERVAL = std::chrono::milliseconds(500);
|
|
|
|
while (m_worker_running) {
|
|
std::this_thread::sleep_for(INTERVAL);
|
|
|
|
// Collect ids of dropped clients
|
|
std::vector<s32> dropped;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
for (auto& [uid, client] : m_clients) {
|
|
if (!client->IsConnected())
|
|
dropped.push_back(uid);
|
|
}
|
|
}
|
|
|
|
for (s32 uid : dropped) {
|
|
LOG_WARNING(NpHandler, "user_id={} connection dropped (network); will retry", uid);
|
|
DisconnectUser(uid); // reports SignedOut + stops/removes the dead client
|
|
MarkForReconnect(uid); // schedule transparent reconnect (network drop, not logout)
|
|
}
|
|
|
|
TryReconnect();
|
|
}
|
|
}
|
|
|
|
void NpHandler::MarkForReconnect(s32 user_id) {
|
|
if (!EmulatorSettings.IsShadNetEnabled())
|
|
return; // offline mode: nothing to reconnect to
|
|
constexpr auto kInitialBackoff = std::chrono::milliseconds(2000);
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto& st = m_reconnect[user_id];
|
|
st.backoff = kInitialBackoff;
|
|
st.next_attempt = std::chrono::steady_clock::now() + st.backoff;
|
|
}
|
|
|
|
void NpHandler::TryReconnect() {
|
|
constexpr auto kMaxBackoff = std::chrono::milliseconds(30000);
|
|
const auto now = std::chrono::steady_clock::now();
|
|
|
|
std::vector<s32> due;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
for (auto& [uid, st] : m_reconnect) {
|
|
if (m_clients.count(uid) || now >= st.next_attempt)
|
|
due.push_back(uid);
|
|
}
|
|
}
|
|
|
|
for (s32 uid : due) {
|
|
if (!m_worker_running)
|
|
return;
|
|
if (!EmulatorSettings.IsShadNetEnabled()) {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
m_reconnect.erase(uid);
|
|
continue;
|
|
}
|
|
// ConnectUserById locks m_mutex_clients internally; call it unlocked.
|
|
const bool ok = ConnectUserById(uid);
|
|
std::lock_guard lock(m_mutex_clients);
|
|
if (ok || m_clients.count(uid)) {
|
|
m_reconnect.erase(uid);
|
|
LOG_INFO(NpHandler, "user_id={} reconnected to shadNet", uid);
|
|
} else if (auto it = m_reconnect.find(uid); it != m_reconnect.end()) {
|
|
it->second.backoff = std::min(it->second.backoff * 2, kMaxBackoff);
|
|
it->second.next_attempt = std::chrono::steady_clock::now() + it->second.backoff;
|
|
LOG_DEBUG(NpHandler, "user_id={} reconnect failed; next attempt in {}ms", uid,
|
|
it->second.backoff.count());
|
|
}
|
|
}
|
|
}
|
|
|
|
bool NpHandler::IsPsnSignedIn(s32 user_id) const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
return it != m_clients.end() && it->second->IsAuthenticated();
|
|
}
|
|
|
|
bool NpHandler::IsAnySignedIn() const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
for (auto& [_, client] : m_clients)
|
|
if (client->IsAuthenticated())
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
OrbisNpId NpHandler::GetNpId(s32 user_id) const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_np_ids.find(user_id);
|
|
return it != m_np_ids.end() ? it->second : OrbisNpId{};
|
|
}
|
|
|
|
OrbisNpOnlineId NpHandler::GetOnlineId(s32 user_id) const {
|
|
return GetNpId(user_id).handle;
|
|
}
|
|
|
|
std::string NpHandler::GetAvatarUrl(s32 user_id) const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
return it != m_clients.end() ? it->second->GetAvatarUrl() : std::string{};
|
|
}
|
|
|
|
OrbisNpAccountId NpHandler::GetAccountId(s32 user_id) const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
return it != m_clients.end() ? static_cast<OrbisNpAccountId>(it->second->GetUserId()) : 0;
|
|
}
|
|
|
|
std::string NpHandler::GetBearerToken(s32 user_id) const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
return it != m_clients.end() ? it->second->GetBearerToken() : std::string{};
|
|
}
|
|
|
|
u32 NpHandler::GetLocalIpAddr(s32 user_id) const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
return it != m_clients.end() ? it->second->GetAddrLocal() : 0;
|
|
}
|
|
|
|
s32 NpHandler::GetUserIdByAccountId(u64 account_id) const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
for (auto& [uid, client] : m_clients) {
|
|
if (static_cast<u64>(client->GetUserId()) == account_id)
|
|
return uid;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
s32 NpHandler::GetUserIdByOnlineId(const OrbisNpOnlineId& online_id) const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
for (auto& [uid, np_id] : m_np_ids) {
|
|
if (strncmp(np_id.handle.data, online_id.data, ORBIS_NP_ONLINEID_MAX_LENGTH) == 0)
|
|
return uid;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// friends calls
|
|
u32 NpHandler::GetNumFriends(s32 user_id) const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
return it != m_clients.end() ? it->second->GetNumFriends() : 0;
|
|
}
|
|
|
|
std::optional<std::string> NpHandler::GetFriendNpid(s32 user_id, u32 index) const {
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
return it != m_clients.end() ? it->second->GetFriendNpid(index) : std::nullopt;
|
|
}
|
|
|
|
void NpHandler::OnFriendQuery(s32 user_id, const ShadNet::NotifyFriendQuery& n) {
|
|
LOG_INFO(NpHandler, "user_id={} FriendQuery from '{}'", user_id, n.fromNpid);
|
|
ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendRequest,
|
|
"Friend request from " + n.fromNpid);
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
auto& st = m_friend_state[user_id];
|
|
if (std::find(st.requests_received.begin(), st.requests_received.end(), n.fromNpid) ==
|
|
st.requests_received.end()) {
|
|
st.requests_received.push_back(n.fromNpid);
|
|
}
|
|
}
|
|
|
|
void NpHandler::OnFriendNew(s32 user_id, const ShadNet::NotifyFriendNew& n) {
|
|
LOG_INFO(NpHandler, "user_id={} FriendNew '{}' ({})", user_id, n.npid,
|
|
n.online ? "online" : "offline");
|
|
ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendNew,
|
|
n.npid + " is now your friend");
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
auto& st = m_friend_state[user_id];
|
|
auto it = std::find_if(st.friends.begin(), st.friends.end(),
|
|
[&](const FriendInfo& f) { return f.npid == n.npid; });
|
|
if (it == st.friends.end()) {
|
|
st.friends.push_back({n.npid, n.online});
|
|
} else {
|
|
it->online = n.online;
|
|
}
|
|
const auto drop = [&](std::vector<std::string>& v) {
|
|
v.erase(std::remove(v.begin(), v.end(), n.npid), v.end());
|
|
};
|
|
drop(st.requests_received);
|
|
drop(st.requests_sent);
|
|
}
|
|
|
|
void NpHandler::OnFriendLost(s32 user_id, const ShadNet::NotifyFriendLost& n) {
|
|
LOG_INFO(NpHandler, "user_id={} FriendLost '{}'", user_id, n.npid);
|
|
ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendLost,
|
|
n.npid + " removed you as a friend");
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
auto& st = m_friend_state[user_id];
|
|
st.friends.erase(std::remove_if(st.friends.begin(), st.friends.end(),
|
|
[&](const FriendInfo& f) { return f.npid == n.npid; }),
|
|
st.friends.end());
|
|
}
|
|
|
|
void NpHandler::OnFriendStatus(s32 user_id, const ShadNet::NotifyFriendStatus& n) {
|
|
LOG_DEBUG(NpHandler, "user_id={} FriendStatus '{}' is {}", user_id, n.npid,
|
|
n.online ? "online" : "offline");
|
|
if (n.online) {
|
|
ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::Online, n.npid + " is online");
|
|
}
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
auto& st = m_friend_state[user_id];
|
|
auto it = std::find_if(st.friends.begin(), st.friends.end(),
|
|
[&](const FriendInfo& f) { return f.npid == n.npid; });
|
|
if (it != st.friends.end()) {
|
|
it->online = n.online;
|
|
} else {
|
|
st.friends.push_back({n.npid, n.online});
|
|
}
|
|
}
|
|
|
|
void NpHandler::OnLoginResult(s32 user_id, const ShadNet::LoginResult& res) {
|
|
if (res.error != ShadNet::ErrorType::NoError) {
|
|
return;
|
|
}
|
|
FriendListSnapshot snap;
|
|
snap.friends.reserve(res.friends.size());
|
|
for (const auto& f : res.friends) {
|
|
snap.friends.push_back({f.npid, f.online});
|
|
}
|
|
snap.requests_sent = res.requestsSent;
|
|
snap.requests_received = res.requestsReceived;
|
|
snap.blocked = res.blocked;
|
|
{
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
m_friend_state[user_id] = std::move(snap);
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"user_id={} friend state seeded: {} friends, {} received, {} sent, {} blocked",
|
|
user_id, res.friends.size(), res.requestsReceived.size(), res.requestsSent.size(),
|
|
res.blocked.size());
|
|
|
|
// Surface friend requests that arrived while offline (no live notification fires for these).
|
|
if (!res.requestsReceived.empty()) {
|
|
ImGui::ShadNetNotify::Push(ImGui::ShadNetNotify::Kind::FriendRequest,
|
|
std::to_string(res.requestsReceived.size()) +
|
|
" pending friend request(s)");
|
|
}
|
|
}
|
|
|
|
std::vector<s32> NpHandler::GetConnectedUsers() const {
|
|
std::vector<s32> out;
|
|
std::lock_guard lock(m_mutex_clients);
|
|
out.reserve(m_clients.size());
|
|
for (const auto& [uid, c] : m_clients) {
|
|
out.push_back(uid);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
NpHandler::FriendListSnapshot NpHandler::GetFriendList(s32 user_id) const {
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
const auto it = m_friend_state.find(user_id);
|
|
return it == m_friend_state.end() ? FriendListSnapshot{} : it->second;
|
|
}
|
|
|
|
s32 NpHandler::SendFriendRequest(s32 user_id, const std::string& npid) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
const auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client->AddFriend(npid);
|
|
LOG_INFO(NpHandler, "user_id={} AddFriend '{}'", user_id, npid);
|
|
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
auto& st = m_friend_state[user_id];
|
|
const bool accepting = std::find(st.requests_received.begin(), st.requests_received.end(),
|
|
npid) != st.requests_received.end();
|
|
if (!accepting && std::find(st.requests_sent.begin(), st.requests_sent.end(), npid) ==
|
|
st.requests_sent.end()) {
|
|
st.requests_sent.push_back(npid);
|
|
}
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::RemoveFriend(s32 user_id, const std::string& npid) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
const auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client->RemoveFriend(npid);
|
|
LOG_INFO(NpHandler, "user_id={} RemoveFriend '{}'", user_id, npid);
|
|
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
auto& st = m_friend_state[user_id];
|
|
st.friends.erase(std::remove_if(st.friends.begin(), st.friends.end(),
|
|
[&](const FriendInfo& f) { return f.npid == npid; }),
|
|
st.friends.end());
|
|
const auto drop = [&](std::vector<std::string>& v) {
|
|
v.erase(std::remove(v.begin(), v.end(), npid), v.end());
|
|
};
|
|
drop(st.requests_received);
|
|
drop(st.requests_sent);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::BlockUser(s32 user_id, const std::string& npid) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
const auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client->AddBlock(npid);
|
|
LOG_INFO(NpHandler, "user_id={} AddBlock '{}'", user_id, npid);
|
|
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
auto& st = m_friend_state[user_id];
|
|
if (std::find(st.blocked.begin(), st.blocked.end(), npid) == st.blocked.end()) {
|
|
st.blocked.push_back(npid);
|
|
}
|
|
st.friends.erase(std::remove_if(st.friends.begin(), st.friends.end(),
|
|
[&](const FriendInfo& f) { return f.npid == npid; }),
|
|
st.friends.end());
|
|
const auto drop = [&](std::vector<std::string>& v) {
|
|
v.erase(std::remove(v.begin(), v.end(), npid), v.end());
|
|
};
|
|
drop(st.requests_received);
|
|
drop(st.requests_sent);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::UnblockUser(s32 user_id, const std::string& npid) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
const auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client->RemoveBlock(npid);
|
|
LOG_INFO(NpHandler, "user_id={} RemoveBlock '{}'", user_id, npid);
|
|
|
|
std::lock_guard lock(m_mutex_friend_state);
|
|
auto& st = m_friend_state[user_id];
|
|
st.blocked.erase(std::remove(st.blocked.begin(), st.blocked.end(), npid), st.blocked.end());
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
// state callbacks
|
|
s32 NpHandler::RegisterStateCallback(StateCallback cb, void* userdata) {
|
|
std::lock_guard lock(m_mutex_cbs);
|
|
const s32 h = m_next_handle++;
|
|
m_state_cbs.push_back({h, std::move(cb), userdata});
|
|
return h;
|
|
}
|
|
|
|
void NpHandler::UnregisterStateCallback(s32 handle) {
|
|
std::lock_guard lock(m_mutex_cbs);
|
|
m_state_cbs.erase(std::remove_if(m_state_cbs.begin(), m_state_cbs.end(),
|
|
[handle](const CbEntry& e) { return e.handle == handle; }),
|
|
m_state_cbs.end());
|
|
}
|
|
|
|
void NpHandler::FireStateCallback(s32 user_id, NpManager::OrbisNpState state) {
|
|
std::lock_guard lock(m_mutex_cbs);
|
|
for (const auto& e : m_state_cbs) {
|
|
if (e.cb)
|
|
e.cb(user_id, state);
|
|
}
|
|
}
|
|
|
|
// Score
|
|
|
|
// A usable NP Communication ID is the 12-char "NPWRxxxxx_00" form. An empty or
|
|
// all-NUL buffer means npbind.dat was missing or unparsed,treat it as invalid
|
|
// so callers reject the request instead of sending a blank comm id on the wire.
|
|
bool IsValidNpCommId(const std::string& id) {
|
|
return !id.empty() && std::any_of(id.begin(), id.end(), [](char c) { return c != '\0'; });
|
|
}
|
|
|
|
std::string NpHandler::GetNpCommId(s32 service_label) const {
|
|
// TODO complete guess of how commid is mapping to service_label.
|
|
constexpr size_t COM_ID_LEN = 12;
|
|
const auto& ids = Common::ElfInfo::Instance().GetNpCommIds();
|
|
std::string com_id;
|
|
if (!ids.empty()) {
|
|
const size_t idx = (service_label >= 0 && static_cast<size_t>(service_label) < ids.size())
|
|
? static_cast<size_t>(service_label)
|
|
: 0;
|
|
com_id = ids[idx];
|
|
if (static_cast<size_t>(service_label) >= ids.size()) {
|
|
LOG_WARNING(NpHandler,
|
|
"GetNpCommId: service_label={} >= npbind entry count {} — falling back "
|
|
"to index 0",
|
|
service_label, ids.size());
|
|
}
|
|
}
|
|
if (com_id.size() < COM_ID_LEN) {
|
|
com_id.resize(COM_ID_LEN, '\0');
|
|
} else if (com_id.size() > COM_ID_LEN) {
|
|
com_id.resize(COM_ID_LEN);
|
|
}
|
|
return com_id;
|
|
}
|
|
|
|
s32 NpHandler::RecordScore(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score,
|
|
const char* comment, size_t commentLen, const u8* gameInfoData,
|
|
size_t gameInfoSize, std::shared_ptr<NpScore::ScoreRequestCtx> req) {
|
|
// Look up the user's shadNet session.
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "RecordScore: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
LOG_WARNING(NpHandler, "RecordScore: user_id={} not authenticated", user_id);
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
// Build RecordScoreRequest proto.
|
|
shadnet::RecordScoreRequest proto;
|
|
proto.set_boardid(boardId);
|
|
proto.set_pcid(pcId);
|
|
proto.set_score(score);
|
|
if (comment && commentLen > 0) {
|
|
proto.set_comment(std::string(comment, commentLen));
|
|
}
|
|
if (gameInfoData && gameInfoSize > 0) {
|
|
proto.set_data(std::string(reinterpret_cast<const char*>(gameInfoData), gameInfoSize));
|
|
}
|
|
|
|
const std::string proto_bytes = proto.SerializeAsString();
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4 + proto_bytes.size());
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
const u32 sz = static_cast<u32>(proto_bytes.size());
|
|
payload.push_back(static_cast<u8>(sz));
|
|
payload.push_back(static_cast<u8>(sz >> 8));
|
|
payload.push_back(static_cast<u8>(sz >> 16));
|
|
payload.push_back(static_cast<u8>(sz >> 24));
|
|
payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end());
|
|
|
|
const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::RecordScore, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::RecordScore;
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"RecordScore: user_id={} service_label={} board={} pcId={} score={} commentLen={} "
|
|
"gameInfoSize={} pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, pcId, score, commentLen, gameInfoSize, pkt_id,
|
|
com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::RecordGameData(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score,
|
|
const u8* data, size_t size,
|
|
std::shared_ptr<NpScore::ScoreRequestCtx> req) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "RecordGameData: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
LOG_WARNING(NpHandler, "RecordGameData: user_id={} not authenticated", user_id);
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
shadnet::RecordScoreGameDataRequest proto;
|
|
proto.set_boardid(boardId);
|
|
proto.set_pcid(pcId);
|
|
proto.set_score(score);
|
|
|
|
const std::string proto_bytes = proto.SerializeAsString();
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4 + proto_bytes.size() + size);
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
const u32 sz = static_cast<u32>(proto_bytes.size());
|
|
payload.push_back(static_cast<u8>(sz));
|
|
payload.push_back(static_cast<u8>(sz >> 8));
|
|
payload.push_back(static_cast<u8>(sz >> 16));
|
|
payload.push_back(static_cast<u8>(sz >> 24));
|
|
payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end());
|
|
if (data != nullptr && size > 0) {
|
|
payload.insert(payload.end(), data, data + size);
|
|
}
|
|
|
|
const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::RecordScoreData, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::RecordScoreData;
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"RecordGameData: user_id={} service_label={} board={} pcId={} score={} dataSize={} "
|
|
"pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, pcId, score, size, pkt_id, com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::GetGameData(s32 user_id, s32 service_label, u32 boardId, const std::string& npId,
|
|
s32 pcId, void* dataOut, u64 recvSize, u64* totalSizeOut,
|
|
std::shared_ptr<NpScore::ScoreRequestCtx> req) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "GetGameData: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
LOG_WARNING(NpHandler, "GetGameData: user_id={} not authenticated", user_id);
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
shadnet::GetScoreGameDataRequest proto;
|
|
proto.set_boardid(boardId);
|
|
proto.set_npid(npId);
|
|
proto.set_pcid(pcId);
|
|
|
|
const std::string proto_bytes = proto.SerializeAsString();
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4 + proto_bytes.size());
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
const u32 sz = static_cast<u32>(proto_bytes.size());
|
|
payload.push_back(static_cast<u8>(sz));
|
|
payload.push_back(static_cast<u8>(sz >> 8));
|
|
payload.push_back(static_cast<u8>(sz >> 16));
|
|
payload.push_back(static_cast<u8>(sz >> 24));
|
|
payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end());
|
|
|
|
const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreData, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::GetScoreData;
|
|
pending.dataOut = dataOut;
|
|
pending.recvSize = recvSize;
|
|
pending.totalSizeOut = totalSizeOut;
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"GetGameData: user_id={} service_label={} board={} npId='{}' pcId={} recvSize={} "
|
|
"pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, npId, pcId, recvSize, pkt_id, com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::GetGameDataByAccountId(s32 user_id, s32 service_label, u32 boardId, u64 accountId,
|
|
s32 pcId, void* dataOut, u64 recvSize, u64* totalSizeOut,
|
|
std::shared_ptr<NpScore::ScoreRequestCtx> req) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "GetGameDataByAccountId: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
LOG_WARNING(NpHandler, "GetGameDataByAccountId: user_id={} not authenticated", user_id);
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
shadnet::GetScoreGameDataByAccountIdRequest proto;
|
|
proto.set_boardid(boardId);
|
|
proto.set_accountid(accountId);
|
|
proto.set_pcid(pcId);
|
|
|
|
const std::string proto_bytes = proto.SerializeAsString();
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4 + proto_bytes.size());
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
const u32 sz = static_cast<u32>(proto_bytes.size());
|
|
payload.push_back(static_cast<u8>(sz));
|
|
payload.push_back(static_cast<u8>(sz >> 8));
|
|
payload.push_back(static_cast<u8>(sz >> 16));
|
|
payload.push_back(static_cast<u8>(sz >> 24));
|
|
payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end());
|
|
|
|
const u64 pkt_id =
|
|
client->SubmitRequest(ShadNet::CommandType::GetScoreGameDataByAccId, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::GetScoreGameDataByAccId;
|
|
pending.dataOut = dataOut;
|
|
pending.recvSize = recvSize;
|
|
pending.totalSizeOut = totalSizeOut;
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"GetGameDataByAccountId: user_id={} service_label={} board={} accountId={} pcId={} "
|
|
"recvSize={} pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, accountId, pcId, recvSize, pkt_id, com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::GetBoardInfo(s32 user_id, s32 service_label, u32 boardId,
|
|
NpScore::OrbisNpScoreBoardInfo* boardInfo,
|
|
std::shared_ptr<NpScore::ScoreRequestCtx> req) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "GetBoardInfo: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
LOG_WARNING(NpHandler, "GetBoardInfo: user_id={} not authenticated", user_id);
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4);
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
payload.push_back(static_cast<u8>(boardId));
|
|
payload.push_back(static_cast<u8>(boardId >> 8));
|
|
payload.push_back(static_cast<u8>(boardId >> 16));
|
|
payload.push_back(static_cast<u8>(boardId >> 24));
|
|
|
|
const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetBoardInfos, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::GetBoardInfos;
|
|
pending.boardInfo = boardInfo;
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler, "GetBoardInfo: user_id={} service_label={} board={} pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, pkt_id, com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::GetRankingByNpId(s32 user_id, s32 service_label, u32 boardId,
|
|
const std::vector<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) {
|
|
// pcIds must either be empty (use 0 for everything) or match npIds in size.
|
|
if (!pcIds.empty() && pcIds.size() != npIds.size()) {
|
|
LOG_ERROR(NpHandler, "GetRankingByNpId: pcIds size {} != npIds size {}", pcIds.size(),
|
|
npIds.size());
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
// Look up the user's session.
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "GetRankingByNpId: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
LOG_WARNING(NpHandler, "GetRankingByNpId: user_id={} not authenticated", user_id);
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
shadnet::GetScoreNpIdRequest proto;
|
|
proto.set_boardid(boardId);
|
|
proto.set_withcomment(commentArray != nullptr);
|
|
proto.set_withgameinfo(infoArray != nullptr);
|
|
for (size_t i = 0; i < npIds.size(); ++i) {
|
|
auto* entry = proto.add_npids();
|
|
entry->set_npid(npIds[i]);
|
|
entry->set_pcid(pcIds.empty() ? 0 : pcIds[i]);
|
|
}
|
|
|
|
const std::string proto_bytes = proto.SerializeAsString();
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4 + proto_bytes.size());
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
const u32 sz = static_cast<u32>(proto_bytes.size());
|
|
payload.push_back(static_cast<u8>(sz));
|
|
payload.push_back(static_cast<u8>(sz >> 8));
|
|
payload.push_back(static_cast<u8>(sz >> 16));
|
|
payload.push_back(static_cast<u8>(sz >> 24));
|
|
payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end());
|
|
|
|
const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreNpid, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::GetScoreNpid;
|
|
pending.requestedNpIds = npIds;
|
|
pending.rankArray = rankArray;
|
|
pending.commentArray = commentArray;
|
|
pending.infoArray = infoArray;
|
|
pending.lastSortDate = lastSortDate;
|
|
pending.totalRecord = totalRecord;
|
|
pending.arrayNum = npIds.size();
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"GetRankingByNpId: user_id={} service_label={} board={} npIdCount={} "
|
|
"withPcId={} withComment={} withGameInfo={} pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, npIds.size(), !pcIds.empty(), commentArray != nullptr,
|
|
infoArray != nullptr, pkt_id, com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::GetRankingByRange(s32 user_id, s32 service_label, u32 boardId, u32 startSerialRank,
|
|
u32 arrayNum, NpScore::OrbisNpScoreRankData* rankArray,
|
|
NpScore::OrbisNpScoreComment* commentArray,
|
|
NpScore::OrbisNpScoreGameInfo* infoArray,
|
|
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
|
|
std::shared_ptr<NpScore::ScoreRequestCtx> req) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "GetRankingByRange: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
LOG_WARNING(NpHandler, "GetRankingByRange: user_id={} not authenticated", user_id);
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
shadnet::GetScoreRangeRequest proto;
|
|
proto.set_boardid(boardId);
|
|
proto.set_startrank(startSerialRank);
|
|
proto.set_numranks(arrayNum);
|
|
proto.set_withcomment(commentArray != nullptr);
|
|
proto.set_withgameinfo(infoArray != nullptr);
|
|
|
|
const std::string proto_bytes = proto.SerializeAsString();
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4 + proto_bytes.size());
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
const u32 sz = static_cast<u32>(proto_bytes.size());
|
|
payload.push_back(static_cast<u8>(sz));
|
|
payload.push_back(static_cast<u8>(sz >> 8));
|
|
payload.push_back(static_cast<u8>(sz >> 16));
|
|
payload.push_back(static_cast<u8>(sz >> 24));
|
|
payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end());
|
|
|
|
const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreRange, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::GetScoreRange;
|
|
pending.plainRankArray = rankArray;
|
|
pending.commentArray = commentArray;
|
|
pending.infoArray = infoArray;
|
|
pending.lastSortDate = lastSortDate;
|
|
pending.totalRecord = totalRecord;
|
|
pending.arrayNum = arrayNum;
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"GetRankingByRange: user_id={} service_label={} board={} startRank={} numRanks={} "
|
|
"withComment={} withGameInfo={} pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, startSerialRank, arrayNum, commentArray != nullptr,
|
|
infoArray != nullptr, pkt_id, com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::GetRankingByRangeA(s32 user_id, s32 service_label, u32 boardId, u32 startSerialRank,
|
|
u32 arrayNum, NpScore::OrbisNpScoreRankDataA* rankArray,
|
|
NpScore::OrbisNpScoreComment* commentArray,
|
|
NpScore::OrbisNpScoreGameInfo* infoArray,
|
|
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
|
|
std::shared_ptr<NpScore::ScoreRequestCtx> req) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "GetRankingByRangeA: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
LOG_WARNING(NpHandler, "GetRankingByRangeA: user_id={} not authenticated", user_id);
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
shadnet::GetScoreRangeRequest proto;
|
|
proto.set_boardid(boardId);
|
|
proto.set_startrank(startSerialRank);
|
|
proto.set_numranks(arrayNum);
|
|
proto.set_withcomment(commentArray != nullptr);
|
|
proto.set_withgameinfo(infoArray != nullptr);
|
|
|
|
const std::string proto_bytes = proto.SerializeAsString();
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4 + proto_bytes.size());
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
const u32 sz = static_cast<u32>(proto_bytes.size());
|
|
payload.push_back(static_cast<u8>(sz));
|
|
payload.push_back(static_cast<u8>(sz >> 8));
|
|
payload.push_back(static_cast<u8>(sz >> 16));
|
|
payload.push_back(static_cast<u8>(sz >> 24));
|
|
payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end());
|
|
|
|
const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreRange, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::GetScoreRange;
|
|
// Note: aRankArray is set, plainRankArray remains null. The shared
|
|
// GetScoreRange/GetScoreFriends branch in OnScoreReply dispatches
|
|
// on which one is non-null to pick the right fill helper.
|
|
pending.aRankArray = rankArray;
|
|
pending.commentArray = commentArray;
|
|
pending.infoArray = infoArray;
|
|
pending.lastSortDate = lastSortDate;
|
|
pending.totalRecord = totalRecord;
|
|
pending.arrayNum = arrayNum;
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"GetRankingByRangeA: user_id={} service_label={} board={} startRank={} numRanks={} "
|
|
"withComment={} withGameInfo={} pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, startSerialRank, arrayNum, commentArray != nullptr,
|
|
infoArray != nullptr, pkt_id, com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::GetRankingByAccountId(s32 user_id, s32 service_label, u32 boardId,
|
|
const std::vector<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) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "GetRankingByAccountId: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
shadnet::GetScoreAccountIdRequest proto;
|
|
proto.set_boardid(boardId);
|
|
for (size_t i = 0; i < accountIds.size(); ++i) {
|
|
auto* entry = proto.add_ids();
|
|
entry->set_accountid(static_cast<int64_t>(accountIds[i]));
|
|
// If the caller provided per-entry pcIds, use them; otherwise all zero.
|
|
entry->set_pcid(i < pcIds.size() ? pcIds[i] : 0);
|
|
}
|
|
proto.set_withcomment(commentArray != nullptr);
|
|
proto.set_withgameinfo(infoArray != nullptr);
|
|
|
|
const std::string proto_bytes = proto.SerializeAsString();
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4 + proto_bytes.size());
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
const u32 sz = static_cast<u32>(proto_bytes.size());
|
|
payload.push_back(static_cast<u8>(sz));
|
|
payload.push_back(static_cast<u8>(sz >> 8));
|
|
payload.push_back(static_cast<u8>(sz >> 16));
|
|
payload.push_back(static_cast<u8>(sz >> 24));
|
|
payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end());
|
|
|
|
const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreAccountId, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::GetScoreAccountId;
|
|
pending.aPlayerRankArray = rankArray;
|
|
pending.commentArray = commentArray;
|
|
pending.infoArray = infoArray;
|
|
pending.lastSortDate = lastSortDate;
|
|
pending.totalRecord = totalRecord;
|
|
pending.arrayNum = accountIds.size();
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"GetRankingByAccountId: user_id={} service_label={} board={} accountIdCount={} "
|
|
"withComment={} withGameInfo={} pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, accountIds.size(), commentArray != nullptr,
|
|
infoArray != nullptr, pkt_id, com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::GetFriendsRanking(s32 user_id, s32 service_label, u32 boardId, bool includeSelf,
|
|
u32 arrayNum, NpScore::OrbisNpScoreRankData* rankArray,
|
|
NpScore::OrbisNpScoreComment* commentArray,
|
|
NpScore::OrbisNpScoreGameInfo* infoArray,
|
|
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
|
|
std::shared_ptr<NpScore::ScoreRequestCtx> req) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "GetFriendsRanking: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
LOG_WARNING(NpHandler, "GetFriendsRanking: user_id={} not authenticated", user_id);
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
shadnet::GetScoreFriendsRequest proto;
|
|
proto.set_boardid(boardId);
|
|
proto.set_includeself(includeSelf);
|
|
proto.set_max(arrayNum);
|
|
proto.set_withcomment(commentArray != nullptr);
|
|
proto.set_withgameinfo(infoArray != nullptr);
|
|
|
|
const std::string proto_bytes = proto.SerializeAsString();
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4 + proto_bytes.size());
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
const u32 sz = static_cast<u32>(proto_bytes.size());
|
|
payload.push_back(static_cast<u8>(sz));
|
|
payload.push_back(static_cast<u8>(sz >> 8));
|
|
payload.push_back(static_cast<u8>(sz >> 16));
|
|
payload.push_back(static_cast<u8>(sz >> 24));
|
|
payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end());
|
|
|
|
const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreFriends, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::GetScoreFriends;
|
|
pending.plainRankArray = rankArray;
|
|
pending.commentArray = commentArray;
|
|
pending.infoArray = infoArray;
|
|
pending.lastSortDate = lastSortDate;
|
|
pending.totalRecord = totalRecord;
|
|
pending.arrayNum = arrayNum;
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"GetFriendsRanking: user_id={} service_label={} board={} includeSelf={} "
|
|
"arrayNum={} withComment={} withGameInfo={} pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, includeSelf, arrayNum, commentArray != nullptr,
|
|
infoArray != nullptr, pkt_id, com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
s32 NpHandler::GetFriendsRankingA(s32 user_id, s32 service_label, u32 boardId, bool includeSelf,
|
|
u32 arrayNum, NpScore::OrbisNpScoreRankDataA* rankArray,
|
|
NpScore::OrbisNpScoreComment* commentArray,
|
|
NpScore::OrbisNpScoreGameInfo* infoArray,
|
|
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
|
|
std::shared_ptr<NpScore::ScoreRequestCtx> req) {
|
|
std::shared_ptr<ShadNet::ShadNetClient> client;
|
|
{
|
|
std::lock_guard lock(m_mutex_clients);
|
|
auto it = m_clients.find(user_id);
|
|
if (it == m_clients.end()) {
|
|
LOG_WARNING(NpHandler, "GetFriendsRankingA: user_id={} not connected", user_id);
|
|
return ORBIS_NP_ERROR_SIGNED_OUT;
|
|
}
|
|
client = it->second;
|
|
}
|
|
if (!client->IsAuthenticated()) {
|
|
LOG_WARNING(NpHandler, "GetFriendsRankingA: user_id={} not authenticated", user_id);
|
|
return ORBIS_NP_COMMUNITY_ERROR_NO_LOGIN;
|
|
}
|
|
|
|
shadnet::GetScoreFriendsRequest proto;
|
|
proto.set_boardid(boardId);
|
|
proto.set_includeself(includeSelf);
|
|
proto.set_max(arrayNum);
|
|
proto.set_withcomment(commentArray != nullptr);
|
|
proto.set_withgameinfo(infoArray != nullptr);
|
|
|
|
const std::string proto_bytes = proto.SerializeAsString();
|
|
const std::string com_id = GetNpCommId(service_label);
|
|
if (!IsValidNpCommId(com_id)) {
|
|
LOG_ERROR(NpHandler, "no valid NP Communication ID (npbind.dat missing or blank); "
|
|
"rejecting request");
|
|
return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
std::vector<u8> payload;
|
|
payload.reserve(12 + 4 + proto_bytes.size());
|
|
payload.insert(payload.end(), com_id.begin(), com_id.end());
|
|
const u32 sz = static_cast<u32>(proto_bytes.size());
|
|
payload.push_back(static_cast<u8>(sz));
|
|
payload.push_back(static_cast<u8>(sz >> 8));
|
|
payload.push_back(static_cast<u8>(sz >> 16));
|
|
payload.push_back(static_cast<u8>(sz >> 24));
|
|
payload.insert(payload.end(), proto_bytes.begin(), proto_bytes.end());
|
|
|
|
const u64 pkt_id = client->SubmitRequest(ShadNet::CommandType::GetScoreFriends, payload);
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
PendingScoreRequest pending;
|
|
pending.req = std::move(req);
|
|
pending.cmd = ShadNet::CommandType::GetScoreFriends;
|
|
// Note: aRankArray is set, plainRankArray remains null. OnScoreReply
|
|
// dispatches on which one is non-null.
|
|
pending.aRankArray = rankArray;
|
|
pending.commentArray = commentArray;
|
|
pending.infoArray = infoArray;
|
|
pending.lastSortDate = lastSortDate;
|
|
pending.totalRecord = totalRecord;
|
|
pending.arrayNum = arrayNum;
|
|
m_pending_score.emplace(pkt_id, std::move(pending));
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"GetFriendsRankingA: user_id={} service_label={} board={} includeSelf={} "
|
|
"arrayNum={} withComment={} withGameInfo={} pkt_id={} com_id='{}'",
|
|
user_id, service_label, boardId, includeSelf, arrayNum, commentArray != nullptr,
|
|
infoArray != nullptr, pkt_id, com_id);
|
|
return ORBIS_OK;
|
|
}
|
|
|
|
static u32 FillPlainRankArrayFromProto(const shadnet::GetScoreResponse& resp, u64 maxSlots,
|
|
NpScore::OrbisNpScoreRankData* rankArray,
|
|
NpScore::OrbisNpScoreComment* commentArray,
|
|
NpScore::OrbisNpScoreGameInfo* infoArray) {
|
|
const int n_resp = resp.rankarray_size();
|
|
u32 out_i = 0;
|
|
for (int i = 0; i < n_resp && out_i < maxSlots; ++i) {
|
|
const auto& r = resp.rankarray(i);
|
|
if (r.npid().empty()) {
|
|
continue;
|
|
}
|
|
auto& out = rankArray[out_i];
|
|
// Copy OnlineId (the npId string) into the 16-byte data field.
|
|
const std::string& npid = r.npid();
|
|
const size_t cp = std::min<size_t>(npid.size(), sizeof(out.npId.handle.data));
|
|
std::memcpy(out.npId.handle.data, npid.data(), cp);
|
|
out.npId.handle.term = 0;
|
|
LOG_INFO(NpHandler,
|
|
"FillPlainRankArrayFromProto: out[{}] (resp[{}]) npid='{}' (len={}) rank={} "
|
|
"score={}",
|
|
out_i, i, npid, npid.size(), r.rank(), r.score());
|
|
out.pcId = r.pcid();
|
|
out.serialRank = r.rank();
|
|
out.rank = r.rank();
|
|
out.highestRank = r.rank();
|
|
out.hasGameData = r.hasgamedata() ? 1 : 0;
|
|
out.scoreValue = r.score();
|
|
out.recordDate.tick = r.recorddate();
|
|
|
|
if (commentArray != nullptr && i < resp.commentarray_size()) {
|
|
const std::string& cmt = resp.commentarray(i);
|
|
const size_t ccp =
|
|
std::min<size_t>(cmt.size(), sizeof(commentArray[out_i].utf8Comment) - 1);
|
|
std::memcpy(commentArray[out_i].utf8Comment, cmt.data(), ccp);
|
|
}
|
|
if (infoArray != nullptr && i < resp.infoarray_size()) {
|
|
const std::string& gi = resp.infoarray(i).data();
|
|
const size_t gcp = std::min<size_t>(gi.size(), sizeof(infoArray[out_i].data));
|
|
infoArray[out_i].infoSize = static_cast<u32>(gcp);
|
|
std::memcpy(infoArray[out_i].data, gi.data(), gcp);
|
|
}
|
|
++out_i;
|
|
}
|
|
return out_i;
|
|
}
|
|
|
|
static u32 FillPlainRankArrayAFromProto(const shadnet::GetScoreResponse& resp, u64 maxSlots,
|
|
NpScore::OrbisNpScoreRankDataA* rankArray,
|
|
NpScore::OrbisNpScoreComment* commentArray,
|
|
NpScore::OrbisNpScoreGameInfo* infoArray) {
|
|
const int n_resp = resp.rankarray_size();
|
|
u32 out_i = 0;
|
|
for (int i = 0; i < n_resp && out_i < maxSlots; ++i) {
|
|
const auto& r = resp.rankarray(i);
|
|
if (r.npid().empty()) {
|
|
continue;
|
|
}
|
|
auto& out = rankArray[out_i];
|
|
// RankDataA stores OnlineId directly (not wrapped in NpId).
|
|
const std::string& npid = r.npid();
|
|
const size_t cp = std::min<size_t>(npid.size(), sizeof(out.onlineId.data));
|
|
std::memcpy(out.onlineId.data, npid.data(), cp);
|
|
out.onlineId.term = 0;
|
|
LOG_INFO(NpHandler,
|
|
"FillPlainRankArrayAFromProto: out[{}] (resp[{}]) npid='{}' (len={}) rank={} "
|
|
"score={} accountId={}",
|
|
out_i, i, npid, npid.size(), r.rank(), r.score(), r.accountid());
|
|
out.pcId = r.pcid();
|
|
out.serialRank = r.rank();
|
|
out.rank = r.rank();
|
|
out.highestRank = r.rank();
|
|
out.hasGameData = r.hasgamedata() ? 1 : 0;
|
|
out.scoreValue = r.score();
|
|
out.recordDate.tick = r.recorddate();
|
|
out.accountId = static_cast<Libraries::Np::OrbisNpAccountId>(r.accountid());
|
|
|
|
if (commentArray != nullptr && i < resp.commentarray_size()) {
|
|
const std::string& cmt = resp.commentarray(i);
|
|
const size_t ccp =
|
|
std::min<size_t>(cmt.size(), sizeof(commentArray[out_i].utf8Comment) - 1);
|
|
std::memcpy(commentArray[out_i].utf8Comment, cmt.data(), ccp);
|
|
}
|
|
if (infoArray != nullptr && i < resp.infoarray_size()) {
|
|
const std::string& gi = resp.infoarray(i).data();
|
|
const size_t gcp = std::min<size_t>(gi.size(), sizeof(infoArray[out_i].data));
|
|
infoArray[out_i].infoSize = static_cast<u32>(gcp);
|
|
std::memcpy(infoArray[out_i].data, gi.data(), gcp);
|
|
}
|
|
++out_i;
|
|
}
|
|
return out_i;
|
|
}
|
|
|
|
static u32 FillPlayerRankArrayAFromProto(const shadnet::GetScoreResponse& resp, u64 requestedCount,
|
|
NpScore::OrbisNpScorePlayerRankDataA* rankArray,
|
|
NpScore::OrbisNpScoreComment* commentArray,
|
|
NpScore::OrbisNpScoreGameInfo* infoArray) {
|
|
const int n_resp = resp.rankarray_size();
|
|
const int n_req = static_cast<int>(requestedCount);
|
|
if (n_resp != n_req) {
|
|
LOG_WARNING(NpHandler,
|
|
"FillPlayerRankArrayAFromProto: response count {} != request count {} — "
|
|
"will fill up to min() and leave extras at hasData=0",
|
|
n_resp, n_req);
|
|
}
|
|
const int n = std::min(n_resp, n_req);
|
|
u32 found = 0;
|
|
for (int i = 0; i < n; ++i) {
|
|
const auto& r = resp.rankarray(i);
|
|
if (r.npid().empty()) {
|
|
continue; // no score on this board for this input accountId
|
|
}
|
|
auto& out = rankArray[i];
|
|
out.hasData = 1;
|
|
const std::string& npid = r.npid();
|
|
const size_t cp = std::min<size_t>(npid.size(), sizeof(out.rankData.onlineId.data));
|
|
std::memcpy(out.rankData.onlineId.data, npid.data(), cp);
|
|
out.rankData.onlineId.term = 0;
|
|
out.rankData.pcId = r.pcid();
|
|
out.rankData.serialRank = r.rank();
|
|
out.rankData.rank = r.rank();
|
|
out.rankData.highestRank = r.rank();
|
|
out.rankData.hasGameData = r.hasgamedata() ? 1 : 0;
|
|
out.rankData.scoreValue = r.score();
|
|
out.rankData.recordDate.tick = r.recorddate();
|
|
out.rankData.accountId = static_cast<Libraries::Np::OrbisNpAccountId>(r.accountid());
|
|
|
|
if (commentArray != nullptr && i < resp.commentarray_size()) {
|
|
const std::string& cmt = resp.commentarray(i);
|
|
const size_t ccp =
|
|
std::min<size_t>(cmt.size(), sizeof(commentArray[i].utf8Comment) - 1);
|
|
std::memcpy(commentArray[i].utf8Comment, cmt.data(), ccp);
|
|
}
|
|
if (infoArray != nullptr && i < resp.infoarray_size()) {
|
|
const std::string& gi = resp.infoarray(i).data();
|
|
const size_t gcp = std::min<size_t>(gi.size(), sizeof(infoArray[i].data));
|
|
infoArray[i].infoSize = static_cast<u32>(gcp);
|
|
std::memcpy(infoArray[i].data, gi.data(), gcp);
|
|
}
|
|
++found;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
static u32 FillRankArrayFromProto(const shadnet::GetScoreResponse& resp,
|
|
const std::vector<std::string>& requestedNpIds,
|
|
NpScore::OrbisNpScorePlayerRankData* rankArray,
|
|
NpScore::OrbisNpScoreComment* commentArray,
|
|
NpScore::OrbisNpScoreGameInfo* infoArray) {
|
|
const int n_resp = resp.rankarray_size();
|
|
const int n_req = static_cast<int>(requestedNpIds.size());
|
|
if (n_resp != n_req) {
|
|
LOG_WARNING(NpHandler,
|
|
"FillRankArrayFromProto: response count {} != request count {} — "
|
|
"will fill up to min() and leave extras at hasData=0",
|
|
n_resp, n_req);
|
|
}
|
|
const int n = std::min(n_resp, n_req);
|
|
u32 found = 0;
|
|
for (int i = 0; i < n; ++i) {
|
|
const auto& r = resp.rankarray(i);
|
|
if (r.npid().empty()) {
|
|
continue; // server signalled "no data for this slot"
|
|
}
|
|
|
|
auto& out = rankArray[i];
|
|
out.hasData = 1;
|
|
const std::string& npid = r.npid();
|
|
const size_t cp = std::min<size_t>(npid.size(), sizeof(out.rankData.npId.handle.data));
|
|
std::memcpy(out.rankData.npId.handle.data, npid.data(), cp);
|
|
out.rankData.npId.handle.term = 0;
|
|
out.rankData.pcId = r.pcid();
|
|
out.rankData.serialRank = r.rank();
|
|
out.rankData.rank = r.rank();
|
|
out.rankData.highestRank = r.rank();
|
|
out.rankData.hasGameData = r.hasgamedata() ? 1 : 0;
|
|
out.rankData.scoreValue = r.score();
|
|
out.rankData.recordDate.tick = r.recorddate();
|
|
|
|
if (commentArray != nullptr && i < resp.commentarray_size()) {
|
|
const std::string& cmt = resp.commentarray(i);
|
|
const size_t ccp =
|
|
std::min<size_t>(cmt.size(), sizeof(commentArray[i].utf8Comment) - 1);
|
|
std::memcpy(commentArray[i].utf8Comment, cmt.data(), ccp);
|
|
}
|
|
if (infoArray != nullptr && i < resp.infoarray_size()) {
|
|
const std::string& gi = resp.infoarray(i).data();
|
|
const size_t gcp = std::min<size_t>(gi.size(), sizeof(infoArray[i].data));
|
|
infoArray[i].infoSize = static_cast<u32>(gcp);
|
|
std::memcpy(infoArray[i].data, gi.data(), gcp);
|
|
}
|
|
++found;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
void NpHandler::OnScoreReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id,
|
|
ShadNet::ErrorType error, const std::vector<u8>& body) {
|
|
PendingScoreRequest pending;
|
|
{
|
|
std::lock_guard lock(m_mutex_pending_score);
|
|
auto it = m_pending_score.find(pkt_id);
|
|
if (it == m_pending_score.end()) {
|
|
LOG_WARNING(NpHandler, "OnScoreReply: no pending request for pkt_id={} cmd={}", pkt_id,
|
|
static_cast<int>(cmd));
|
|
return;
|
|
}
|
|
pending = std::move(it->second);
|
|
m_pending_score.erase(it);
|
|
}
|
|
auto& req = pending.req;
|
|
|
|
// Server-side errors take precedence over success-path parsing.
|
|
if (error != ShadNet::ErrorType::NoError) {
|
|
s32 orbis_err = ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE;
|
|
switch (error) {
|
|
case ShadNet::ErrorType::ScoreNotBest:
|
|
orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_NOT_BEST_SCORE;
|
|
break;
|
|
case ShadNet::ErrorType::ScoreInvalid:
|
|
orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_INVALID_SCORE;
|
|
break;
|
|
case ShadNet::ErrorType::NotFound:
|
|
orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_RANKING_BOARD_MASTER_NOT_FOUND;
|
|
break;
|
|
case ShadNet::ErrorType::Unauthorized:
|
|
orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_FORBIDDEN;
|
|
break;
|
|
case ShadNet::ErrorType::DbFail:
|
|
orbis_err = ORBIS_NP_COMMUNITY_SERVER_ERROR_INTERNAL_SERVER_ERROR;
|
|
break;
|
|
case ShadNet::ErrorType::Malformed:
|
|
case ShadNet::ErrorType::Invalid:
|
|
case ShadNet::ErrorType::InvalidInput:
|
|
orbis_err = ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
LOG_WARNING(NpHandler,
|
|
"OnScoreReply: user_id={} pkt_id={} cmd={} server_error={} → orbis_err={:#x}",
|
|
user_id, pkt_id, static_cast<int>(cmd), static_cast<int>(error), orbis_err);
|
|
req->SetResult(orbis_err);
|
|
return;
|
|
}
|
|
|
|
// Per-command success-path parsing.
|
|
switch (cmd) {
|
|
case ShadNet::CommandType::RecordScore: {
|
|
// Reply body = u32 LE rank.
|
|
if (req->tmpRankOut != nullptr && body.size() >= 4) {
|
|
const u32 rank = static_cast<u32>(body[0]) | (static_cast<u32>(body[1]) << 8) |
|
|
(static_cast<u32>(body[2]) << 16) | (static_cast<u32>(body[3]) << 24);
|
|
*req->tmpRankOut = rank;
|
|
LOG_INFO(NpHandler, "OnScoreReply: RecordScore user_id={} pkt_id={} rank={}", user_id,
|
|
pkt_id, rank);
|
|
}
|
|
req->SetResult(ORBIS_OK);
|
|
break;
|
|
}
|
|
|
|
case ShadNet::CommandType::RecordScoreData: {
|
|
// Reply body is empty on success — error byte already consumed by
|
|
// the caller. Server-side errors are mapped to ErrorType values
|
|
// (NotFound / ScoreInvalid / ScoreHasData) which arrive as the
|
|
// packet's error byte; if we got here, error == NoError.
|
|
LOG_INFO(NpHandler, "OnScoreReply: RecordScoreData user_id={} pkt_id={} ok", user_id,
|
|
pkt_id);
|
|
req->SetResult(ORBIS_OK);
|
|
break;
|
|
}
|
|
|
|
case ShadNet::CommandType::GetScoreData:
|
|
case ShadNet::CommandType::GetScoreGameDataByAccId: {
|
|
const char* cmd_name = (cmd == ShadNet::CommandType::GetScoreData)
|
|
? "GetScoreData"
|
|
: "GetScoreGameDataByAccId";
|
|
if (body.size() < 4) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: {} body too small ({})", cmd_name, body.size());
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
const u32 stored_size = static_cast<u32>(body[0]) | (static_cast<u32>(body[1]) << 8) |
|
|
(static_cast<u32>(body[2]) << 16) |
|
|
(static_cast<u32>(body[3]) << 24);
|
|
if (static_cast<size_t>(4) + stored_size > body.size()) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: {} blob size {} exceeds body size {}", cmd_name,
|
|
stored_size, body.size());
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
|
|
// Always populate *totalSizeOut with the actual stored size on
|
|
// the server, even when truncating into a smaller dataOut buffer.
|
|
// Lets the caller know if they undersized their buffer and need
|
|
// to retry with a larger one.
|
|
if (pending.totalSizeOut != nullptr) {
|
|
*pending.totalSizeOut = stored_size;
|
|
}
|
|
if (pending.dataOut != nullptr && pending.recvSize > 0) {
|
|
const u64 copy_n = std::min<u64>(static_cast<u64>(stored_size), pending.recvSize);
|
|
std::memcpy(pending.dataOut, body.data() + 4, static_cast<size_t>(copy_n));
|
|
if (copy_n < stored_size) {
|
|
LOG_WARNING(NpHandler,
|
|
"OnScoreReply: {} truncated user_id={} pkt_id={} stored={} recv={}",
|
|
cmd_name, user_id, pkt_id, stored_size, pending.recvSize);
|
|
}
|
|
}
|
|
LOG_INFO(NpHandler, "OnScoreReply: {} user_id={} pkt_id={} storedSize={} recvSize={}",
|
|
cmd_name, user_id, pkt_id, stored_size, pending.recvSize);
|
|
req->SetResult(ORBIS_OK);
|
|
break;
|
|
}
|
|
|
|
case ShadNet::CommandType::GetBoardInfos: {
|
|
if (body.size() < 4) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: GetBoardInfos body too small ({})", body.size());
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
const u32 proto_size = static_cast<u32>(body[0]) | (static_cast<u32>(body[1]) << 8) |
|
|
(static_cast<u32>(body[2]) << 16) |
|
|
(static_cast<u32>(body[3]) << 24);
|
|
if (static_cast<size_t>(4) + proto_size > body.size()) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: GetBoardInfos proto size {} exceeds body size {}",
|
|
proto_size, body.size());
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
shadnet::BoardInfo bi;
|
|
if (!bi.ParseFromArray(body.data() + 4, static_cast<int>(proto_size))) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: GetBoardInfos proto parse failed");
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
if (pending.boardInfo != nullptr) {
|
|
pending.boardInfo->rankLimit = bi.ranklimit();
|
|
pending.boardInfo->updateMode = bi.updatemode();
|
|
pending.boardInfo->sortMode = bi.sortmode();
|
|
pending.boardInfo->uploadNumLimit = bi.uploadnumlimit();
|
|
pending.boardInfo->uploadSizeLimit = bi.uploadsizelimit();
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"OnScoreReply: GetBoardInfos user_id={} pkt_id={} rankLimit={} updateMode={} "
|
|
"sortMode={} uploadNumLimit={} uploadSizeLimit={}",
|
|
user_id, pkt_id, bi.ranklimit(), bi.updatemode(), bi.sortmode(),
|
|
bi.uploadnumlimit(), bi.uploadsizelimit());
|
|
req->SetResult(ORBIS_OK);
|
|
break;
|
|
}
|
|
|
|
case ShadNet::CommandType::GetScoreNpid: {
|
|
// Reply body = u32 LE proto size + GetScoreResponse proto bytes.
|
|
if (body.size() < 4) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: GetScoreNpid body too small ({})", body.size());
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
const u32 proto_size = static_cast<u32>(body[0]) | (static_cast<u32>(body[1]) << 8) |
|
|
(static_cast<u32>(body[2]) << 16) |
|
|
(static_cast<u32>(body[3]) << 24);
|
|
if (static_cast<size_t>(4) + proto_size > body.size()) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: GetScoreNpid proto size {} exceeds body size {}",
|
|
proto_size, body.size());
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
shadnet::GetScoreResponse resp;
|
|
if (!resp.ParseFromArray(body.data() + 4, static_cast<int>(proto_size))) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: GetScoreNpid proto parse failed");
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
const u32 found = FillRankArrayFromProto(resp, pending.requestedNpIds, pending.rankArray,
|
|
pending.commentArray, pending.infoArray);
|
|
if (pending.lastSortDate != nullptr) {
|
|
pending.lastSortDate->tick = resp.lastsortdate();
|
|
}
|
|
if (pending.totalRecord != nullptr) {
|
|
*pending.totalRecord = resp.totalrecord();
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"OnScoreReply: GetScoreNpid user_id={} pkt_id={} found={}/{} totalRecord={}",
|
|
user_id, pkt_id, found, pending.arrayNum, resp.totalrecord());
|
|
req->SetResult(static_cast<s32>(found));
|
|
break;
|
|
}
|
|
case ShadNet::CommandType::GetScoreAccountId: {
|
|
// Reply body = u32 LE proto size + GetScoreResponse proto bytes.
|
|
// Index-aligned with the request's accountIds[] (empty-npid sentinel
|
|
// marks "no score on this board"), same contract as GetScoreNpid.
|
|
if (body.size() < 4) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: GetScoreAccountId body too small ({})",
|
|
body.size());
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
const u32 proto_size = static_cast<u32>(body[0]) | (static_cast<u32>(body[1]) << 8) |
|
|
(static_cast<u32>(body[2]) << 16) |
|
|
(static_cast<u32>(body[3]) << 24);
|
|
if (static_cast<size_t>(4) + proto_size > body.size()) {
|
|
LOG_ERROR(NpHandler,
|
|
"OnScoreReply: GetScoreAccountId proto size {} exceeds body size {}",
|
|
proto_size, body.size());
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
shadnet::GetScoreResponse resp;
|
|
if (!resp.ParseFromArray(body.data() + 4, static_cast<int>(proto_size))) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: GetScoreAccountId proto parse failed");
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
const u32 found =
|
|
FillPlayerRankArrayAFromProto(resp, pending.arrayNum, pending.aPlayerRankArray,
|
|
pending.commentArray, pending.infoArray);
|
|
if (pending.lastSortDate != nullptr) {
|
|
pending.lastSortDate->tick = resp.lastsortdate();
|
|
}
|
|
if (pending.totalRecord != nullptr) {
|
|
*pending.totalRecord = resp.totalrecord();
|
|
}
|
|
LOG_INFO(NpHandler,
|
|
"OnScoreReply: GetScoreAccountId user_id={} pkt_id={} found={}/{} totalRecord={}",
|
|
user_id, pkt_id, found, pending.arrayNum, resp.totalrecord());
|
|
req->SetResult(static_cast<s32>(found));
|
|
break;
|
|
}
|
|
|
|
case ShadNet::CommandType::GetScoreRange:
|
|
case ShadNet::CommandType::GetScoreFriends: {
|
|
const char* cmd_name =
|
|
(cmd == ShadNet::CommandType::GetScoreRange) ? "GetScoreRange" : "GetScoreFriends";
|
|
if (body.size() < 4) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: {} body too small ({})", cmd_name, body.size());
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
const u32 proto_size = static_cast<u32>(body[0]) | (static_cast<u32>(body[1]) << 8) |
|
|
(static_cast<u32>(body[2]) << 16) |
|
|
(static_cast<u32>(body[3]) << 24);
|
|
if (static_cast<size_t>(4) + proto_size > body.size()) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: {} proto size {} exceeds body size {}", cmd_name,
|
|
proto_size, body.size());
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
shadnet::GetScoreResponse resp;
|
|
if (!resp.ParseFromArray(body.data() + 4, static_cast<int>(proto_size))) {
|
|
LOG_ERROR(NpHandler, "OnScoreReply: {} proto parse failed", cmd_name);
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
u32 found = 0;
|
|
if (pending.aRankArray != nullptr) {
|
|
found = FillPlainRankArrayAFromProto(resp, pending.arrayNum, pending.aRankArray,
|
|
pending.commentArray, pending.infoArray);
|
|
} else {
|
|
found = FillPlainRankArrayFromProto(resp, pending.arrayNum, pending.plainRankArray,
|
|
pending.commentArray, pending.infoArray);
|
|
}
|
|
if (pending.lastSortDate != nullptr) {
|
|
pending.lastSortDate->tick = resp.lastsortdate();
|
|
}
|
|
if (pending.totalRecord != nullptr) {
|
|
*pending.totalRecord = resp.totalrecord();
|
|
}
|
|
LOG_INFO(NpHandler, "OnScoreReply: {} user_id={} pkt_id={} found={}/{} totalRecord={}{}",
|
|
cmd_name, user_id, pkt_id, found, pending.arrayNum, resp.totalrecord(),
|
|
pending.aRankArray != nullptr ? " (A-variant)" : "");
|
|
req->SetResult(static_cast<s32>(found));
|
|
break;
|
|
}
|
|
|
|
default:
|
|
LOG_WARNING(NpHandler, "OnScoreReply: unexpected cmd={} pkt_id={}", static_cast<int>(cmd),
|
|
pkt_id);
|
|
req->SetResult(ORBIS_NP_COMMUNITY_ERROR_BAD_RESPONSE);
|
|
break;
|
|
}
|
|
}
|
|
|
|
} // namespace Libraries::Np
|