Files
AnyPS5/core/libs/tests/GuestNpTrophyData.cpp
T
Yunus Shaikh d374de5e38 feat(libSceNpTrophy2): report the title's trophy data from trophyNN.ucp
The title's sce_sys/trophy2/trophyNN.ucp is read on first use with a
bounds-checked .ucp reader and a small JSON reader. tropconf.json gives
each trophy's grade, group, hidden and reward flags, platinum and
unlock condition, and tropmeta_<tag>.json its texts in the console
language, then en-US, then the file's default language. Contexts
remember their user and service label, so the queries find the file.

The trophy, group and game queries report these names, grades, groups
and counts, list every trophy and group, and give progressive trophies
their target. Unlocks are not recorded yet, so every trophy reports as
locked and nothing is written to the host. A missing file is ignored
and a malformed one is logged once; either way the queries keep their
placeholders. A JSON document that nests deeper than 64 levels or holds
more than 1048576 values counts as malformed, which keeps the parsed
values to about 140 MiB.
2026-10-17 23:25:07 +00:00

189 lines
9.9 KiB
C++

#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp"
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <random>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
extern "C" {
int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t);
int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*);
int APS5_VABI sceNpTrophy2GetGroupInfo(int, int, int, NpTrophy2GroupDetails*, NpTrophy2GroupData*);
int APS5_VABI sceNpTrophy2GetGroupInfoArray(int, int, uint32_t, uint32_t, NpTrophy2GroupDetails*, NpTrophy2GroupData*, uint32_t*);
int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*);
int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTrophy2Details*, NpTrophy2Data*, uint32_t*);
}
static constexpr int InitialUser = 0x10000000;
static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[
{"id":"0","grade":"P"},
{"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}},
{"id":"2","grade":"S","platinumTrophyId":"0","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100"}},
{"id":"3","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}},
{"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}},
{"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}},
{"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true},
{"id":"7","grade":"B","groupId":"1"}
]})";
static const char TropMeta[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"},
"trophyMetadata":[{"id":"0","name":"All of them"},{"id":"1","name":"Ten","detail":"Reach ten"},{"id":"5","name":"Five steps"},
{"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"}],
"groupMetadata":[{"id":"1","name":"Expansion"}]}})";
static void Require(bool value, const char* message) {
if (value) return;
std::fprintf(stderr, "%s\n", message);
std::abort();
}
template <typename TCall>
static bool ThrowsInvalidArgument(TCall call) {
try {
call();
} catch (const std::invalid_argument&) {
return true;
}
return false;
}
static void PutBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) {
for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast<char>((value >> (8 * (size - 1 - i))) & 0xFF);
}
static std::string Ucp(const std::vector<std::pair<std::string, std::string>>& files) {
std::size_t offset = 0x60 + files.size() * 0x40;
std::size_t size = offset;
for (const auto& file : files) size += file.second.size();
std::string data(size, '\0');
PutBigEndian(data, 0, 0xB228C60A, 4);
PutBigEndian(data, 4, 1, 4);
PutBigEndian(data, 8, size, 8);
PutBigEndian(data, 0x10, files.size(), 4);
PutBigEndian(data, 0x14, 0x40, 4);
std::size_t entry = 0x60;
for (const auto& [name, contents] : files) {
data.replace(entry, name.size(), name);
PutBigEndian(data, entry + 0x20, offset, 8);
PutBigEndian(data, entry + 0x28, contents.size(), 8);
data.replace(offset, contents.size(), contents);
offset += contents.size();
entry += 0x40;
}
return data;
}
static void Write(const std::filesystem::path& path, const std::string& text) {
std::filesystem::create_directories(path.parent_path());
std::FILE* file = std::fopen(path.string().c_str(), "wb");
Require(file != nullptr, "a test file was not created");
const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size();
Require(std::fclose(file) == 0 && written, "a test file was not written");
}
static int TrophyContext(std::uint32_t serviceLabel) {
int context = 0;
Require(sceNpTrophy2CreateContext(&context, InitialUser, serviceLabel, 0) == 0, "trophy context was not created");
return context;
}
static NpTrophy2Data Trophy(int context, int trophyId, NpTrophy2Details* details = nullptr) {
NpTrophy2Data data{};
Require(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, details, &data) == 0, "trophy info was not returned");
Require(data.trophy_id == trophyId, "trophy info is for another trophy");
return data;
}
static NpTrophy2GameData Game(int context, NpTrophy2GameDetails* details = nullptr) {
NpTrophy2GameData data{};
Require(sceNpTrophy2GetGameInfo(context, 1, details, &data) == 0, "game info was not returned");
return data;
}
static void ReportTrophyData(int context) {
NpTrophy2GameDetails game{};
const auto progress = Game(context, &game);
Require(std::strcmp(game.title, "Synthetic Game") == 0 && game.num_groups == 2 && game.num_trophies == 8 && game.num_platinum == 1 && game.num_gold == 1 && game.num_silver == 1 && game.num_bronze == 5,
"the game details are not the title's");
Require(progress.unlocked_trophies == 0 && progress.progress_percentage == 0, "the game has unlocks before any event");
NpTrophy2Details details{};
Trophy(context, 1, &details);
Require(std::strcmp(details.name, "Ten") == 0 && std::strcmp(details.description, "Reach ten") == 0 && details.trophy_grade == 2 && details.group_id == -1 && !details.hidden &&
details.target.type == 0 && details.target.value == 0,
"trophy 1 does not report its name, grade and group");
Trophy(context, 6, &details);
Require(std::strcmp(details.name, "Secret") == 0 && std::strcmp(details.reward, "A hat") == 0 && details.trophy_grade == 4 && details.group_id == 1 && details.hidden && details.has_reward,
"trophy 6 does not report its hidden flag, reward and group");
Trophy(context, 0, &details);
Require(details.trophy_grade == 1 && std::strcmp(details.name, "All of them") == 0, "the platinum is not reported");
const auto steps = Trophy(context, 5, &details);
Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 0, "the progressive trophy does not report its target");
NpTrophy2Data unknown{};
Require(ThrowsInvalidArgument([context, &unknown] { sceNpTrophy2GetTrophyInfo(context, 1, 99, nullptr, &unknown); }), "a trophy that is not in the package was reported");
std::vector<NpTrophy2Details> list(10);
std::vector<NpTrophy2Data> listed(10);
std::uint32_t count = 0;
Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 10, list.data(), listed.data(), &count) == 0 && count == 8, "the trophy list does not hold every trophy");
for (std::uint32_t i = 0; i < count; ++i) Require(list[i].trophy_id == static_cast<int>(i) && listed[i].trophy_id == static_cast<int>(i), "the trophy list is not in id order");
Require(std::strcmp(list[5].name, "Five steps") == 0 && listed[5].progress.type == 1, "the trophy list does not carry names and progress");
Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 6, 5, list.data(), nullptr, &count) == 0 && count == 2 && list[0].trophy_id == 6 && list[1].trophy_id == 7,
"the trophy list does not start at its offset");
Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 8, 5, nullptr, listed.data(), &count) == 0 && count == 0, "the trophy list goes past its end");
NpTrophy2GroupDetails group{};
NpTrophy2GroupData groupData{};
Require(sceNpTrophy2GetGroupInfo(context, 1, 1, &group, &groupData) == 0 && std::strcmp(group.title, "Expansion") == 0 && group.num_trophies == 2 && group.num_bronze == 2 && groupData.group_id == 1,
"group 1 is not reported");
NpTrophy2GroupData unknownGroup{};
Require(ThrowsInvalidArgument([context, &unknownGroup] { sceNpTrophy2GetGroupInfo(context, 1, 7, nullptr, &unknownGroup); }), "a group that is not in the package was reported");
std::vector<NpTrophy2GroupDetails> groups(4);
Require(sceNpTrophy2GetGroupInfoArray(context, 1, 0, 4, groups.data(), nullptr, &count) == 0 && count == 2 && groups[0].group_id == -1 && std::strcmp(groups[0].title, "Synthetic Game") == 0 &&
groups[0].num_trophies == 6 && groups[1].group_id == 1,
"the group list does not hold the base game and group 1");
}
static void ReportPlaceholders() {
for (const std::uint32_t serviceLabel : {1u, 2u}) {
const auto context = TrophyContext(serviceLabel);
NpTrophy2GameDetails game{};
Game(context, &game);
NpTrophy2Details details{};
Trophy(context, 12, &details);
std::uint32_t count = 0;
NpTrophy2Data listed{};
Require(std::strcmp(game.title, "Game") == 0 && std::strcmp(details.name, "Trophy") == 0 &&
sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 10, nullptr, &listed, &count) == 0 && count == 1,
"a missing or malformed trophy package does not leave the placeholders");
}
}
int main() {
const auto previous = std::filesystem::current_path();
const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-data-" + std::to_string(std::random_device{}()));
std::filesystem::create_directories(root);
std::filesystem::current_path(root);
Write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})");
Write("app0/sce_sys/trophy2/trophy00.ucp", Ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMeta}}));
Write("app0/sce_sys/trophy2/trophy02.ucp", "not a package");
const auto context = TrophyContext(0);
ReportTrophyData(context);
ReportPlaceholders();
std::filesystem::current_path(previous);
std::filesystem::remove_all(root);
std::puts("Trophy data tests passed");
return 0;
}