From 91b3741a572cc50c5f62fb701ceb77af35d5e1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 5 May 2026 18:38:50 +0200 Subject: [PATCH] Implement a simple cheat database download facility --- Common/Log.h | 1 + Common/Log/LogManager.cpp | 1 + Common/UI/Notice.h | 6 +- Common/UI/PopupScreens.cpp | 1 + UI/CwCheatScreen.cpp | 143 +++++++++++++++++++++++++++++++-- UI/CwCheatScreen.h | 5 ++ UI/OnScreenDisplay.cpp | 4 - UI/UIAtlas.cpp | 1 + Windows/PPSSPP.vcxproj | 1 + Windows/PPSSPP.vcxproj.filters | 3 + assets/cheats.json | 9 +++ assets/ui_images/images.svg | 17 ++-- 12 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 assets/cheats.json diff --git a/Common/Log.h b/Common/Log.h index 1e672cec60..268bfc35fb 100644 --- a/Common/Log.h +++ b/Common/Log.h @@ -57,6 +57,7 @@ enum class Log { GeDebugger, UI, IAP, + CwCheats, sceAudio, sceCtrl, diff --git a/Common/Log/LogManager.cpp b/Common/Log/LogManager.cpp index 3c1a4b3bb6..dff5ecba86 100644 --- a/Common/Log/LogManager.cpp +++ b/Common/Log/LogManager.cpp @@ -96,6 +96,7 @@ static const char * const g_logTypeNames[] = { "GEDEBUGGER", "UI", "IAP", + "CWCHEATS", "SCEAUDIO", "SCECTRL", "SCEDISP", diff --git a/Common/UI/Notice.h b/Common/UI/Notice.h index e35710ae76..5962e77502 100644 --- a/Common/UI/Notice.h +++ b/Common/UI/Notice.h @@ -34,9 +34,6 @@ public: level_ = level; text_ = text; } - void SetSquishy(bool squishy) { - squishy_ = squishy; - } void SetWrapText(bool wrapText) { wrapText_ = wrapText; } @@ -53,8 +50,7 @@ private: std::string iconName_; NoticeLevel level_; mutable float height1_ = 0.0f; - bool squishy_ = false; - bool wrapText_ = false; + bool wrapText_ = true; }; ImageID GetOSDIcon(NoticeLevel level); diff --git a/Common/UI/PopupScreens.cpp b/Common/UI/PopupScreens.cpp index 325677df6b..b78dc024df 100644 --- a/Common/UI/PopupScreens.cpp +++ b/Common/UI/PopupScreens.cpp @@ -816,6 +816,7 @@ void AskForInput(ScreenManager *screenManager, RequesterToken token, UI::View *s PopupTextInputChoice::PopupTextInputChoice(RequesterToken token, std::string *value, std::string_view title, std::string_view placeholder, int maxLen, ScreenManager *screenManager, LayoutParams *layoutParams) : AbstractChoiceWithValueDisplay(title, layoutParams), screenManager_(screenManager), value_(value), placeHolder_(placeholder), maxLen_(maxLen), token_(token), restriction_(StringRestriction::None) { + _dbg_assert_(value); OnClick.Handle(this, &PopupTextInputChoice::HandleClick); } diff --git a/UI/CwCheatScreen.cpp b/UI/CwCheatScreen.cpp index 216c70d01e..4f949fe8f6 100644 --- a/UI/CwCheatScreen.cpp +++ b/UI/CwCheatScreen.cpp @@ -18,29 +18,132 @@ #include "ppsspp_config.h" #include "ext/xxhash.h" #include "Common/UI/UI.h" +#include "Common/UI/PopupScreens.h" #include "Common/Data/Text/I18n.h" #include "Common/Data/Encoding/Utf8.h" #include "Common/File/FileUtil.h" +#include "Common/Net/HTTPClient.h" #include "Common/StringUtils.h" #include "Common/System/System.h" #include "Common/System/Request.h" -#include "Common/UI/PopupScreens.h" #include "Core/System.h" #include "Core/Config.h" #include "Core/CwCheat.h" #include "Core/MIPS/JitCommon/JitCommon.h" - +#undef new +#ifdef SYSTEM_RAPIDJSON +#include +#else +#include "ext/rapidjson/include/rapidjson/document.h" +#endif +#include "Common/DbgNew.h" #include "UI/GameInfoCache.h" #include "UI/CwCheatScreen.h" #include "UI/MiscViews.h" static const int FILE_CHECK_FRAME_INTERVAL = 53; +constexpr char g_cheatDBListUrl[] = "https://metadata.ppsspp.org/cheats.json"; + static Path GetGlobalCheatFilePath() { return GetSysDirectory(DIRECTORY_CHEATS) / "cheat.db"; } +struct CheatDatabaseDownloadInfo { + std::string name; + std::string maintainer; + std::string url; +}; + +// Use rapidjson to parse the cheat database list. +std::vector ParseJson(std::string_view json) { + rapidjson::Document d; + d.Parse(json.data(), json.size()); + + std::vector downloads; + + if (d.HasParseError()) { + ERROR_LOG(Log::CwCheats, "Failed to parse cheat database list: %.*s", STR_VIEW(json)); + return downloads; + } + if (!d.IsObject()) { + ERROR_LOG(Log::CwCheats, "1"); + return downloads; + } + if (!d.HasMember("databases") || !d["databases"].IsArray()) { + ERROR_LOG(Log::CwCheats, "databases is not an array"); + return downloads; + } + + const auto& dbs = d["databases"]; + for (auto& db : dbs.GetArray()) { + if (!db.IsObject()) + continue; + CheatDatabaseDownloadInfo info; + if (!db.HasMember("name") || !db["name"].IsString()) + continue; + info.name = db["name"].GetString(); + if (!db.HasMember("maintainer") || !db["maintainer"].IsString()) + continue; + info.maintainer = db["maintainer"].GetString(); + if (!db.HasMember("url") || !db["url"].IsString()) + continue; + info.url = db["url"].GetString(); + downloads.push_back(info); + } + return downloads; +} + +class CwCheatDownloadPopupScreen : public UI::PopupScreen { +public: + CwCheatDownloadPopupScreen(); + + void CreatePopupContents(UI::ViewGroup *parent) override; + void update() override; + + const char *tag() const override { return "CwCheatDownloadScreen"; } + + const std::string &ChosenUrl() const { return chosenUrl_; } + +private: + std::shared_ptr cheatsRequest_; + std::string chosenUrl_; + std::vector cheatDbs_; +}; + +CwCheatDownloadPopupScreen::CwCheatDownloadPopupScreen() : PopupScreen(T(I18NCat::DIALOG, "Back")) { + cheatsRequest_ = g_DownloadManager.StartDownload(g_cheatDBListUrl, Path(), http::RequestFlags::KeepInMemory, nullptr, "cheatdblist"); +} + +void CwCheatDownloadPopupScreen::update() { + UI::PopupScreen::update(); + if (cheatsRequest_ && cheatsRequest_->Done()) { + std::string data; + cheatsRequest_->buffer().TakeAll(&data); + cheatDbs_ = ParseJson(data); + cheatsRequest_.reset(); + RecreateViews(); + } +} + +void CwCheatDownloadPopupScreen::CreatePopupContents(UI::ViewGroup *parent) { + using namespace UI; + auto cw = GetI18NCategory(I18NCat::CWCHEATS); + + if (File::Exists(GetGlobalCheatFilePath())) { + parent->Add(new NoticeView(NoticeLevel::WARN, cw->T("A cheat database already exists. Downloading a new one will overwrite it."), "")); + } + + for (auto &db : cheatDbs_) { + parent->Add(new Choice(db.name + " - " + db.maintainer, ImageID("I_DOWNLOAD")))->OnClick.Add([this, url = db.url](UI::EventParams &) { + INFO_LOG(Log::CwCheats, "Chosen cheat database URL: %s", url.c_str()); + chosenUrl_ = url; + TriggerFinish(DialogResult::DR_OK); + }); + } +} + CwCheatScreen::CwCheatScreen(const Path &gamePath) : UITwoPaneBaseDialogScreen(gamePath, TwoPaneFlags::Default) { } @@ -61,8 +164,7 @@ bool CwCheatScreen::TryLoadCheatInfo() { return false; } gameID = info->GetParamSFO().GetValueString("DISC_ID"); - if ((info->id.empty() || !info->disc_total) - && gamePath_.FilePathContainsNoCase("PSP/GAME/")) { + if ((info->id.empty() || !info->disc_total) && gamePath_.FilePathContainsNoCase("PSP/GAME/")) { gameID = g_paramSFO.GenerateFakeID(gamePath_); } @@ -100,6 +202,19 @@ bool CwCheatScreen::key(const KeyInput &input) { return UITwoPaneBaseDialogScreen::key(input); } +void CwCheatScreen::dialogFinished(const Screen *dialog, DialogResult result) { + if (auto downloadScreen = dynamic_cast(dialog)) { + if (downloadRequest_) { + ERROR_LOG(Log::CwCheats, "Download already in progress, ignoring new download request."); + return; + } + INFO_LOG(Log::CwCheats, "Starting download of cheat database from %s", downloadScreen->ChosenUrl().c_str()); + downloadRequest_ = g_DownloadManager.StartDownload(downloadScreen->ChosenUrl(), GetGlobalCheatFilePath(), http::RequestFlags::ProgressBar, nullptr, "cheatdbdownload"); + } else { + UITwoPaneBaseDialogScreen::dialogFinished(dialog, result); + } +} + void CwCheatScreen::CreateSettingsViews(UI::ViewGroup *leftColumn) { using namespace UI; auto cw = GetI18NCategory(I18NCat::CWCHEATS); @@ -109,6 +224,10 @@ void CwCheatScreen::CreateSettingsViews(UI::ViewGroup *leftColumn) { //leftColumn->Add(new Choice(cw->T("Add Cheat")))->OnClick.Handle(this, &CwCheatScreen::OnAddCheat); leftColumn->Add(new ItemHeader(cw->T("Import Cheats"))); + leftColumn->Add(new Choice(cw->T("Download database")))->OnClick.Add([this](UI::EventParams &) { + screenManager()->push(new CwCheatDownloadPopupScreen()); + }); + Path cheatPath = GetGlobalCheatFilePath(); std::string root = GetSysDirectory(DIRECTORY_MEMSTICK_ROOT).ToString(); @@ -222,6 +341,16 @@ void CwCheatScreen::update() { fileCheckCounter_ = 0; } + if (downloadRequest_ && downloadRequest_->Done()) { + if (!downloadRequest_->Failed()) { + INFO_LOG(Log::CwCheats, "Cheat database downloaded successfully to %s.", GetGlobalCheatFilePath().ToVisualString().c_str()); + RecreateViews(); + } else { + ERROR_LOG(Log::CwCheats, "Failed to download cheat database"); + } + downloadRequest_.reset(); + } + UIBaseDialogScreen::update(); } @@ -313,7 +442,7 @@ void CwCheatScreen::OnImportBrowse(UI::EventParams ¶ms) { return; } Path path(value); - INFO_LOG(Log::System, "Attempting to load cheats from: '%s'", path.ToVisualString().c_str()); + INFO_LOG(Log::CwCheats, "Attempting to load cheats from: '%s'", path.ToVisualString().c_str()); int cheatsFound = 0; ImportAndReport(path); }); @@ -327,12 +456,12 @@ void CwCheatScreen::OnImportCheat(UI::EventParams ¶ms) { bool CwCheatScreen::ImportCheats(const Path &cheatFile, int *cheatsFound) { FILE *in = File::OpenCFile(cheatFile, "rt"); if (!in) { - WARN_LOG(Log::Common, "Unable to open %s\n", cheatFile.c_str()); + WARN_LOG(Log::CwCheats, "Unable to open %s\n", cheatFile.c_str()); return false; } if (gameID_.length() != 9 || !engine_) { - WARN_LOG(Log::Common, "CWCHEAT: Incorrect ID(%s) - can't import cheats.", gameID_.c_str()); + WARN_LOG(Log::CwCheats, "CWCHEAT: Incorrect ID(%s) - can't import cheats.", gameID_.c_str()); return false; } diff --git a/UI/CwCheatScreen.h b/UI/CwCheatScreen.h index 6922aa26b6..84e2525bdc 100644 --- a/UI/CwCheatScreen.h +++ b/UI/CwCheatScreen.h @@ -26,6 +26,7 @@ #include "UI/BaseScreens.h" #include "UI/SimpleDialogScreen.h" #include "UI/MiscViews.h" +#include "Common/Net/HTTPClient.h" struct CheatFileInfo; class CWCheatEngine; @@ -58,6 +59,8 @@ protected: void CreateContentViews(UI::ViewGroup *) override; std::string_view GetTitle() const override; + void dialogFinished(const Screen *dialog, DialogResult result) override; + private: void OnCheckBox(int index); bool ImportCheats(const Path &cheatFile, int *cheatsFound); @@ -80,4 +83,6 @@ private: UI::ViewGroup *cheatList_ = nullptr; ViewSearch search_; + + std::shared_ptr downloadRequest_; }; diff --git a/UI/OnScreenDisplay.cpp b/UI/OnScreenDisplay.cpp index 1cc1923eca..271855fd7a 100644 --- a/UI/OnScreenDisplay.cpp +++ b/UI/OnScreenDisplay.cpp @@ -439,10 +439,6 @@ void NoticeView::GetContentDimensionsBySpec(const UIContext &dc, UI::MeasureSpec ApplyBoundBySpec(layoutWidth, horiz); const int align = wrapText_ ? FLAG_WRAP_TEXT : 0; MeasureNotice(dc, level_, text_, detailsText_, iconName_, align, layoutWidth, &w, &h, &height1_); - // Layout hack! Some weird problems with the layout that I can't figure out right now.. - if (squishy_) { - w = 50.0; - } } void NoticeView::Draw(UIContext &dc) { diff --git a/UI/UIAtlas.cpp b/UI/UIAtlas.cpp index 791c1d8fa6..c2ca1142d5 100644 --- a/UI/UIAtlas.cpp +++ b/UI/UIAtlas.cpp @@ -197,6 +197,7 @@ static const ImageMeta g_uiImageIDs[] = { {"I_ARCHIVE_ZIP", false}, {"I_ARCHIVE_7Z", false}, {"I_ARCHIVE_RAR", false}, + {"I_DOWNLOAD", false}, }; static std::string PNGNameFromID(std::string_view id) { diff --git a/Windows/PPSSPP.vcxproj b/Windows/PPSSPP.vcxproj index ed8a0029ef..3f62b6e383 100644 --- a/Windows/PPSSPP.vcxproj +++ b/Windows/PPSSPP.vcxproj @@ -1185,6 +1185,7 @@ true true + diff --git a/Windows/PPSSPP.vcxproj.filters b/Windows/PPSSPP.vcxproj.filters index 4bc460a744..40ca4d9637 100644 --- a/Windows/PPSSPP.vcxproj.filters +++ b/Windows/PPSSPP.vcxproj.filters @@ -845,6 +845,9 @@ assets + + assets + diff --git a/assets/cheats.json b/assets/cheats.json new file mode 100644 index 0000000000..39711f8053 --- /dev/null +++ b/assets/cheats.json @@ -0,0 +1,9 @@ +{ + "databases": [ + { + "name": "CWCheat-Database-Plus", + "maintainer": "Saramagrean", + "url": "https://raw.githubusercontent.com/Saramagrean/CWCheat-Database-Plus-/refs/heads/master/cheat.db" + } + ] +} diff --git a/assets/ui_images/images.svg b/assets/ui_images/images.svg index 8b1ecbdcb3..13b8100a8d 100644 --- a/assets/ui_images/images.svg +++ b/assets/ui_images/images.svg @@ -23,9 +23,9 @@ inkscape:pagecheckerboard="true" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="4" - inkscape:cx="361.75" - inkscape:cy="499.625" + inkscape:zoom="5.6568542" + inkscape:cx="331.63308" + inkscape:cy="448.74764" inkscape:window-width="3840" inkscape:window-height="2071" inkscape:window-x="-9" @@ -125,7 +125,7 @@ transform="matrix(0.51483133,0,0,0.48865723,85.611849,129.89633)" aria-label="SD" />