#include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" #include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include #include #include #include #include #include #include #include #include #include #include struct NpUniversalDataSystemEvent; struct NpUniversalDataSystemEventPropertyObject; extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); int APS5_VABI sceNpUniversalDataSystemRegisterContext(int, int, uint64_t); int APS5_VABI sceNpUniversalDataSystemCreateHandle(int*); int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); 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 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((value >> (8 * (size - 1 - i))) & 0xFF); } static std::string Ucp(const std::vector>& 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 void PostUnlock(int trophyId) { static int context = 0; static int handle = 0; if (context == 0) { Require(sceNpUniversalDataSystemCreateContext(&context, InitialUser, 0, 0) == 0, "UDS context was not created"); Require(sceNpUniversalDataSystemCreateHandle(&handle) == 0, "UDS handle was not created"); Require(sceNpUniversalDataSystemRegisterContext(context, handle, 0) == 0, "UDS context was not registered"); } NpUniversalDataSystemEventPropertyObject* properties = nullptr; Require(sceNpUniversalDataSystemCreateEventPropertyObject(&properties) == 0, "property object was not created"); Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", trophyId) == 0, "trophy id was not set"); NpUniversalDataSystemEvent* event = nullptr; Require(sceNpUniversalDataSystemCreateEvent("_UnlockTrophy", properties, &event, nullptr) == 0, "event was not created"); Require(sceNpUniversalDataSystemPostEvent(context, handle, event, 0) == 0, "event was not posted"); Require(sceNpUniversalDataSystemDestroyEvent(event) == 0, "event was not destroyed"); Require(sceNpUniversalDataSystemDestroyEventPropertyObject(properties) == 0, "property object was not destroyed"); } 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; } struct Unlocks { std::vector trophies; }; static void Remember(void* context, const NpTrophy2Unlock& unlock) { Require(unlock.titleId == "PPSA00000" && unlock.userId == InitialUser && unlock.serviceLabel == 0 && unlock.tick != 0, "an unlock does not carry its title, user, label and time"); static_cast(context)->trophies.push_back(unlock.trophyId); } 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 list(10); std::vector 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(i) && listed[i].trophy_id == static_cast(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 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 PostUnlocks(int context) { Unlocks unlocks; const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(Remember, &unlocks); for (const int trophyId : {1, 2, 3, 6}) PostUnlock(trophyId); const auto partway = Game(context); Require(!Trophy(context, 0).unlocked && partway.unlocked_trophies == 4 && partway.unlocked_gold == 1 && partway.unlocked_silver == 1 && partway.unlocked_bronze == 2 && partway.progress_percentage == (6 + 2 + 2) * 100 / (6 + 2 + 5), "the game does not count the unlocks by grade"); PostUnlock(4); Require(!Trophy(context, 0).unlocked, "the platinum unlocked before every trophy that names it"); for (const int trophyId : {5, 0, 42}) PostUnlock(trophyId); const auto refused = Game(context); Require(!Trophy(context, 5).unlocked && !Trophy(context, 0).unlocked && refused.unlocked_trophies == 5 && refused.unlocked_platinum == 0 && refused.unlocked_bronze == 3 && refused.progress_percentage == (6 + 2 + 3) * 100 / (6 + 2 + 5), "a progressive, platinum or unknown trophy posted through UDS unlocked, or changed the counts"); NpTrophy2RemoveUnlockListener_nid_no_patch(listener); Require(unlocks.trophies == (std::vector{1, 2, 3, 6, 4}), "the listener did not see each posted unlock once, in order, or saw a refused one"); } 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); PostUnlocks(context); ReportPlaceholders(); std::filesystem::current_path(previous); std::filesystem::remove_all(root); std::puts("Trophy data tests passed"); return 0; }