mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-22 00:22:57 -05:00
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.
189 lines
9.9 KiB
C++
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;
|
|
}
|