Files
Nicklas Johansen c3dac03592 Online: keep the resolved-peer log line findable by the log privacy test
test_thd_log_lines_use_the_mask looked for the old 'resolved %s:%u' text,
which went with the synchronous resolve. The resolve log now has one line
per case, each with the masked address, and the test looks for the new
'resolved (%s:%u)'.
2026-11-02 21:28:07 +01:00

604 lines
23 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// The Hunter's Dream backend: sceNpSignaling over the peer table, and the NP manager, auth,
// lookup, score and WebAPI imports answered here (The Hunter's Dream has no PSN behind it).
// Adapted from droogie/bbhost src/hle/np_signaling.cpp and src/hle/system.cpp (566184a).
#include "thd_internal.h"
#include <atomic>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <map>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <vector>
#include "core/libraries/libs.h"
namespace BbThd {
namespace {
// ---- sceNpSignaling ---------------------------------------------------------------------------------
// The game activates a connection to a peer's NpId and expects ESTABLISHED (0x01) then ACTIVE
// (0x0c); its status read wants 2 with the peer's address and port. 0x00 is dead.
constexpr int kSigErrInvalidArg = static_cast<int>(0x80550003);
constexpr std::uint16_t kEvDead = 0x00, kEvEstablished = 0x01, kEvActive = 0x0c;
struct SigCtx {
void* cb = nullptr;
void* arg = nullptr;
};
struct Conn {
unsigned ctx = 0;
std::string online_id;
std::uint16_t member_id = 0;
bool announced = false;
bool dead = false;
};
std::mutex g_sig_mu;
std::map<unsigned, SigCtx> g_sig_ctx;
std::map<unsigned, Conn> g_conn;
unsigned g_next_sig_ctx = 1;
unsigned g_next_conn = 1;
void sig_fire(unsigned ctx, unsigned conn, std::uint16_t event, int error) {
SigCtx c;
{
std::lock_guard lk(g_sig_mu);
const auto it = g_sig_ctx.find(ctx);
if (it == g_sig_ctx.end() || !it->second.cb) return;
c = it->second;
}
if (Trace()) Log("np: NpSignaling event 0x%x conn %u", event, conn);
CallGuest(c.cb, static_cast<std::int64_t>(ctx), static_cast<std::int64_t>(conn), static_cast<std::int64_t>(event),
static_cast<std::int64_t>(error), c.arg);
}
// A connection with a known peer: ESTABLISHED at +200 ms, ACTIVE at +400.
void announce_locked(unsigned conn_id, Conn& c) {
if (c.announced || !c.member_id) return;
c.announced = true;
PeersSetConn(c.member_id, conn_id, 2);
const unsigned ctx = c.ctx;
DispatchAfter(200, Prio::Signaling, [ctx, conn_id] { sig_fire(ctx, conn_id, kEvEstablished, 0); });
DispatchAfter(400, Prio::Signaling, [ctx, conn_id] { sig_fire(ctx, conn_id, kEvActive, 0); });
}
s32 PS4_SYSV_ABI sig_init(u64, int, int, u64) { return 0; }
s32 PS4_SYSV_ABI sig_term() {
std::lock_guard lk(g_sig_mu);
g_sig_ctx.clear();
g_conn.clear();
return 0;
}
s32 PS4_SYSV_ABI sig_create_ctx(const void*, void* cb, void* arg, unsigned* ctx) {
if (!ctx) return kSigErrInvalidArg;
std::lock_guard lk(g_sig_mu);
const unsigned id = g_next_sig_ctx++;
g_sig_ctx[id] = SigCtx{cb, arg};
*ctx = id;
return 0;
}
s32 PS4_SYSV_ABI sig_delete_ctx(unsigned ctx) {
std::lock_guard lk(g_sig_mu);
g_sig_ctx.erase(ctx);
for (auto it = g_conn.begin(); it != g_conn.end();) it = it->second.ctx == ctx ? g_conn.erase(it) : std::next(it);
return 0;
}
std::atomic<std::int64_t> g_resolve_at{0};
bool RoomBackend() {
static const bool room = [] {
const char* v = std::getenv("BB_NET_BACKEND");
return v && std::strcmp(v, "room") == 0;
}();
return room;
}
// Asks the server for the peer's address: an HTTPS round trip, so never on the game's thread.
// `first` is the try sig_activate starts; later ones come from status reads.
void RetryResolve(unsigned conn, const std::string& peer, bool force, bool first = false) {
const std::int64_t now = NowMs();
if (!force && now - g_resolve_at.load() < 2000) return;
g_resolve_at = now;
Peer known;
if (PeersFindOnline(peer, &known)) {
std::lock_guard lk(g_sig_mu);
const auto it = g_conn.find(conn);
if (it != g_conn.end() && !it->second.dead && !it->second.member_id) {
it->second.member_id = known.member_id;
announce_locked(conn, it->second);
}
return;
}
std::uint32_t addr = 0;
std::uint16_t port = 0;
std::string error;
if (!ServerSignalingResolve(peer, &addr, &port, error)) {
if (first) Log("signaling: %s not known and not resolved: %s", peer.c_str(), error.c_str());
return;
}
// The connection may have gone while the request ran (deactivated, or the room left and
// SignalingReset): then the peer stays out of the peer table and nothing is announced.
// g_sig_mu before the peer table's lock, as sig_activate has always taken them.
std::lock_guard lk(g_sig_mu);
const auto it = g_conn.find(conn);
if (it == g_conn.end() || it->second.dead || it->second.member_id) return;
it->second.member_id = PeersProvisional(peer, addr, port).member_id;
const auto ms = static_cast<long long>(NowMs() - now);
if (first) Log("signaling: %s resolved (%s:%u) in %lld ms", peer.c_str(), BbNet::MaskIpv4(addr).c_str(), port, ms);
else Log("signaling: %s found on a later try (%s:%u) in %lld ms", peer.c_str(), BbNet::MaskIpv4(addr).c_str(), port, ms);
announce_locked(conn, it->second);
}
// One long-lived thread runs the resolves in turn. NpPost keeps its HTTPS connection per thread,
// so this one stays open between peers: a thread per resolve paid a new TCP and TLS handshake
// each time (~500 ms instead of ~270). The thread starts with the first resolve (online play).
struct ResolveJob {
unsigned conn;
std::string peer;
bool first;
};
std::mutex g_rq_mu;
std::condition_variable g_rq_cv;
std::deque<ResolveJob> g_rq;
bool g_rq_running = false;
void ResolveWorker() {
for (;;) {
ResolveJob job;
{
std::unique_lock lk(g_rq_mu);
g_rq_cv.wait(lk, [] { return !g_rq.empty(); });
job = std::move(g_rq.front());
g_rq.pop_front();
}
RetryResolve(job.conn, job.peer, true, job.first);
}
}
void QueueResolve(unsigned conn, const std::string& peer, bool first = false) {
std::lock_guard lk(g_rq_mu);
for (const ResolveJob& j : g_rq) {
if (j.conn == conn) return; // already waiting its turn
}
g_rq.push_back({conn, peer, first});
if (!g_rq_running) {
g_rq_running = true;
std::thread(ResolveWorker).detach();
}
g_rq_cv.notify_one();
}
// Idempotent per peer: the game re-activates.
s32 PS4_SYSV_ABI sig_activate(unsigned ctx, const void* npid, unsigned* conn_out) {
if (!conn_out) return kSigErrInvalidArg;
const std::string peer = NpIdText(npid);
unsigned retry = 0;
std::uint32_t fresh_addr = 0;
std::uint16_t fresh_port = 0;
if (RoomBackend()) {
std::string error;
Peer cached;
if (ServerSignalingResolve(peer, &fresh_addr, &fresh_port, error) && PeersFindOnline(peer, &cached) &&
(cached.addr != fresh_addr || cached.port != fresh_port)) {
Log("signaling: %s came back at a new address; the old one is dropped", peer.c_str());
{
std::lock_guard lk(g_sig_mu);
for (auto& [id, c] : g_conn)
if (c.online_id == peer) c.dead = true;
}
if (cached.member_id >= 0xff00) PeersProvisional(peer, fresh_addr, fresh_port);
else PeersErase(cached.member_id);
}
}
{
std::lock_guard lk(g_sig_mu);
for (auto& [id, c] : g_conn) {
if (c.ctx == ctx && c.online_id == peer && !c.dead) {
*conn_out = id;
if (!c.member_id) {
Peer p;
if (PeersFindOnline(peer, &p)) c.member_id = p.member_id;
}
announce_locked(id, c);
if (!c.member_id) retry = id;
else return 0;
break;
}
}
}
if (retry) {
QueueResolve(retry, peer);
return 0;
}
std::lock_guard lk(g_sig_mu);
const unsigned id = g_next_conn++;
Conn c;
c.ctx = ctx;
c.online_id = peer;
Peer p;
if (PeersFindOnline(peer, &p)) c.member_id = p.member_id;
g_conn[id] = c;
*conn_out = id;
if (!c.member_id) {
// The host activates its connection to the guest it summons before the guest is a room
// member: the server resolves the address from the guest's context_start. That request
// blocked the game's frame for a server round trip (~270 ms) with every new peer; it now
// runs beside the game, the status read answers "pending" meanwhile, and the connection
// is announced when the address arrives, as for a peer resolved on a later try.
Log("sceNpSignalingActivateConnection %s -> conn %u (resolving)", peer.c_str(), id);
g_resolve_at = NowMs(); // the game's status reads right after do not start a second one
QueueResolve(id, peer, true);
return 0;
}
Log("sceNpSignalingActivateConnection %s -> conn %u", peer.c_str(), id);
announce_locked(id, g_conn[id]);
return 0;
}
s32 PS4_SYSV_ABI sig_deactivate(unsigned ctx, unsigned conn) {
std::lock_guard lk(g_sig_mu);
const auto it = g_conn.find(conn);
if (it != g_conn.end() && it->second.ctx == ctx) {
it->second.dead = true;
if (it->second.member_id) PeersSetConn(it->second.member_id, conn, 0);
}
return 0;
}
// (ctxId, connId, status*, SceNetInAddr*, SceInPort_t*)
s32 PS4_SYSV_ABI sig_status(unsigned ctx, unsigned conn, int* st, std::uint32_t* addr, std::uint16_t* port) {
int status = 0;
std::uint32_t a = 0;
std::uint16_t p = 0;
std::string unresolved;
{
std::lock_guard lk(g_sig_mu);
const auto it = g_conn.find(conn);
if (it != g_conn.end() && it->second.ctx == ctx && !it->second.dead) {
Peer peer;
if (it->second.member_id && PeersGet(it->second.member_id, &peer) && peer.addr) {
status = it->second.announced ? 2 : 1;
a = peer.addr;
p = static_cast<std::uint16_t>((peer.port << 8) | (peer.port >> 8));
} else {
status = 1;
if (!it->second.member_id) unresolved = it->second.online_id;
}
}
}
if (!unresolved.empty() && NowMs() - g_resolve_at.load() >= 2000) {
g_resolve_at = NowMs();
QueueResolve(conn, unresolved);
}
if (st) *st = status;
if (addr) *addr = a;
if (port) *port = p;
return 0;
}
// ---- NP manager: signed in as the account, no PSN behind it -----------------------------------------
constexpr int kNpErrInvalidArg = static_cast<int>(0x80550003);
std::atomic<int> g_np_req{1};
s32 PS4_SYSV_ABI np_ok() { return 0; }
s32 PS4_SYSV_ABI np_register_cb(void*, void*) { return 0; }
s32 PS4_SYSV_ABI np_get_state(int, int* state) {
if (!state) return kNpErrInvalidArg;
// Signed out without an account: the game plays offline rather than under a made-up name.
*state = SignedIn() ? 2 : 1;
return 0;
}
s32 PS4_SYSV_ABI np_online_id(int, void* id) {
if (!id) return kNpErrInvalidArg;
const std::string cur = OnlineId();
std::memset(id, 0, 20);
std::memcpy(id, cur.c_str(), std::min<std::size_t>(cur.size(), 16));
return 0;
}
// The same handle as the online id: the session service matches peers by it.
s32 PS4_SYSV_ABI np_get_npid(int, void* id) {
if (!id) return kNpErrInvalidArg;
FillNpId(id, OnlineId().c_str());
return 0;
}
s32 PS4_SYSV_ABI np_presence_status(int, int* st) {
if (st) *st = 1;
return 0;
}
s32 PS4_SYSV_ABI np_cmp_online(const void* a, const void* b) {
if (!a || !b) return kNpErrInvalidArg;
return std::memcmp(a, b, 16);
}
s32 PS4_SYSV_ABI np_cmp_npid(const void* a, const void* b) {
if (!a || !b) return kNpErrInvalidArg;
return std::memcmp(a, b, 36);
}
s32 PS4_SYSV_ABI np_create_request(const void*) { return g_np_req.fetch_add(1); }
s32 PS4_SYSV_ABI np_request_done(int) { return 0; }
s32 PS4_SYSV_ABI np_poll_async(int, int* result) {
if (result) *result = 0;
return 0; // finished
}
s32 PS4_SYSV_ABI np_check_availability(int, const void*, void*) { return 0; }
s32 PS4_SYSV_ABI np_parental(int, const void*, std::int8_t* age, std::uint8_t* info) {
if (age) *age = 25;
if (info) std::memset(info, 0, 3);
return 0;
}
s32 PS4_SYSV_ABI np_check_plus(int, const void*, std::uint8_t* result) {
if (result) result[0] = 1;
return 0;
}
s32 PS4_SYSV_ABI np_plus_notify(int, u64) { return 0; }
// The game server binds the session to the account by X-BB-Token, not by this code.
s32 PS4_SYSV_ABI npauth_get_code(int, const void*, char* code, int* issuer) {
if (code) {
std::memset(code, 0, 128);
std::strncpy(code, "DUMMY", 127);
}
if (issuer) *issuer = 10;
return 0;
}
// ---- lookup ---------------------------------------------------------------------------------------------
std::atomic<int> g_lookup_id{1};
s32 PS4_SYSV_ABI lookup_title_ctx(const void*) { return g_lookup_id.fetch_add(1); }
s32 PS4_SYSV_ABI lookup_async(int, const void*) { return g_lookup_id.fetch_add(1); }
s32 PS4_SYSV_ABI lookup_npid(int, const void* online, void* npid, void*) {
if (!online || !npid) return kNpErrInvalidArg;
char name[17]{};
std::memcpy(name, online, 16);
FillNpId(npid, name);
return 0;
}
// ---- score: no leaderboard server ------------------------------------------------------------------
constexpr int kScoreErrOffline = static_cast<int>(0x80550605u);
std::atomic<int> g_score_id{1};
s32 PS4_SYSV_ABI score_title_ctx(const void*, int) { return g_score_id.fetch_add(1); }
s32 PS4_SYSV_ABI score_create_request(int) { return g_score_id.fetch_add(1); }
s32 PS4_SYSV_ABI score_set_pc_id(int, int) { return 0; }
s32 PS4_SYSV_ABI score_offline() { return kScoreErrOffline; }
s32 PS4_SYSV_ABI score_comment_ok(int, const char*) { return 0; }
// ---- WebAPI: answered here, never sent anywhere -------------------------------------------------------
constexpr int kWebApiInvalid = static_cast<int>(0x80552902);
constexpr int kWebApiNoReq = static_cast<int>(0x80552906);
constexpr char kWebApiJsonType[] = "application/json; charset=utf-8";
struct WebApiReq {
int status = 200;
std::string body;
std::size_t read_off = 0;
};
std::mutex g_web_mu;
std::atomic<int> g_web_id{1};
std::int64_t g_web_req = 1;
std::unordered_map<std::int64_t, WebApiReq> g_web_reqs;
std::string webapi_body_for_path(const char* path) {
if (path && std::strstr(path, "friendList")) return "{\"totalResults\":0,\"friendList\":[]}";
if (path && std::strstr(path, "blockList")) return "{\"totalResults\":0,\"blockList\":[]}";
return "{\"totalResults\":0}";
}
s32 PS4_SYSV_ABI webapi_init(int, u64) { return g_web_id.fetch_add(1); }
s32 PS4_SYSV_ABI webapi_create_ctx(int, const void*) { return g_web_id.fetch_add(1); }
s32 PS4_SYSV_ABI webapi_create_filter(int, const void*, u64) { return g_web_id.fetch_add(1); }
s32 PS4_SYSV_ABI webapi_reg_push(int, int, void*, void*) { return g_web_id.fetch_add(1); }
s32 PS4_SYSV_ABI webapi_create_req(int, const char*, const char* path, int, const void*, std::int64_t* req_id) {
if (!path || !req_id) return kWebApiInvalid;
std::lock_guard lk(g_web_mu);
const std::int64_t id = g_web_req++;
g_web_reqs[id] = WebApiReq{200, webapi_body_for_path(path), 0};
*req_id = id;
return 0;
}
s32 PS4_SYSV_ABI webapi_delete_req(std::int64_t req) {
std::lock_guard lk(g_web_mu);
g_web_reqs.erase(req);
return 0;
}
s32 PS4_SYSV_ABI webapi_send(std::int64_t req, const void*, u64) {
std::lock_guard lk(g_web_mu);
return g_web_reqs.count(req) ? 0 : kWebApiNoReq;
}
s32 PS4_SYSV_ABI webapi_status(std::int64_t req, int* code) {
if (!code) return kWebApiInvalid;
std::lock_guard lk(g_web_mu);
const auto it = g_web_reqs.find(req);
if (it == g_web_reqs.end()) return kWebApiNoReq;
*code = it->second.status;
return 0;
}
s32 PS4_SYSV_ABI webapi_read(std::int64_t req, void* data, u64 size) {
std::lock_guard lk(g_web_mu);
const auto it = g_web_reqs.find(req);
if (it == g_web_reqs.end()) return kWebApiNoReq;
WebApiReq& r = it->second;
if (!data || size == 0 || r.read_off >= r.body.size()) return 0;
const std::size_t n = std::min<std::size_t>(static_cast<std::size_t>(size), r.body.size() - r.read_off);
std::memcpy(data, r.body.data() + r.read_off, n);
r.read_off += n;
return static_cast<int>(n);
}
bool is_content_type(const char* name) {
return std::strcmp(name, "Content-Type") == 0 || std::strcmp(name, "content-type") == 0;
}
s32 PS4_SYSV_ABI webapi_hdr_len(std::int64_t req, const char* name, u64* len) {
if (!name || !len) return kWebApiInvalid;
std::lock_guard lk(g_web_mu);
if (!g_web_reqs.count(req)) return kWebApiNoReq;
if (!is_content_type(name)) return kWebApiInvalid;
*len = sizeof(kWebApiJsonType);
return 0;
}
s32 PS4_SYSV_ABI webapi_hdr_val(std::int64_t req, const char* name, char* buf, u64 n) {
if (!name || !buf || n == 0) return kWebApiInvalid;
std::lock_guard lk(g_web_mu);
if (!g_web_reqs.count(req)) return kWebApiNoReq;
if (!is_content_type(name)) return kWebApiInvalid;
std::snprintf(buf, static_cast<std::size_t>(n), "%s", kWebApiJsonType);
return 0;
}
s32 PS4_SYSV_ABI webapi_parse_npid(const char* json, void* npid) {
if (!json || !npid) return kWebApiInvalid;
const char* s = json;
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') ++s;
char online[17]{};
auto take_quoted = [&](const char* p) {
const char* q = std::strchr(p, '"');
if (!q) return false;
++q;
const char* e = std::strchr(q, '"');
if (!e) return false;
const std::size_t n = static_cast<std::size_t>(e - q);
if (n == 0 || n >= sizeof(online)) return false;
std::memcpy(online, q, n);
return true;
};
if (*s == '{') {
const char* key = std::strstr(s, "onlineId");
if (!key) key = std::strstr(s, "\"data\"");
// The value after the key's colon.
const char* colon = key ? std::strchr(key, ':') : nullptr;
if (!colon || !take_quoted(colon)) return kWebApiInvalid;
} else if (*s == '"') {
if (!take_quoted(s)) return kWebApiInvalid;
} else {
std::snprintf(online, sizeof(online), "%s", s);
}
FillNpId(npid, online);
return 0;
}
} // namespace
void SignalingPeerKnown(std::uint16_t member_id) {
Peer p;
if (!PeersGet(member_id, &p)) return;
std::lock_guard lk(g_sig_mu);
for (auto& [id, c] : g_conn) {
if (!c.dead && c.online_id == p.online_id) {
c.member_id = member_id;
announce_locked(id, c);
}
}
}
void SignalingPeerDead(const std::string& online_id) {
std::vector<std::pair<unsigned, unsigned>> dead;
{
std::lock_guard lk(g_sig_mu);
for (auto& [id, c] : g_conn) {
if (!c.dead && c.online_id == online_id) {
c.dead = true;
dead.emplace_back(c.ctx, id);
}
}
}
for (const auto& [ctx, conn] : dead) {
DispatchAfter(100, Prio::Signaling, [ctx = ctx, conn = conn] { sig_fire(ctx, conn, kEvDead, 0); });
}
}
void SignalingReset() {
std::lock_guard lk(g_sig_mu);
for (auto& [id, c] : g_conn) c.dead = true;
}
void RegisterNp(Core::Loader::SymbolsResolver* sym) {
#define SIG(nid, fn) LIB_FUNCTION(nid, "libSceNpSignaling", 1, "libSceNpSignaling", fn)
SIG("3KOuC4RmZZU", sig_init);
SIG("NPhw0UXaNrk", sig_term);
SIG("5yYjEdd4t8Y", sig_create_ctx);
SIG("hx+LIg-1koI", sig_delete_ctx);
SIG("0UvTFeomAUM", sig_activate);
SIG("6UEembipgrM", sig_deactivate);
SIG("bD-JizUb3JM", sig_status);
#undef SIG
#define NP(nid, fn) LIB_FUNCTION(nid, "libSceNpManager", 1, "libSceNpManager", fn)
NP("VfRSmPmj8Q8", np_register_cb); // sceNpRegisterStateCallback
NP("mjjTXh+NHWY", np_ok); // sceNpUnregisterStateCallback
NP("uFJpaKNBAj4", np_register_cb); // sceNpRegisterGamePresenceCallback
NP("GImICnh+boA", np_register_cb); // sceNpRegisterPlusEventCallback
NP("xViqJdDgKl0", np_ok); // sceNpUnregisterPlusEventCallback
NP("3Zl8BePTh9Y", np_ok); // sceNpCheckCallback
NP("Ec63y59l9tw", np_ok); // sceNpSetNpTitleId
NP("A2CQ3kgSopQ", np_ok); // sceNpSetContentRestriction
NP("Gaxrp3EWY-M", np_plus_notify); // sceNpNotifyPlusFeature
NP("eQH7nWPcAgc", np_get_state); // sceNpGetState
NP("XDncXQIJUSk", np_online_id); // sceNpGetOnlineId
NP("p-o74CnoNzY", np_get_npid); // sceNpGetNpId
NP("IPb1hd1wAGc", np_presence_status); // sceNpGetGamePresenceStatus
NP("eiqMCt9UshI", np_create_request); // sceNpCreateAsyncRequest
NP("S7QTn72PrDw", np_request_done); // sceNpDeleteRequest
NP("OzKvTvg3ZYU", np_request_done); // sceNpAbortRequest
NP("uqcPJLWL08M", np_poll_async); // sceNpPollAsync
NP("2rsFmlGWleQ", np_check_availability); // sceNpCheckNpAvailability
NP("ilwLM4zOmu4", np_parental); // sceNpGetParentalControlInfo
NP("r6MyYJkryz8", np_check_plus); // sceNpCheckPlus
#undef NP
LIB_FUNCTION("i8UmXTSq7N4", "libSceNpCommon", 1, "libSceNpCommon", np_cmp_npid);
LIB_FUNCTION("dj+O5aD2a0Q", "libSceNpCommon", 1, "libSceNpCommon", np_cmp_online);
#define AUTH(nid, fn) LIB_FUNCTION(nid, "libSceNpAuth", 1, "libSceNpAuth", fn)
AUTH("N+mr7GjTvr8", np_create_request);
AUTH("H8wG9Bk-nPc", np_request_done);
AUTH("gjSyfzSsDcE", np_poll_async);
AUTH("KxGkOrQJTqY", npauth_get_code);
#undef AUTH
#define LOOKUP(nid, fn) LIB_FUNCTION(nid, "libSceNpManager", 1, "libSceNpManager", fn)
LOOKUP("8533Q+LU7EQ", lookup_title_ctx);
LOOKUP("mtqDK9zkoIE", np_request_done);
LOOKUP("JA4+sS39GMs", lookup_async);
LOOKUP("wLaxchvEEnk", np_request_done);
LOOKUP("eYz4v5Uek9U", np_request_done);
LOOKUP("V4EVrruHuy8", np_poll_async);
LOOKUP("T6tnM1Uti4g", lookup_npid);
#undef LOOKUP
#define SCORE(nid, fn) LIB_FUNCTION(nid, "libSceNpScore", 1, "libSceNpScore", fn)
SCORE("KnNA1TEgtBI", score_title_ctx);
SCORE("G0pE+RNCwfk", np_request_done);
SCORE("gW8qyjYrUbk", score_create_request);
SCORE("dK8-SgYf6r4", np_request_done);
SCORE("1i7kmKbX6hk", np_request_done);
SCORE("bygbKdHmjn4", score_set_pc_id);
SCORE("ETS-uM-vH9Q", score_offline); // GetRankingByNpIdPcId
SCORE("KBHxDjyk-jA", score_offline); // GetRankingByRange
SCORE("zKoVok6FFEI", score_offline); // GetGameData
SCORE("zT0XBtgtOSI", score_offline); // RecordScore
SCORE("LoVMVrijVOk", score_offline); // GetBoardInfo
SCORE("bcoVwcBjQ9E", score_offline); // RecordGameData
SCORE("2b3TI0mDYiI", score_comment_ok);
SCORE("r4oAo9in0TA", score_comment_ok);
#undef SCORE
#define WEB(nid, fn) LIB_FUNCTION(nid, "libSceNpWebApi", 1, "libSceNpWebApi", fn)
WEB("G3AnLNdRBjE", webapi_init);
WEB("asz3TtIqGF8", np_request_done); // Terminate
WEB("x1Y7yiYSk7c", webapi_create_ctx);
WEB("XUjdsSTTZ3U", np_request_done); // DeleteContext
WEB("y5Ta5JCzQHY", webapi_create_filter);
WEB("zE+R6Rcx3W0", np_ok); // DeletePushEventFilter
WEB("PfSTDCgNMgc", webapi_reg_push);
WEB("qK4o2656W4w", np_ok); // UnregisterPushEventCallback
WEB("rdgs5Z1MyFw", webapi_create_req);
WEB("noQgleu+KLE", webapi_delete_req);
WEB("JzhYTP2fG18", webapi_delete_req); // AbortRequest
WEB("kVbL4hL3K7w", webapi_send);
WEB("k210oKgP80Y", webapi_status);
WEB("CQtPRSF6Ds8", webapi_read);
WEB("743ZzEBzlV8", webapi_hdr_len);
WEB("VwJ5L0Higg0", webapi_hdr_val);
WEB("or0e885BlXo", webapi_parse_npid);
#undef WEB
}
} // namespace BbThd