#include "stdafx.h" #include "cellMsgDialog.h" #include "Emu/Cell/PPUModule.h" #include "Emu/Cell/timers.hpp" #include "Emu/IdManager.h" #include "Emu/System.h" #include "Emu/VFS.h" #include "Emu/system_config.h" #include "Loader/TROPUSR.h" #include "Loader/TRP.h" #include "util/rXml.h" #include "cellSysutil.h" #include "sceNp.h" #include "sceNpTrophy.h" #include "util/StrUtil.h" #include "cellos/sys_event.h" #include "cellos/sys_fs.h" #include "rx/asm.hpp" #include #include #include LOG_CHANNEL(sceNpTrophy); TrophyNotificationBase::~TrophyNotificationBase() {} struct trophy_context_t { static const u32 id_base = 1; static const u32 id_step = 1; static const u32 id_count = 4; SAVESTATE_INIT_POS(42); std::string trp_name; std::unique_ptr tropusr; bool read_only = false; trophy_context_t() = default; trophy_context_t(utils::serial& ar) : trp_name(ar.pop()) { std::string trophy_path = vfs::get(Emu.GetDir() + "TROPDIR/" + trp_name + "/TROPHY.TRP"); fs::file trp_stream(trophy_path); if (!trp_stream) { // Fallback trophy_path = vfs::get("/dev_bdvd/PS3_GAME/TROPDIR/" + trp_name + "/TROPHY.TRP"); trp_stream.open(trophy_path); } if (!ar.pop()) { ar(read_only); return; } ar(read_only); if (!trp_stream && g_cfg.savestate.state_inspection_mode) { return; } const std::string trophyPath = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + trp_name; tropusr = std::make_unique(); const std::string trophyUsrPath = trophyPath + "/TROPUSR.DAT"; const std::string trophyConfPath = trophyPath + "/TROPCONF.SFM"; ensure(tropusr->Load(trophyUsrPath, trophyConfPath).success); } void save(utils::serial& ar) { ar(trp_name, tropusr.operator bool(), read_only); } }; struct trophy_handle_t { static const u32 id_base = 1; static const u32 id_step = 1; static const u32 id_count = 4; SAVESTATE_INIT_POS(43); bool is_aborted = false; trophy_handle_t() = default; trophy_handle_t(utils::serial& ar) : is_aborted(ar) {} void save(utils::serial& ar) { ar(is_aborted); } }; struct sce_np_trophy_manager { shared_mutex mtx; atomic_t is_initialized = false; // Get context + check handle given static std::pair get_context_ex(u32 context, u32 handle, bool test_writeable = false) { decltype(get_context_ex(0, 0)) res{}; auto& [ctxt, error] = res; if (context < trophy_context_t::id_base || context >= trophy_context_t::id_base + trophy_context_t::id_count) { // Id was not in range of valid ids error = SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; return res; } ctxt = idm::check_unlocked(context); if (!ctxt) { error = SCE_NP_TROPHY_ERROR_UNKNOWN_CONTEXT; return res; } if (test_writeable && ctxt->read_only) { error = SCE_NP_TROPHY_ERROR_INVALID_CONTEXT; return res; } if (handle < trophy_handle_t::id_base || handle >= trophy_handle_t::id_base + trophy_handle_t::id_count) { error = SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; return res; } const auto hndl = idm::check_unlocked(handle); if (!hndl) { error = SCE_NP_TROPHY_ERROR_UNKNOWN_HANDLE; return res; } else if (hndl->is_aborted) { error = SCE_NP_TROPHY_ERROR_ABORT; return res; } return res; } SAVESTATE_INIT_POS(12); sce_np_trophy_manager() = default; sce_np_trophy_manager(utils::serial& ar) : is_initialized(ar) {} void save(utils::serial& ar) { ar(is_initialized); if (is_initialized) { USING_SERIALIZATION_VERSION(sceNpTrophy); } } }; template <> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](auto error) { switch (error) { STR_CASE(SCE_NP_TROPHY_ERROR_ALREADY_INITIALIZED); STR_CASE(SCE_NP_TROPHY_ERROR_NOT_INITIALIZED); STR_CASE(SCE_NP_TROPHY_ERROR_NOT_SUPPORTED); STR_CASE(SCE_NP_TROPHY_ERROR_CONTEXT_NOT_REGISTERED); STR_CASE(SCE_NP_TROPHY_ERROR_OUT_OF_MEMORY); STR_CASE(SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT); STR_CASE(SCE_NP_TROPHY_ERROR_EXCEEDS_MAX); STR_CASE(SCE_NP_TROPHY_ERROR_INSUFFICIENT); STR_CASE(SCE_NP_TROPHY_ERROR_UNKNOWN_CONTEXT); STR_CASE(SCE_NP_TROPHY_ERROR_INVALID_FORMAT); STR_CASE(SCE_NP_TROPHY_ERROR_BAD_RESPONSE); STR_CASE(SCE_NP_TROPHY_ERROR_INVALID_GRADE); STR_CASE(SCE_NP_TROPHY_ERROR_INVALID_CONTEXT); STR_CASE(SCE_NP_TROPHY_ERROR_PROCESSING_ABORTED); STR_CASE(SCE_NP_TROPHY_ERROR_ABORT); STR_CASE(SCE_NP_TROPHY_ERROR_UNKNOWN_HANDLE); STR_CASE(SCE_NP_TROPHY_ERROR_LOCKED); STR_CASE(SCE_NP_TROPHY_ERROR_HIDDEN); STR_CASE(SCE_NP_TROPHY_ERROR_CANNOT_UNLOCK_PLATINUM); STR_CASE(SCE_NP_TROPHY_ERROR_ALREADY_UNLOCKED); STR_CASE(SCE_NP_TROPHY_ERROR_INVALID_TYPE); STR_CASE(SCE_NP_TROPHY_ERROR_INVALID_HANDLE); STR_CASE(SCE_NP_TROPHY_ERROR_INVALID_NP_COMM_ID); STR_CASE(SCE_NP_TROPHY_ERROR_UNKNOWN_NP_COMM_ID); STR_CASE(SCE_NP_TROPHY_ERROR_DISC_IO); STR_CASE(SCE_NP_TROPHY_ERROR_CONF_DOES_NOT_EXIST); STR_CASE(SCE_NP_TROPHY_ERROR_UNSUPPORTED_FORMAT); STR_CASE(SCE_NP_TROPHY_ERROR_ALREADY_INSTALLED); STR_CASE(SCE_NP_TROPHY_ERROR_BROKEN_DATA); STR_CASE(SCE_NP_TROPHY_ERROR_VERIFICATION_FAILURE); STR_CASE(SCE_NP_TROPHY_ERROR_INVALID_TROPHY_ID); STR_CASE(SCE_NP_TROPHY_ERROR_UNKNOWN_TROPHY_ID); STR_CASE(SCE_NP_TROPHY_ERROR_UNKNOWN_TITLE); STR_CASE(SCE_NP_TROPHY_ERROR_UNKNOWN_FILE); STR_CASE(SCE_NP_TROPHY_ERROR_DISC_NOT_MOUNTED); STR_CASE(SCE_NP_TROPHY_ERROR_SHUTDOWN); STR_CASE(SCE_NP_TROPHY_ERROR_TITLE_ICON_NOT_FOUND); STR_CASE(SCE_NP_TROPHY_ERROR_TROPHY_ICON_NOT_FOUND); STR_CASE(SCE_NP_TROPHY_ERROR_INSUFFICIENT_DISK_SPACE); STR_CASE(SCE_NP_TROPHY_ERROR_ILLEGAL_UPDATE); STR_CASE(SCE_NP_TROPHY_ERROR_SAVEDATA_USER_DOES_NOT_MATCH); STR_CASE(SCE_NP_TROPHY_ERROR_TROPHY_ID_DOES_NOT_EXIST); STR_CASE(SCE_NP_TROPHY_ERROR_SERVICE_UNAVAILABLE); STR_CASE(SCE_NP_TROPHY_ERROR_UNKNOWN); } return unknown; }); } template <> void fmt_class_string::format(std::string& out, u64 arg) { const auto& sign = get_object(arg); fmt::append(out, "%s", sign.data); } template <> void fmt_class_string::format(std::string& out, u64 arg) { const auto& id = get_object(arg); const u8 term = id.data[9]; fmt::append(out, "{ data='%s', term='%s' (0x%x), num=%d, dummy=%d }", id.data, std::isprint(term) ? fmt::format("%c", term) : "", term, id.num, id.dummy); } // Helpers static error_code NpTrophyGetTrophyInfo(const trophy_context_t* ctxt, s32 trophyId, SceNpTrophyDetails* details, SceNpTrophyData* data); static void show_trophy_notification(const trophy_context_t* ctxt, s32 trophyId) { // Get icon for the notification. const std::string padded_trophy_id = fmt::format("%03u", trophyId); const std::string trophy_icon_path = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name + "/TROP" + padded_trophy_id + ".PNG"; fs::file trophy_icon_file = fs::file(vfs::get(trophy_icon_path)); std::vector trophy_icon_data; trophy_icon_file.read(trophy_icon_data, trophy_icon_file.size()); SceNpTrophyDetails details{}; if (const auto ret = NpTrophyGetTrophyInfo(ctxt, trophyId, &details, nullptr)) { sceNpTrophy.error("Failed to get info for trophy dialog. Error code 0x%x", +ret); } if (auto trophy_notification_dialog = Emu.GetCallbacks().get_trophy_notification_dialog()) { trophy_notification_dialog->ShowTrophyNotification(details, trophy_icon_data); } } // Functions error_code sceNpTrophyInit(vm::ptr pool, u32 poolSize, u32 containerId, u64 options) { sceNpTrophy.warning("sceNpTrophyInit(pool=*0x%x, poolSize=0x%x, " "containerId=0x%x, options=0x%llx)", pool, poolSize, containerId, options); auto& trophy_manager = g_fxo->get(); std::scoped_lock lock(trophy_manager.mtx); if (trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_ALREADY_INITIALIZED; } if (options > 0) { return SCE_NP_TROPHY_ERROR_NOT_SUPPORTED; } trophy_manager.is_initialized = true; return CELL_OK; } error_code sceNpTrophyTerm() { sceNpTrophy.warning("sceNpTrophyTerm()"); auto& trophy_manager = g_fxo->get(); std::scoped_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } idm::clear(); idm::clear(); trophy_manager.is_initialized = false; return CELL_OK; } error_code sceNpTrophyCreateHandle(vm::ptr handle) { sceNpTrophy.warning("sceNpTrophyCreateHandle(handle=*0x%x)", handle); auto& trophy_manager = g_fxo->get(); std::scoped_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } if (!handle) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } const u32 id = idm::make(); if (!id) { return SCE_NP_TROPHY_ERROR_EXCEEDS_MAX; } *handle = id; return CELL_OK; } error_code sceNpTrophyDestroyHandle(u32 handle) { sceNpTrophy.warning("sceNpTrophyDestroyHandle(handle=0x%x)", handle); auto& trophy_manager = g_fxo->get(); std::scoped_lock lock(trophy_manager.mtx); // TODO: find out if this is checked // if (!trophy_manager.is_initialized) //{ // return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; //} if (handle < trophy_handle_t::id_base || handle >= trophy_handle_t::id_base + trophy_handle_t::id_count) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } if (!idm::remove(handle)) { return SCE_NP_TROPHY_ERROR_UNKNOWN_HANDLE; } return CELL_OK; } error_code sceNpTrophyGetGameDetails() { UNIMPLEMENTED_FUNC(sceNpTrophy); return CELL_OK; } error_code sceNpTrophyAbortHandle(u32 handle) { sceNpTrophy.todo("sceNpTrophyAbortHandle(handle=0x%x)", handle); auto& trophy_manager = g_fxo->get(); std::scoped_lock lock(trophy_manager.mtx); // TODO: find out if this is checked // if (!trophy_manager.is_initialized) //{ // return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; //} if (handle < trophy_handle_t::id_base || handle >= trophy_handle_t::id_base + trophy_handle_t::id_count) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } const auto hndl = idm::check_unlocked(handle); if (!hndl) { return SCE_NP_TROPHY_ERROR_UNKNOWN_HANDLE; } // Once it is aborted it cannot be used anymore // TODO: Implement function abortion process maybe? (depends if its actually // make sense for some functions) hndl->is_aborted = true; return CELL_OK; } error_code sceNpTrophyCreateContext( vm::ptr context, vm::cptr commId, vm::cptr commSign, u64 options) { sceNpTrophy.warning("sceNpTrophyCreateContext(context=*0x%x, commId=*0x%x, " "commSign=*0x%x, options=0x%llx)", context, commId, commSign, options); if (!commSign) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } sceNpTrophy.notice("sceNpTrophyCreateContext(): commSign = %s", *commSign); auto& trophy_manager = g_fxo->get(); std::scoped_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } if (!context || !commId) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } if (options > SCE_NP_TROPHY_OPTIONS_CREATE_CONTEXT_READ_ONLY) { return SCE_NP_TROPHY_ERROR_NOT_SUPPORTED; } sceNpTrophy.warning("sceNpTrophyCreateContext(): commId = %s", *commId); // rough checks for further fmt::format call const s32 comm_num = commId->num; if (comm_num > 99) { return SCE_NP_TROPHY_ERROR_INVALID_NP_COMM_ID; } // NOTE: commId->term is unused in our code (at least until someone finds out // if we need to account for it) // Generate trophy context name, limited to 9 characters // Read once for thread-safety reasons std::string name_str(commId->data, 9); // resize the name if it was shorter than expected if (const auto pos = name_str.find_first_of('\0'); pos != std::string_view::npos) { name_str = name_str.substr(0, pos); } const SceNpCommunicationSignature commSign_data = *commSign; if (read_from_ptr>(commSign_data.data, 0) != NP_TROPHY_COMM_SIGN_MAGIC) { return SCE_NP_TROPHY_ERROR_INVALID_NP_COMM_ID; } if (std::any_of(&commSign_data.data[6], &commSign_data.data[6] + 6, FN(x != '\0'))) { // 6 padding bytes - must be 0 return SCE_NP_TROPHY_ERROR_INVALID_NP_COMM_ID; } if (read_from_ptr>(commSign_data.data, 4) != 0x100) { // Signifies version (1.00), although only one constant is allowed return SCE_NP_TROPHY_ERROR_INVALID_NP_COMM_ID; } // append the commId number as "_xx" std::string name = fmt::format("%s_%02d", name_str, comm_num); // create trophy context const auto ctxt = idm::make_ptr(); if (!ctxt) { return SCE_NP_TROPHY_ERROR_EXCEEDS_MAX; } // set trophy context parameters (could be passed to constructor through // make_ptr call) ctxt->trp_name = name; ctxt->read_only = !!(options & SCE_NP_TROPHY_OPTIONS_CREATE_CONTEXT_READ_ONLY); *context = idm::last_id(); // set current trophy name for trophy list overlay { current_trophy_name& current_id = g_fxo->get(); std::lock_guard lock(current_id.mtx); current_id.name = std::move(name); } return CELL_OK; } error_code sceNpTrophyDestroyContext(u32 context) { sceNpTrophy.warning("sceNpTrophyDestroyContext(context=0x%x)", context); auto& trophy_manager = g_fxo->get(); std::scoped_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } if (context < trophy_context_t::id_base || context >= trophy_context_t::id_base + trophy_context_t::id_count) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } if (!idm::remove(context)) { return SCE_NP_TROPHY_ERROR_UNKNOWN_CONTEXT; } return CELL_OK; } error_code sceNpTrophyRegisterContext(ppu_thread& ppu, u32 context, u32 handle, vm::ptr statusCb, vm::ptr arg, u64 options) { sceNpTrophy.warning("sceNpTrophyRegisterContext(context=0x%x, handle=0x%x, " "statusCb=*0x%x, arg=*0x%x, options=0x%llx)", context, handle, statusCb, arg, options); auto& trophy_manager = g_fxo->get(); std::shared_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt, error] = trophy_manager.get_context_ex(context, handle, true); const auto handle_ptr = idm::get_unlocked(handle); if (error) { return error; } if (!statusCb) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } if (options > SCE_NP_TROPHY_OPTIONS_REGISTER_CONTEXT_SHOW_ERROR_EXIT) { return SCE_NP_TROPHY_ERROR_NOT_SUPPORTED; } const auto on_error = [options]() { if (!!(options & SCE_NP_TROPHY_OPTIONS_REGISTER_CONTEXT_SHOW_ERROR_EXIT)) { static_cast(open_exit_dialog( "Error during trophy registration! The game will now be terminated.", true, msg_dialog_source::_sceNpTrophy)); } }; // open trophy pack file std::string trp_path = vfs::get(Emu.GetDir() + "TROPDIR/" + ctxt->trp_name + "/TROPHY.TRP"); fs::file stream(trp_path); if (!stream && Emu.GetCat() == "GD") { sceNpTrophy.warning("sceNpTrophyRegisterContext failed to open trophy file " "from boot path: '%s' (%s)", trp_path, fs::g_tls_error); trp_path = vfs::get("/dev_bdvd/PS3_GAME/TROPDIR/" + ctxt->trp_name + "/TROPHY.TRP"); stream.open(trp_path); } // check if exists and opened if (!stream) { const std::string msg = fmt::format("Failed to open trophy file: '%s' (%s)", trp_path, fs::g_tls_error); return {SCE_NP_TROPHY_ERROR_CONF_DOES_NOT_EXIST, msg}; } // TODO: // SCE_NP_TROPHY_STATUS_DATA_CORRUPT -> reinstall // SCE_NP_TROPHY_STATUS_REQUIRES_UPDATE -> reinstall (for example if a patch // has updates for the trophy data) SCE_NP_TROPHY_STATUS_CHANGES_DETECTED -> // reinstall (only possible in dev mode) const std::string trophyPath = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name; const s32 trp_status = fs::is_dir(vfs::get(trophyPath)) ? SCE_NP_TROPHY_STATUS_INSTALLED : SCE_NP_TROPHY_STATUS_NOT_INSTALLED; lock.unlock(); sceNpTrophy.notice( "sceNpTrophyRegisterContext(): Callback is being called (trp_status=%u)", trp_status); // "Ask permission" to install the trophy data. // The callback is called once and then if it returns >= 0 the cb is called // through events(coming from vsh) that are passed to the CB through // cellSysutilCheckCallback if (statusCb(ppu, context, trp_status, 0, 0, arg) < 0) { on_error(); return SCE_NP_TROPHY_ERROR_PROCESSING_ABORTED; } std::unique_lock lock2(trophy_manager.mtx); // Rerun error checks, the callback could have changed stuff by calling // sceNpTrophy functions internally if (!trophy_manager.is_initialized) { on_error(); return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt2, error2] = trophy_manager.get_context_ex(context, handle); if (error2) { // Recheck for any errors, such as if AbortHandle was called return error2; } // Paranoid checks: context/handler could have been destroyed and replaced // with new ones with the same IDs Return an error for such cases if (ctxt2 != ctxt) { on_error(); return SCE_NP_TROPHY_ERROR_UNKNOWN_CONTEXT; } if (handle_ptr.get() != idm::check_unlocked(handle)) { on_error(); return SCE_NP_TROPHY_ERROR_UNKNOWN_HANDLE; } TRPLoader trp(stream); if (!trp.LoadHeader()) { sceNpTrophy.error( "sceNpTrophyRegisterContext(): Failed to load trophy config header"); on_error(); return SCE_NP_TROPHY_ERROR_ILLEGAL_UPDATE; } // Rename or discard certain entries based on the files found const usz kTargetBufferLength = 31; char target[kTargetBufferLength + 1]{}; strcpy_trunc(target, fmt::format("TROP_%02d.SFM", static_cast(g_cfg.sys.language))); if (trp.ContainsEntry(target)) { trp.RemoveEntry("TROPCONF.SFM"); trp.RemoveEntry("TROP.SFM"); trp.RenameEntry(target, "TROPCONF.SFM"); } else if (trp.ContainsEntry("TROP.SFM")) { trp.RemoveEntry("TROPCONF.SFM"); trp.RenameEntry("TROP.SFM", "TROPCONF.SFM"); } else if (!trp.ContainsEntry("TROPCONF.SFM")) { sceNpTrophy.error( "sceNpTrophyRegisterContext(): Invalid/Incomplete trophy config"); on_error(); return SCE_NP_TROPHY_ERROR_ILLEGAL_UPDATE; } // Discard unnecessary TROP_XX.SFM files for (s32 i = 0; i <= 18; i++) { strcpy_trunc(target, fmt::format("TROP_%02d.SFM", i)); if (i != g_cfg.sys.language) { trp.RemoveEntry(target); } } if (!trp.Install(trophyPath)) { sceNpTrophy.error("sceNpTrophyRegisterContext(): Failed to install trophy " "context '%s' (%s)", trophyPath, fs::g_tls_error); on_error(); return SCE_NP_TROPHY_ERROR_ILLEGAL_UPDATE; } const auto& tropusr = ctxt->tropusr = std::make_unique(); const std::string trophyUsrPath = trophyPath + "/TROPUSR.DAT"; const std::string trophyConfPath = trophyPath + "/TROPCONF.SFM"; ensure(tropusr->Load(trophyUsrPath, trophyConfPath).success); // This emulates vsh sending the events and ensures that not 2 events are // processed at once const std::pair statuses[] = { {SCE_NP_TROPHY_STATUS_PROCESSING_SETUP, 3}, {SCE_NP_TROPHY_STATUS_PROCESSING_PROGRESS, ::narrow(tropusr->GetTrophiesCount()) - 1}, {SCE_NP_TROPHY_STATUS_PROCESSING_FINALIZE, 4}, {SCE_NP_TROPHY_STATUS_PROCESSING_COMPLETE, 0}}; lock2.unlock(); lv2_obj::sleep(ppu); // Create a counter which is destroyed after the function ends const auto queued = std::make_shared>(0); for (auto status : statuses) { // One status max per cellSysutilCheckCallback call *queued += status.second; for (s32 completed = 0; completed <= status.second; completed++) { sysutil_register_cb([statusCb, status, context, completed, arg, queued](ppu_thread& cb_ppu) -> s32 { // TODO: it is possible that we need to check the return value here as // well. statusCb(cb_ppu, context, status.first, completed, status.second, arg); if (queued && (*queued)-- == 1) { queued->notify_one(); } return 0; }); } u64 current = get_system_time(); const u64 until = current + 300'000; // If too much time passes just send the rest of the events anyway for (u32 old_value; current < until && (old_value = *queued); current = get_system_time()) { thread_ctrl::wait_on(*queued, old_value, until - current); if (ppu.is_stopped()) { return {}; } } } return CELL_OK; } error_code sceNpTrophyGetRequiredDiskSpace(u32 context, u32 handle, vm::ptr reqspace, u64 options) { sceNpTrophy.warning("sceNpTrophyGetRequiredDiskSpace(context=0x%x, " "handle=0x%x, reqspace=*0x%x, options=0x%llx)", context, handle, reqspace, options); if (!reqspace) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } if (options > 0) { return SCE_NP_TROPHY_ERROR_NOT_SUPPORTED; } auto& trophy_manager = g_fxo->get(); reader_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt, error] = trophy_manager.get_context_ex(context, handle); if (error) { return error; } u64 space = 0; if (!fs::is_dir(vfs::get("/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name))) { // open trophy pack file std::string trophy_path = vfs::get(Emu.GetDir() + "TROPDIR/" + ctxt->trp_name + "/TROPHY.TRP"); fs::file stream(trophy_path); if (!stream && Emu.GetCat() == "GD") { sceNpTrophy.warning("sceNpTrophyGetRequiredDiskSpace failed to open " "trophy file from boot path: '%s'", trophy_path); trophy_path = vfs::get("/dev_bdvd/PS3_GAME/TROPDIR/" + ctxt->trp_name + "/TROPHY.TRP"); stream.open(trophy_path); } // check if exists and opened if (!stream) { return {SCE_NP_TROPHY_ERROR_CONF_DOES_NOT_EXIST, trophy_path}; } TRPLoader trp(stream); if (trp.LoadHeader()) { space = trp.GetRequiredSpace(); } else { sceNpTrophy.error("sceNpTrophyGetRequiredDiskSpace(): Failed to load " "trophy header! (trp_name=%s)", ctxt->trp_name); } } else { sceNpTrophy.warning("sceNpTrophyGetRequiredDiskSpace(): Trophy config is " "already installed (trp_name=%s)", ctxt->trp_name); } sceNpTrophy.warning("sceNpTrophyGetRequiredDiskSpace(): reqspace is 0x%llx", space); *reqspace = space; return CELL_OK; } error_code sceNpTrophySetSoundLevel(u32 context, u32 handle, u32 level, u64 options) { sceNpTrophy.todo("sceNpTrophySetSoundLevel(context=0x%x, handle=0x%x, " "level=%d, options=0x%llx)", context, handle, level, options); if (level > 100 || level < 20) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } if (options > 0) { return SCE_NP_TROPHY_ERROR_NOT_SUPPORTED; } auto& trophy_manager = g_fxo->get(); reader_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt, error] = trophy_manager.get_context_ex(context, handle); if (error) { return error; } return CELL_OK; } error_code sceNpTrophyGetGameInfo(u32 context, u32 handle, vm::ptr details, vm::ptr data) { sceNpTrophy.warning("sceNpTrophyGetGameInfo(context=0x%x, handle=0x%x, " "details=*0x%x, data=*0x%x)", context, handle, details, data); auto& trophy_manager = g_fxo->get(); reader_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt, error] = trophy_manager.get_context_ex(context, handle); if (error) { return error; } if (!ctxt->tropusr) { // TODO: May return SCE_NP_TROPHY_ERROR_UNKNOWN_TITLE for older sdk version return SCE_NP_TROPHY_ERROR_CONTEXT_NOT_REGISTERED; } if (!details && !data) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } const std::string config_path = vfs::get("/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name + "/TROPCONF.SFM"); fs::file config(config_path); if (!config) { return {SCE_NP_TROPHY_ERROR_CONF_DOES_NOT_EXIST, config_path}; } if (details) *details = {}; if (data) *data = {}; trophy_xml_document doc{}; pugi::xml_parse_result res = doc.Read(config.to_string()); if (!res) { sceNpTrophy.error("sceNpTrophyGetGameInfo: Failed to read TROPCONF.SFM: %s", config_path); // TODO: return some error return CELL_OK; } std::shared_ptr trophy_base = doc.GetRoot(); if (!trophy_base) { sceNpTrophy.error("sceNpTrophyGetGameInfo: Failed to read TROPCONF.SFM " "(root is null): %s", config_path); // TODO: return some error return CELL_OK; } for (std::shared_ptr n = trophy_base->GetChildren(); n; n = n->GetNext()) { const std::string n_name = n->GetName(); if (details) { if (n_name == "title-name") { strcpy_trunc(details->title, n->GetNodeContent()); continue; } else if (n_name == "title-detail") { strcpy_trunc(details->description, n->GetNodeContent()); continue; } } if (n_name == "trophy") { if (details) { details->numTrophies++; switch (n->GetAttribute("ttype")[0]) { case 'B': details->numBronze++; break; case 'S': details->numSilver++; break; case 'G': details->numGold++; break; case 'P': details->numPlatinum++; break; } } if (data) { const u32 trophy_id = atoi(n->GetAttribute("id").c_str()); if (ctxt->tropusr->GetTrophyUnlockState(trophy_id)) { data->unlockedTrophies++; switch (n->GetAttribute("ttype")[0]) { case 'B': data->unlockedBronze++; break; case 'S': data->unlockedSilver++; break; case 'G': data->unlockedGold++; break; case 'P': data->unlockedPlatinum++; break; default: break; } } } } } return CELL_OK; } error_code sceNpTrophyGetLatestTrophies() { UNIMPLEMENTED_FUNC(sceNpTrophy); return CELL_OK; } error_code sceNpTrophyUnlockTrophy(ppu_thread& ppu, u32 context, u32 handle, s32 trophyId, vm::ptr platinumId) { sceNpTrophy.warning("sceNpTrophyUnlockTrophy(context=0x%x, handle=0x%x, " "trophyId=%d, platinumId=*0x%x)", context, handle, trophyId, platinumId); auto& trophy_manager = g_fxo->get(); reader_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt, error] = trophy_manager.get_context_ex(context, handle); if (error) { return error; } if (!ctxt->tropusr) { // TODO: May return SCE_NP_TROPHY_ERROR_UNKNOWN_TITLE for older sdk version return SCE_NP_TROPHY_ERROR_CONTEXT_NOT_REGISTERED; } if (trophyId < 0 || trophyId >= static_cast(ctxt->tropusr->GetTrophiesCount())) { return SCE_NP_TROPHY_ERROR_INVALID_TROPHY_ID; } if (ctxt->tropusr->GetTrophyGrade(trophyId) == SCE_NP_TROPHY_GRADE_PLATINUM) { return SCE_NP_TROPHY_ERROR_CANNOT_UNLOCK_PLATINUM; } if (ctxt->tropusr->GetTrophyUnlockState(trophyId)) { return SCE_NP_TROPHY_ERROR_ALREADY_UNLOCKED; } vm::var tick; if (error_code error = cellRtcGetCurrentTick(ppu, tick)) { sceNpTrophy.error("sceNpTrophyUnlockTrophy: Failed to get timestamp: 0x%x", +error); } if (ctxt->tropusr->UnlockTrophy(trophyId, tick->tick, tick->tick)) { sceNpTrophy.notice("Trophy %d unlocked", trophyId); } // TODO: Make sure that unlocking platinum trophies is properly implemented // and improve upon it const std::string& config_path = vfs::get("/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name + "/TROPCONF.SFM"); const u32 unlocked_platinum_id = ctxt->tropusr->GetUnlockedPlatinumID(trophyId, config_path); if (unlocked_platinum_id != SCE_NP_TROPHY_INVALID_TROPHY_ID) { sceNpTrophy.warning("sceNpTrophyUnlockTrophy: All requirements for " "unlocking the platinum trophy (ID = %d) were met.)", unlocked_platinum_id); if (ctxt->tropusr->UnlockTrophy(unlocked_platinum_id, tick->tick, tick->tick)) { sceNpTrophy.success("You unlocked a platinum trophy! Hooray!!!"); } } if (platinumId) { *platinumId = unlocked_platinum_id; sceNpTrophy.warning("sceNpTrophyUnlockTrophy: platinumId was set to %d", unlocked_platinum_id); } const std::string trophyPath = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name + "/TROPUSR.DAT"; if (!ctxt->tropusr->Save(trophyPath)) { sceNpTrophy.error("sceNpTrophyUnlockTrophy: failed to save '%s'", trophyPath); } if (g_cfg.misc.show_trophy_popups) { // Enqueue popup for the regular trophy show_trophy_notification(ctxt, trophyId); if (unlocked_platinum_id != SCE_NP_TROPHY_INVALID_TROPHY_ID) { // Enqueue popup for the holy platinum trophy show_trophy_notification(ctxt, unlocked_platinum_id); } } return CELL_OK; } error_code sceNpTrophyGetTrophyUnlockState(u32 context, u32 handle, vm::ptr flags, vm::ptr count) { sceNpTrophy.warning("sceNpTrophyGetTrophyUnlockState(context=0x%x, " "handle=0x%x, flags=*0x%x, count=*0x%x)", context, handle, flags, count); if (!flags || !count) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } auto& trophy_manager = g_fxo->get(); reader_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt, error] = trophy_manager.get_context_ex(context, handle); if (error) { return error; } TROPUSRLoader* tropusr = nullptr; TROPUSRLoader local_tropusr{}; if (ctxt->read_only) { const std::string trophyPath = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name; const std::string trophyUsrPath = trophyPath + "/TROPUSR.DAT"; const std::string trophyConfPath = trophyPath + "/TROPCONF.SFM"; if (local_tropusr.Load(trophyUsrPath, trophyConfPath).success) { tropusr = &local_tropusr; } else { // TODO: confirm *count = 0; *flags = {}; return CELL_OK; } } else { if (!ctxt->tropusr) { // TODO: May return SCE_NP_TROPHY_ERROR_UNKNOWN_TITLE for older sdk // version return SCE_NP_TROPHY_ERROR_CONTEXT_NOT_REGISTERED; } tropusr = ctxt->tropusr.get(); } ensure(tropusr); const u32 count_ = tropusr->GetTrophiesCount(); *count = count_; if (count_ > 128) sceNpTrophy.error( "sceNpTrophyGetTrophyUnlockState: More than 128 trophies detected!"); // Needs hw testing *flags = {}; // Pack up to 128 bools in u32 flag_bits[4] for (u32 id = 0; id < count_; id++) { if (tropusr->GetTrophyUnlockState(id)) flags->flag_bits[id / 32] |= 1 << (id % 32); else flags->flag_bits[id / 32] &= ~(1 << (id % 32)); } return CELL_OK; } error_code sceNpTrophyGetTrophyDetails() { UNIMPLEMENTED_FUNC(sceNpTrophy); return CELL_OK; } static error_code NpTrophyGetTrophyInfo(const trophy_context_t* ctxt, s32 trophyId, SceNpTrophyDetails* details, SceNpTrophyData* data) { if (!details && !data) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } if (!ctxt->tropusr) { // TODO: May return SCE_NP_TROPHY_ERROR_UNKNOWN_TITLE for older sdk version return SCE_NP_TROPHY_ERROR_CONTEXT_NOT_REGISTERED; } const std::string config_path = vfs::get("/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name + "/TROPCONF.SFM"); fs::file config(config_path); if (!config) { return {SCE_NP_TROPHY_ERROR_CONF_DOES_NOT_EXIST, config_path}; } SceNpTrophyDetails tmp_details{}; SceNpTrophyData tmp_data{}; trophy_xml_document doc{}; pugi::xml_parse_result res = doc.Read(config.to_string()); if (!res) { sceNpTrophy.error("sceNpTrophyGetGameInfo: Failed to read TROPCONF.SFM: %s", config_path); // TODO: return some error } auto trophy_base = doc.GetRoot(); if (!trophy_base) { sceNpTrophy.error("sceNpTrophyGetGameInfo: Failed to read TROPCONF.SFM " "(root is null): %s", config_path); // TODO: return some error } bool found = false; for (std::shared_ptr n = trophy_base ? trophy_base->GetChildren() : nullptr; n; n = n->GetNext()) { if (n->GetName() == "trophy" && (trophyId == atoi(n->GetAttribute("id").c_str()))) { found = true; const bool hidden = n->GetAttribute("hidden")[0] == 'y'; const bool unlocked = !!ctxt->tropusr->GetTrophyUnlockState(trophyId); if (hidden && !unlocked) // Trophy is hidden { return SCE_NP_TROPHY_ERROR_HIDDEN; } if (details) { tmp_details.trophyId = trophyId; tmp_details.hidden = hidden; switch (n->GetAttribute("ttype")[0]) { case 'B': tmp_details.trophyGrade = SCE_NP_TROPHY_GRADE_BRONZE; break; case 'S': tmp_details.trophyGrade = SCE_NP_TROPHY_GRADE_SILVER; break; case 'G': tmp_details.trophyGrade = SCE_NP_TROPHY_GRADE_GOLD; break; case 'P': tmp_details.trophyGrade = SCE_NP_TROPHY_GRADE_PLATINUM; break; default: break; } for (std::shared_ptr n2 = n->GetChildren(); n2; n2 = n2->GetNext()) { const std::string n2_name = n2->GetName(); if (n2_name == "name") { strcpy_trunc(tmp_details.name, n2->GetNodeContent()); } else if (n2_name == "detail") { strcpy_trunc(tmp_details.description, n2->GetNodeContent()); } } } if (data) { tmp_data.trophyId = trophyId; tmp_data.unlocked = unlocked; tmp_data.timestamp = ctxt->tropusr->GetTrophyTimestamp(trophyId); } break; } } if (!found) { return SCE_NP_TROPHY_ERROR_INVALID_TROPHY_ID; } if (details) { *details = tmp_details; } if (data) { *data = tmp_data; } return CELL_OK; } error_code sceNpTrophyGetTrophyInfo(u32 context, u32 handle, s32 trophyId, vm::ptr details, vm::ptr data) { sceNpTrophy.warning("sceNpTrophyGetTrophyInfo(context=0x%x, handle=0x%x, " "trophyId=%d, details=*0x%x, data=*0x%x)", context, handle, trophyId, details, data); if (trophyId < 0 || trophyId > 127) // max 128 trophies { return SCE_NP_TROPHY_ERROR_INVALID_TROPHY_ID; } auto& trophy_manager = g_fxo->get(); reader_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt, error] = trophy_manager.get_context_ex(context, handle); if (error) { return error; } return NpTrophyGetTrophyInfo(ctxt, trophyId, details ? details.get_ptr() : nullptr, data ? data.get_ptr() : nullptr); } error_code sceNpTrophyGetGameProgress(u32 context, u32 handle, vm::ptr percentage) { sceNpTrophy.warning( "sceNpTrophyGetGameProgress(context=0x%x, handle=0x%x, percentage=*0x%x)", context, handle, percentage); if (!percentage) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } auto& trophy_manager = g_fxo->get(); reader_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt, error] = trophy_manager.get_context_ex(context, handle); if (error) { return error; } if (!ctxt->tropusr) { // TODO: May return SCE_NP_TROPHY_ERROR_UNKNOWN_TITLE for older sdk version return SCE_NP_TROPHY_ERROR_CONTEXT_NOT_REGISTERED; } const u32 unlocked = ctxt->tropusr->GetUnlockedTrophiesCount(); const u32 trp_count = ctxt->tropusr->GetTrophiesCount(); // Round result to nearest (TODO: Check 0 trophies) *percentage = trp_count ? rx::rounded_div(unlocked * 100, trp_count) : 0; if (trp_count == 0 || trp_count > 128) { sceNpTrophy.warning("sceNpTrophyGetGameProgress(): Trophies count may be " "invalid or untested (%d)", trp_count); } return CELL_OK; } error_code sceNpTrophyGetGameIcon(u32 context, u32 handle, vm::ptr buffer, vm::ptr size) { sceNpTrophy.warning("sceNpTrophyGetGameIcon(context=0x%x, handle=0x%x, " "buffer=*0x%x, size=*0x%x)", context, handle, buffer, size); auto& trophy_manager = g_fxo->get(); reader_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt, error] = trophy_manager.get_context_ex(context, handle); if (error) { return error; } if (!size) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } // Try to get icon in current language first const std::string trophy_path = fmt::format("/dev_hdd0/home/%s/trophy/%s/", Emu.GetUsr(), ctxt->trp_name); fs::file icon_file(vfs::get(fmt::format( "%s/ICON0_%02d.PNG", trophy_path, static_cast(g_cfg.sys.language)))); if (!icon_file && !icon_file.open(vfs::get(fmt::format("%s/ICON0.PNG", trophy_path)))) { return SCE_NP_TROPHY_ERROR_UNKNOWN_FILE; } const u32 icon_size = ::size32(icon_file); if (buffer && *size >= icon_size) { lv2_file::op_read(icon_file, buffer, icon_size); } *size = icon_size; return CELL_OK; } error_code sceNpTrophyGetUserInfo() { UNIMPLEMENTED_FUNC(sceNpTrophy); return CELL_OK; } error_code sceNpTrophyGetTrophyIcon(u32 context, u32 handle, s32 trophyId, vm::ptr buffer, vm::ptr size) { sceNpTrophy.warning("sceNpTrophyGetTrophyIcon(context=0x%x, handle=0x%x, " "trophyId=%d, buffer=*0x%x, size=*0x%x)", context, handle, trophyId, buffer, size); auto& trophy_manager = g_fxo->get(); reader_lock lock(trophy_manager.mtx); if (!trophy_manager.is_initialized) { return SCE_NP_TROPHY_ERROR_NOT_INITIALIZED; } const auto [ctxt, error] = trophy_manager.get_context_ex(context, handle); if (error) { return error; } if (!size) { return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT; } if (!ctxt->tropusr) { // TODO: May return SCE_NP_TROPHY_ERROR_UNKNOWN_TITLE for older sdk version return SCE_NP_TROPHY_ERROR_CONTEXT_NOT_REGISTERED; } if (ctxt->tropusr->GetTrophiesCount() <= static_cast(trophyId)) { return SCE_NP_TROPHY_ERROR_INVALID_TROPHY_ID; } if (!ctxt->tropusr->GetTrophyUnlockState(trophyId)) { const std::string config_path = vfs::get("/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name + "/TROPCONF.SFM"); fs::file config(config_path); if (config) { trophy_xml_document doc{}; pugi::xml_parse_result res = doc.Read(config.to_string()); if (!res) { sceNpTrophy.error( "sceNpTrophyGetTrophyIcon: Failed to read TROPCONF.SFM: %s", config_path); // TODO: return some error } auto trophy_base = doc.GetRoot(); if (!trophy_base) { sceNpTrophy.error("sceNpTrophyGetTrophyIcon: Failed to read " "TROPCONF.SFM (root is null): %s", config_path); // TODO: return some error } for (std::shared_ptr n = trophy_base ? trophy_base->GetChildren() : nullptr; n; n = n->GetNext()) { if (n->GetName() == "trophy" && trophyId == atoi(n->GetAttribute("id").c_str()) && n->GetAttribute("hidden")[0] == 'y') { return SCE_NP_TROPHY_ERROR_HIDDEN; } } } else { // TODO: Maybe return SCE_NP_TROPHY_ERROR_CONF_DOES_NOT_EXIST } return SCE_NP_TROPHY_ERROR_LOCKED; } fs::file icon_file(vfs::get("/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name + fmt::format("/TROP%03d.PNG", trophyId))); if (!icon_file) { return SCE_NP_TROPHY_ERROR_UNKNOWN_FILE; } const u32 icon_size = ::size32(icon_file); if (buffer && *size >= icon_size) { lv2_file::op_read(icon_file, buffer, icon_size); } *size = icon_size; return CELL_OK; } DECLARE(ppu_module_manager::sceNpTrophy)("sceNpTrophy", []() { REG_FUNC(sceNpTrophy, sceNpTrophyGetGameProgress); REG_FUNC(sceNpTrophy, sceNpTrophyRegisterContext); REG_FUNC(sceNpTrophy, sceNpTrophyCreateHandle); REG_FUNC(sceNpTrophy, sceNpTrophySetSoundLevel); REG_FUNC(sceNpTrophy, sceNpTrophyGetRequiredDiskSpace); REG_FUNC(sceNpTrophy, sceNpTrophyDestroyContext); REG_FUNC(sceNpTrophy, sceNpTrophyInit); REG_FUNC(sceNpTrophy, sceNpTrophyAbortHandle); REG_FUNC(sceNpTrophy, sceNpTrophyGetGameInfo); REG_FUNC(sceNpTrophy, sceNpTrophyDestroyHandle); REG_FUNC(sceNpTrophy, sceNpTrophyGetGameDetails); REG_FUNC(sceNpTrophy, sceNpTrophyUnlockTrophy); REG_FUNC(sceNpTrophy, sceNpTrophyGetLatestTrophies); REG_FUNC(sceNpTrophy, sceNpTrophyTerm); REG_FUNC(sceNpTrophy, sceNpTrophyGetTrophyUnlockState); REG_FUNC(sceNpTrophy, sceNpTrophyGetUserInfo); REG_FUNC(sceNpTrophy, sceNpTrophyGetTrophyIcon); REG_FUNC(sceNpTrophy, sceNpTrophyCreateContext); REG_FUNC(sceNpTrophy, sceNpTrophyGetTrophyDetails); REG_FUNC(sceNpTrophy, sceNpTrophyGetTrophyInfo); REG_FUNC(sceNpTrophy, sceNpTrophyGetGameIcon); });