From 47fcb9562fd531350f7d9c847dda322c5f50d56d Mon Sep 17 00:00:00 2001 From: Megamouse Date: Fri, 17 Nov 2023 21:33:30 +0100 Subject: [PATCH] Trophy Manager: allow to lock/unlock trophies --- rpcs3/Emu/Cell/Modules/cellRtc.cpp | 52 ++++++++++++++- rpcs3/Emu/Cell/Modules/cellRtc.h | 1 + rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp | 10 ++- rpcs3/Loader/TROPUSR.cpp | 23 +++++-- rpcs3/Loader/TROPUSR.h | 27 ++++---- rpcs3/rpcs3qt/trophy_manager_dialog.cpp | 85 +++++++++++++++++++++++++ rpcs3/rpcs3qt/trophy_manager_dialog.h | 1 + 7 files changed, 177 insertions(+), 22 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellRtc.cpp b/rpcs3/Emu/Cell/Modules/cellRtc.cpp index 40c781665d..7845bcf952 100644 --- a/rpcs3/Emu/Cell/Modules/cellRtc.cpp +++ b/rpcs3/Emu/Cell/Modules/cellRtc.cpp @@ -807,10 +807,10 @@ CellRtcDateTime tick_to_date_time(u64 tick) bool exit_while = false; do { - bool leap = is_leap_year(years); - for (u32 m = 0; m <= 11; m++) + const bool leap = is_leap_year(years); + for (u32 m = 0; m < 12; m++) { - u8 daysinmonth = DAYS_IN_MONTH[m + (leap * 12)]; + const u8 daysinmonth = DAYS_IN_MONTH[m + (leap * 12)]; if (days_tmp >= daysinmonth) { months++; @@ -842,6 +842,52 @@ CellRtcDateTime tick_to_date_time(u64 tick) return date_time; } +u64 date_time_to_tick(CellRtcDateTime date_time) +{ + const auto get_days_in_year = [](u16 year, u16 months) -> u64 + { + const bool leap = is_leap_year(year); + u64 days = 0; + for (u16 m = 0; m < months; m++) + { + days += DAYS_IN_MONTH[m + (leap * 12)]; + } + return days; + }; + + u64 days = 0; + + if (date_time.day > 1) + { + // We only need the whole days before "this" day + days += date_time.day - 1ULL; + } + + if (date_time.month > 1) + { + // We only need the whole months before "this" month + days += get_days_in_year(date_time.year, date_time.month - 1ULL); + } + + if (date_time.year > 1) + { + // We only need the whole years before "this" year + // NOTE: tick_to_date_time starts counting with year 1, so count [1,n[ instead of [0,n-1[ + for (u16 year = 1; year < date_time.year; year++) + { + days += get_days_in_year(year, 12); + } + } + + u64 tick = date_time.microsecond + + u64{date_time.second} * 1000000ULL + + u64{date_time.minute} * 60ULL * 1000000ULL + + u64{date_time.hour} * 60ULL * 60ULL * 1000000ULL + + days * 24ULL * 60ULL * 60ULL * 1000000ULL; + + return tick; +} + error_code cellRtcSetTick(vm::ptr pTime, vm::cptr pTick) { cellRtc.todo("cellRtcSetTick(pTime=*0x%x, pTick=*0x%x)", pTime, pTick); diff --git a/rpcs3/Emu/Cell/Modules/cellRtc.h b/rpcs3/Emu/Cell/Modules/cellRtc.h index 7a8bad18e3..7450da9f1e 100644 --- a/rpcs3/Emu/Cell/Modules/cellRtc.h +++ b/rpcs3/Emu/Cell/Modules/cellRtc.h @@ -53,3 +53,4 @@ error_code cellRtcGetTick(vm::cptr pTime, vm::ptr error_code cellRtcGetCurrentTick(vm::ptr pTick); CellRtcDateTime tick_to_date_time(u64 tick); +u64 date_time_to_tick(CellRtcDateTime date_time); diff --git a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp index 7494ea49af..7277dc6875 100644 --- a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp +++ b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp @@ -1033,7 +1033,10 @@ error_code sceNpTrophyUnlockTrophy(u32 context, u32 handle, s32 trophyId, vm::pt sceNpTrophy.error("sceNpTrophyUnlockTrophy: Failed to get timestamp: 0x%x", +error); } - ctxt->tropusr->UnlockTrophy(trophyId, tick->tick, tick->tick); + 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"); @@ -1056,7 +1059,10 @@ error_code sceNpTrophyUnlockTrophy(u32 context, u32 handle, s32 trophyId, vm::pt } const std::string trophyPath = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name + "/TROPUSR.DAT"; - ctxt->tropusr->Save(trophyPath); + if (!ctxt->tropusr->Save(trophyPath)) + { + sceNpTrophy.error("sceNpTrophyUnlockTrophy: failed to save '%s'", trophyPath); + } if (g_cfg.misc.show_trophy_popups) { diff --git a/rpcs3/Loader/TROPUSR.cpp b/rpcs3/Loader/TROPUSR.cpp index 67b84961fd..55d6415955 100644 --- a/rpcs3/Loader/TROPUSR.cpp +++ b/rpcs3/Loader/TROPUSR.cpp @@ -31,9 +31,9 @@ std::shared_ptr trophy_xml_document::GetRoot() return trophy_base; } -TROPUSRLoader::load_result TROPUSRLoader::Load(const std::string& filepath, const std::string& configpath) +TROPUSRLoader::load_result TROPUSRLoader::Load(std::string_view filepath, std::string_view configpath) { - const std::string& path = vfs::get(filepath); + const std::string path = vfs::get(filepath); load_result res{}; @@ -143,7 +143,7 @@ bool TROPUSRLoader::LoadTables() } // TODO: TROPUSRLoader::Save deletes the TROPUSR and creates it again. This is probably very slow. -bool TROPUSRLoader::Save(const std::string& filepath) +bool TROPUSRLoader::Save(std::string_view filepath) { fs::pending_file temp(vfs::get(filepath)); @@ -160,7 +160,7 @@ bool TROPUSRLoader::Save(const std::string& filepath) return temp.commit(); } -bool TROPUSRLoader::Generate(const std::string& filepath, const std::string& configpath) +bool TROPUSRLoader::Generate(std::string_view filepath, std::string_view configpath) { fs::file config(vfs::get(configpath)); @@ -377,3 +377,18 @@ bool TROPUSRLoader::UnlockTrophy(u32 id, u64 timestamp1, u64 timestamp2) return true; } + +bool TROPUSRLoader::LockTrophy(u32 id) +{ + if (id >= m_table6.size()) + { + trp_log.warning("TROPUSRLoader::LockTrophy: Invalid id=%d", id); + return false; + } + + m_table6[id].trophy_state = 0; + m_table6[id].timestamp1 = 0; + m_table6[id].timestamp2 = 0; + + return true; +} diff --git a/rpcs3/Loader/TROPUSR.h b/rpcs3/Loader/TROPUSR.h index f1bac15bd8..50f8079c0e 100644 --- a/rpcs3/Loader/TROPUSR.h +++ b/rpcs3/Loader/TROPUSR.h @@ -81,10 +81,10 @@ class TROPUSRLoader std::vector m_table4; std::vector m_table6; - virtual bool Generate(const std::string& filepath, const std::string& configpath); - virtual bool LoadHeader(); - virtual bool LoadTableHeaders(); - virtual bool LoadTables(); + [[nodiscard]] bool Generate(std::string_view filepath, std::string_view configpath); + [[nodiscard]] bool LoadHeader(); + [[nodiscard]] bool LoadTableHeaders(); + [[nodiscard]] bool LoadTables(); public: virtual ~TROPUSRLoader() = default; @@ -95,17 +95,18 @@ public: bool success; }; - virtual load_result Load(const std::string& filepath, const std::string& configpath); - virtual bool Save(const std::string& filepath); + [[nodiscard]] load_result Load(std::string_view filepath, std::string_view configpath); + [[nodiscard]] bool Save(std::string_view filepath); - virtual u32 GetTrophiesCount() const; - virtual u32 GetUnlockedTrophiesCount() const; + [[nodiscard]] u32 GetTrophiesCount() const; + [[nodiscard]] u32 GetUnlockedTrophiesCount() const; - virtual u32 GetUnlockedPlatinumID(u32 trophy_id, const std::string& config_path); + [[nodiscard]] u32 GetUnlockedPlatinumID(u32 trophy_id, const std::string& config_path); - virtual u32 GetTrophyGrade(u32 id) const; - virtual u32 GetTrophyUnlockState(u32 id) const; - virtual u64 GetTrophyTimestamp(u32 id) const; + [[nodiscard]] u32 GetTrophyGrade(u32 id) const; + [[nodiscard]] u32 GetTrophyUnlockState(u32 id) const; + [[nodiscard]] u64 GetTrophyTimestamp(u32 id) const; - virtual bool UnlockTrophy(u32 id, u64 timestamp1, u64 timestamp2); + [[nodiscard]] bool UnlockTrophy(u32 id, u64 timestamp1, u64 timestamp2); + [[nodiscard]] bool LockTrophy(u32 id); }; diff --git a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp index 2ffed31222..addfe2fe8e 100644 --- a/rpcs3/rpcs3qt/trophy_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/trophy_manager_dialog.cpp @@ -860,6 +860,74 @@ void trophy_manager_dialog::ShowTrophyTableContextMenu(const QPoint& pos) menu->addMenu(copy_menu); } + const QTableWidgetItem* id_item = m_trophy_table->item(row, static_cast(gui::trophy_list_columns::id)); + const QTableWidgetItem* type_item = m_trophy_table->item(row, static_cast(gui::trophy_list_columns::type)); + if (id_item && type_item && !Emu.IsRunning()) + { + const int type = type_item->data(Qt::UserRole).toInt(); + const int trophy_id = id_item->text().toInt(); + const bool is_unlocked = m_trophies_db[db_ind]->trop_usr->GetTrophyUnlockState(trophy_id); + + QAction* lock_unlock_trophy = new QAction(is_unlocked ? tr("&Lock Trophy") : tr("&Unlock Trophy"), menu); + connect(lock_unlock_trophy, &QAction::triggered, this, [this, db_ind, trophy_id, is_unlocked, row, type]() + { + if (type == SCE_NP_TROPHY_GRADE_PLATINUM && !is_unlocked) + { + QMessageBox::information(this, tr("Action not permitted."), tr("Platinum trophies can only be unlocked ingame."), QMessageBox::Ok); + return; + } + + auto& db = m_trophies_db[db_ind]; + const std::string path = vfs::retrieve(db->path); + const std::string tropusr_path = path + "/TROPUSR.DAT"; + const std::string tropconf_path = path + "/TROPCONF.SFM"; + + // Reload trophy file just make sure it hasn't changed + if (!db->trop_usr->Load(tropusr_path, tropconf_path).success) + { + gui_log.error("Failed to load trophy file"); + return; + } + + u64 tick = 0; + + if (is_unlocked) + { + if (!db->trop_usr->LockTrophy(trophy_id)) + { + gui_log.error("Failed to lock trophy %d", trophy_id); + return; + } + } + else + { + tick = DateTimeToTick(QDateTime::currentDateTime()); + + if (!db->trop_usr->UnlockTrophy(trophy_id, tick, tick)) + { + gui_log.error("Failed to unlock trophy %d", trophy_id); + return; + } + } + if (!db->trop_usr->Save(tropusr_path)) + { + gui_log.error("Failed to save '%s': error=%s", path, fs::g_tls_error); + return; + } + if (QTableWidgetItem* lock_item = m_trophy_table->item(row, static_cast(gui::trophy_list_columns::is_unlocked))) + { + lock_item->setText(db->trop_usr->GetTrophyUnlockState(trophy_id) ? tr("Earned") : tr("Not Earned")); + } + if (QTableWidgetItem* date_item = m_trophy_table->item(row, static_cast(gui::trophy_list_columns::time_unlocked))) + { + date_item->setText(tick ? QLocale().toString(TickToDateTime(tick), gui::persistent::last_played_date_with_time_of_day_format) : tr("Unknown")); + date_item->setData(Qt::UserRole, QVariant::fromValue(tick)); + } + }); + menu->addSeparator(); + menu->addAction(lock_unlock_trophy); + } + menu->exec(m_trophy_table->viewport()->mapToGlobal(pos)); } @@ -1261,3 +1329,20 @@ QDateTime trophy_manager_dialog::TickToDateTime(u64 tick) Qt::TimeSpec::UTC); return datetime.toLocalTime(); } + +u64 trophy_manager_dialog::DateTimeToTick(QDateTime date_time) +{ + const QDateTime utc = date_time.toUTC(); + const QDate date = utc.date(); + const QTime time = utc.time(); + const CellRtcDateTime rtc_date = { + .year = static_cast(date.year()), + .month = static_cast(date.month()), + .day = static_cast(date.day()), + .hour = static_cast(time.hour()), + .minute = static_cast(time.minute()), + .second = static_cast(time.second()), + .microsecond = static_cast(time.msec() * 1000), + }; + return date_time_to_tick(rtc_date); +} diff --git a/rpcs3/rpcs3qt/trophy_manager_dialog.h b/rpcs3/rpcs3qt/trophy_manager_dialog.h index 1f15f3a24b..ead945114b 100644 --- a/rpcs3/rpcs3qt/trophy_manager_dialog.h +++ b/rpcs3/rpcs3qt/trophy_manager_dialog.h @@ -81,6 +81,7 @@ private: bool eventFilter(QObject *object, QEvent *event) override; static QDateTime TickToDateTime(u64 tick); + static u64 DateTimeToTick(QDateTime date_time); std::shared_ptr m_gui_settings;