shadPS4/src/core/libraries/np/np_handler.cpp
georgemoralis aff387e8a9
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
2026-06-24 23:57:43 +03:00

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