Files
ppsspp/UI/GameScreen.cpp
T
Henrik RydgårdandClaude Opus 5 f0dafdee10 Show and remove installed game updates from the game info screen
An installed update silently replaces what the game boots, so the info
pane now says when there is one - version, size and where it lives - and
the context menu offers to remove it again.

Removing takes the whole PSP/GAME/<DISC_ID> folder when the update is all
that's in it. When a digital game shares the folder, only PBOOT.PBP goes,
since deleting the folder would take the game with it and nothing records
what the install wrote. The confirmation names the exact path either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018izZ1mGTWhz2RqeudqsDQR
2026-09-08 11:15:15 -06:00

821 lines
30 KiB
C++

// Copyright (c) 2013- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include "ppsspp_config.h"
#include "Common/Render/DrawBuffer.h"
#include "Common/UI/Context.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
#include "Common/UI/PopupScreens.h"
#include "Common/UI/Notice.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Data/Text/Parsers.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/File/FileUtil.h"
#include "Common/StringUtils.h"
#include "Common/System/System.h"
#include "Common/System/OSD.h"
#include "Common/System/Request.h"
#include "Common/System/NativeApp.h"
#include "Common/UI/ScreenManager.h"
#include "Core/Config.h"
#include "Core/Reporting.h"
#include "Core/System.h"
#include "Core/Loaders.h"
#include "Core/HLE/Plugins.h"
#include "Core/Util/GameDB.h"
#include "Core/Util/RecentFiles.h"
#include "Core/Util/PathUtil.h"
#include "Core/Util/VideoPlayer.h"
#include "UI/OnScreenDisplay.h"
#include "UI/Background.h"
#include "UI/CwCheatScreen.h"
#include "UI/EmuScreen.h"
#include "UI/GameScreen.h"
#include "UI/GameSettingsScreen.h"
#include "UI/GameInfoCache.h"
#include "UI/BaseScreens.h"
#include "UI/MiscScreens.h"
#include "UI/MainScreen.h"
#include "UI/BackgroundAudio.h"
#include "UI/SavedataScreen.h"
#include "UI/MiscViews.h"
// This is in an objective-C file.
#if PPSSPP_PLATFORM(IOS)
void copyDeepLinkForPath(std::string_view filePath);
#else
// dummy
void copyDeepLinkForPath(std::string_view) {}
#endif
constexpr GameInfoFlags g_desiredFlags = GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::PIC0 | GameInfoFlags::PIC1 | GameInfoFlags::ICON1_PMF | GameInfoFlags::UNCOMPRESSED_SIZE | GameInfoFlags::SIZE | GameInfoFlags::SAVEDATA_SIZE | GameInfoFlags::BUNDLED_UPDATE_INFO;
class PMFView : public UI::InertView {
public:
PMFView(std::string data, UI::LayoutParams *params) : UI::InertView(params), data_(data) {
startTime_ = Instant::Now();
player_ = pmf_create();
if (0 == pmf_init(player_, (const uint8_t *)data_.data(), data_.size(), &width_, &height_)) {
} else {
ERROR_LOG(Log::G3D, "Couldn't load pmf");
pmf_destroy(player_);
player_ = nullptr;
}
}
~PMFView() {
if (curFrame_) {
curFrame_->Release();
curFrame_ = nullptr;
}
if (player_) {
pmf_destroy(player_);
player_ = nullptr;
}
}
void DeviceLost() override {
if (curFrame_) {
curFrame_->Release();
curFrame_ = nullptr;
}
}
void GetContentDimensions(const UIContext &dc, float &w, float &h) const override {
// Just use the video size.
w = width_;
h = height_;
}
void Draw(UIContext &dc) override {
if (!player_) {
return;
}
Draw::DrawContext *draw = dc.GetDrawContext();
// Dimensions are capped by pmf_init, but use size_t arithmetic anyway
// so a regression can't overflow the allocation.
std::vector<u8> frame((size_t)width_ * (size_t)height_ * 4);
if (pmf_update(player_, startTime_.ElapsedSeconds(), frame.data())) {
if (curFrame_) {
curFrame_->Release();
curFrame_ = nullptr;
}
Draw::TextureDesc desc{};
desc.width = width_;
desc.height = height_;
desc.depth = 1;
desc.format = Draw::DataFormat::R8G8B8A8_UNORM;
desc.generateMips = false;
desc.tag = "pmf_frame";
desc.type = Draw::TextureType::LINEAR2D;
desc.mipLevels = 1;
desc.initData.push_back(frame.data());
curFrame_ = draw->CreateTexture(desc);
}
if (curFrame_) {
int dropsize = 5;
dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, bounds_.x - dropsize * 1.5f, bounds_.y - dropsize * 1.5f, bounds_.x2() + dropsize * 1.5f, bounds_.y2() + dropsize * 1.5f, 0xFF000000, 1.0f);
dc.Flush();
draw->BindTexture(0, curFrame_);
dc.Draw()->DrawTexRect(bounds_, 0, 0, 1.0f, 1.0f, 0xFFFFFFFF);
dc.Flush();
}
dc.RebindTexture();
}
private:
std::string data_;
Draw::Texture *curFrame_ = nullptr;
PMFPlayer *player_ = nullptr;
Instant startTime_;
int width_ = 0;
int height_ = 0;
};
GameScreen::GameScreen(const Path &gamePath, bool inGame) : UITwoPaneBaseDialogScreen(gamePath, TwoPaneFlags::SettingsToTheRight | TwoPaneFlags::CustomContextMenu), inGame_(inGame) {
g_BackgroundAudio.SetGame(gamePath);
System_PostUIMessage(UIMessage::GAME_SELECTED, gamePath.ToString());
// We want to re-fetch savedata size when opening this screen. We re-fetch PARAM_SFO because it's connected to updating the hasConfig bool.
constexpr GameInfoFlags refetchFlags = GameInfoFlags::SAVEDATA_SIZE | GameInfoFlags::PARAM_SFO;
info_ = g_gameInfoCache->GetInfo(NULL, gamePath_, g_desiredFlags, &knownFlags_, refetchFlags);
}
GameScreen::~GameScreen() {
if (CRC32string == "...") {
Reporting::CancelCRC();
}
System_PostUIMessage(UIMessage::GAME_SELECTED, "");
}
template <typename I> std::string int2hexstr(I w, size_t hex_len = sizeof(I) << 1) {
static const char* digits = "0123456789ABCDEF";
std::string rc(hex_len, '0');
for (size_t i = 0, j = (hex_len - 1) * 4; i < hex_len; ++i, j -= 4)
rc[i] = digits[(w >> j) & 0x0f];
return rc;
}
void GameScreen::update() {
UITwoPaneBaseDialogScreen::update();
GameInfoFlags hasFlags;
g_gameInfoCache->GetInfo(NULL, gamePath_, g_desiredFlags, &hasFlags);
bool recreate = false;
if (knownFlags_ != hasFlags) {
bool justAudio = ((u32)knownFlags_ ^ (u32)hasFlags) == (u32)GameInfoFlags::SND;
knownFlags_ = hasFlags;
// don't need to recreate if the audio loaded.
if (!justAudio) {
recreate = true;
}
}
// Has the user requested a CRC32?
if (CRC32string == "...") {
// Wait until the CRC32 is ready. It might take time on some devices.
const bool hasCRC = Reporting::HasCRC(gamePath_);
if (hasCRC != knownHasCRC_) {
knownHasCRC_ = hasCRC;
recreate = true;
}
}
if (recreate) {
RecreateViews();
}
}
static bool FileTypeSupportsCRC(IdentifiedFileType fileType) {
switch (fileType) {
case IdentifiedFileType::PSP_PBP:
case IdentifiedFileType::PSP_PBP_DIRECTORY:
case IdentifiedFileType::PSP_ISO_NP:
case IdentifiedFileType::PSP_ISO:
return true;
default:
return false;
}
}
static bool FileTypeHasIcon(IdentifiedFileType fileType) {
switch (fileType) {
case IdentifiedFileType::PSP_PBP:
case IdentifiedFileType::PSP_PBP_DIRECTORY:
case IdentifiedFileType::PSP_ISO_NP:
case IdentifiedFileType::PSP_ISO:
case IdentifiedFileType::PSP_UMD_VIDEO_ISO:
case IdentifiedFileType::PSP_DISC_DIRECTORY:
return true;
default:
return false;
}
}
static bool FileTypeIsPlayable(IdentifiedFileType fileType) {
switch (fileType) {
case IdentifiedFileType::ERROR_IDENTIFYING:
case IdentifiedFileType::UNKNOWN:
case IdentifiedFileType::PSX_ISO:
case IdentifiedFileType::PS2_ISO:
case IdentifiedFileType::PS3_ISO:
case IdentifiedFileType::UNKNOWN_BIN:
case IdentifiedFileType::UNKNOWN_ELF:
case IdentifiedFileType::UNKNOWN_ISO:
case IdentifiedFileType::NORMAL_DIRECTORY:
case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY:
case IdentifiedFileType::PSP_UMD_VIDEO_ISO:
// Reverse logic.
return false;
default:
return true;
}
}
void GameScreen::RefreshInstalledUpdate() {
hasInstalledUpdate_ = false;
// Homebrew reuses real disc IDs often enough that we'd get false positives.
if (isHomebrew_ || !(knownFlags_ & GameInfoFlags::PARAM_SFO)) {
return;
}
hasInstalledUpdate_ = FindInstalledGameUpdate(info_->id, &installedUpdate_);
}
void GameScreen::CreateContentViews(UI::ViewGroup *parent) {
if (!info_) {
// Shouldn't happen
return;
}
const bool portrait = GetDeviceOrientation() == DeviceOrientation::Portrait;
auto di = GetI18NCategory(I18NCat::DIALOG);
auto ga = GetI18NCategory(I18NCat::GAME);
// Information in the top left.
// Back button to the bottom left.
// Scrolling action menu to the right.
using namespace UI;
Margins actionMenuMargins(0, 15, 15, 0);
ScrollView *leftScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0f, Margins(8)));
ViewGroup *leftColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
leftScroll->Add(leftColumn);
parent->Add(leftScroll);
const bool fileTypeSupportCRC = FileTypeSupportsCRC(info_->fileType);
const bool fileTypeHasIcon = FileTypeHasIcon(info_->fileType);
// Need an explicit size here because homebrew uses screenshots as icons.
LinearLayout *mainGameInfo;
if (portrait) {
mainGameInfo = new LinearLayout(ORIENT_VERTICAL);
leftColumn->Add(new Spacer(8.0f));
if (fileTypeHasIcon && !(info_->icon.dataLoaded && info_->icon.data.empty())) {
leftColumn->Add(new GameImageView(gamePath_, GameInfoFlags::ICON, 2.0f, new LinearLayoutParams(144 * 2, 80 * 2, UI::Margins(0))));
}
leftColumn->Add(mainGameInfo);
} else {
mainGameInfo = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f));
ViewGroup *badgeHolder = new LinearLayout(ORIENT_HORIZONTAL);
if (fileTypeHasIcon && !(info_->icon.dataLoaded && info_->icon.data.empty())) {
badgeHolder->Add(new GameImageView(gamePath_, GameInfoFlags::ICON, 2.0f, new LinearLayoutParams(144 * 2, 80 * 2, UI::Margins(0))));
}
badgeHolder->Add(mainGameInfo);
leftColumn->Add(badgeHolder);
}
mainGameInfo->SetSpacing(3.0f);
if (pmf_player_available() && fileTypeHasIcon && (knownFlags_ & GameInfoFlags::ICON1_PMF) && !info_->icon1pmf.empty()) {
std::string pmfData;
{
std::lock_guard<std::mutex> guard(info_->lock);
pmfData = info_->icon1pmf;
}
if (!pmfData.empty()) {
leftColumn->Add(new PMFView(pmfData, new LinearLayoutParams(144 * 2, 80 * 2, UI::Margins(0, 10))));
}
}
std::vector<GameDBInfo> dbInfos;
const bool inGameDB = g_gameDB.GetGameInfos(info_->id_version, &dbInfos);
// Show the game ID title below the icon. The top title will be from the DB.
std::string title = info_->GetTitle();
if (knownFlags_ & GameInfoFlags::PARAM_SFO) {
TextView* tvTitle = mainGameInfo->Add(new TextView(title, ALIGN_LEFT | FLAG_WRAP_TEXT, false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
tvTitle->SetShadow(true);
std::string regionID = ReplaceAll(info_->id_version, "_", " v");
if (!regionID.empty()) {
regionID += ": ";
}
if (info_->region != GameRegion::UNKNOWN) {
regionID += GameRegionToString(info_->region);
} else {
regionID += IdentifiedFileTypeToString(info_->fileType);
}
TextView *tvID = mainGameInfo->Add(new TextView(regionID, ALIGN_LEFT | FLAG_WRAP_TEXT, true, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
tvID->SetShadow(true);
if (!info_->errorString.empty()) {
mainGameInfo->Add(new NoticeView(NoticeLevel::WARN, info_->errorString, ""));
}
// Add information about extra memory (used by some mods).
auto keys = info_->GetParamSFO().GetKeys();
for (auto &iter : keys) {
if (iter == "MEMSIZE") {
mainGameInfo->Add(new TextView(iter + StringFromFormat(": %d", info_->GetParamSFO().GetValueInt(iter))))->SetTextSize(UI::TextSize::Small);
}
// We already show the version elsewhere.
// else if (iter == "DISC_VERSION") {
// mainGameInfo->Add(new TextView(iter + ": " + info_->GetParamSFO().GetValueString(iter)));
// }
}
}
LinearLayout *infoLayout = new LinearLayout(ORIENT_VERTICAL, new AnchorLayoutParams(10, 200, NONE, NONE));
leftColumn->Add(infoLayout);
if (info_->fileType == IdentifiedFileType::PSP_UMD_VIDEO_ISO) {
auto er = GetI18NCategory(I18NCat::ERRORS);
leftColumn->Add(new NoticeView(NoticeLevel::INFO, er->T("PPSSPP doesn't support UMD Video."), ""));
leftColumn->Add(new Choice(di->T("More info"), ImageID("I_LINK_OUT_QUESTION"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)))->OnClick.Add([](UI::EventParams &e) {
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/docs/reference/umd-video/");
});
}
if ((knownFlags_ & GameInfoFlags::UNCOMPRESSED_SIZE) && (knownFlags_ & GameInfoFlags::SIZE)) {
auto st = GetI18NCategory(I18NCat::STORE); // Borrow the size string from here
char temp[256];
snprintf(temp, sizeof(temp), "%s: %s", st->T_cstr("Size"), NiceSizeFormat(info_->gameSizeOnDisk).c_str());
if (info_->gameSizeUncompressed != info_->gameSizeOnDisk) {
size_t len = strlen(temp);
snprintf(temp + len, sizeof(temp) - len, " (%s: %s)", ga->T_cstr("Uncompressed"), NiceSizeFormat(info_->gameSizeUncompressed).c_str());
}
TextView *tvGameSize = mainGameInfo->Add(new TextView(temp, ALIGN_LEFT, true, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
tvGameSize->SetShadow(true);
}
// Most game discs carry a firmware updater, which holds files we'd like to have, like the fonts.
if ((knownFlags_ & GameInfoFlags::BUNDLED_UPDATE_INFO) && info_->bundledUpdate.present) {
char temp[256];
snprintf(temp, sizeof(temp), "%s: %s, %s", ga->T_cstr("Firmware update on disc"),
info_->bundledUpdate.Describe().c_str(), NiceSizeFormat(info_->bundledUpdate.archiveSize).c_str());
TextView *tvUpdate = mainGameInfo->Add(new TextView(temp, ALIGN_LEFT, true, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
tvUpdate->SetShadow(true);
}
if ((knownFlags_ & GameInfoFlags::SAVEDATA_SIZE)) {
char temp[256];
if (info_->saveDataSize > 0) {
snprintf(temp, sizeof(temp), "%s: %s", ga->T_cstr("SaveData"), NiceSizeFormat(info_->saveDataSize).c_str());
TextView *tvSaveDataSize = infoLayout->Add(new TextView(temp, ALIGN_LEFT, true, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
tvSaveDataSize->SetShadow(true);
}
if (info_->installDataSize > 0) {
snprintf(temp, sizeof(temp), "%s: %1.2f %s", ga->T_cstr("InstallData"), (float)(info_->installDataSize) / 1024.f / 1024.f, ga->T_cstr("MB"));
TextView *tvInstallDataSize = infoLayout->Add(new TextView(temp, ALIGN_LEFT, true, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
tvInstallDataSize->SetShadow(true);
}
}
infoLayout->Add(new TextView(GetFriendlyPath(gamePath_), ALIGN_LEFT | FLAG_WRAP_TEXT, true, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)))->SetShadow(true);
std::string timeStr;
if (g_Config.TimeTracker().GetPlayedTimeString(info_->id, &timeStr)) {
LinearLayout *timeHoriz = infoLayout->Add(new LinearLayout(ORIENT_HORIZONTAL));
TextView *tvPlayTime = timeHoriz->Add(new TextView(timeStr, ALIGN_LEFT, true, new LinearLayoutParams(0.0f, Gravity::G_VCENTER)));
tvPlayTime->SetShadow(true);
tvPlayTime->SetText(timeStr);
auto di = GetI18NCategory(I18NCat::DIALOG);
Choice *btnResetTime = timeHoriz->Add(new Choice(di->T("Reset"), new LinearLayoutParams(0.0f, Gravity::G_VCENTER)));
btnResetTime->OnClick.Add([this, ga, timeStr, title](UI::EventParams &) {
auto di = GetI18NCategory(I18NCat::DIALOG);
auto gta = GetI18NCategory(I18NCat::GAME);
std::string id = info_->id;
std::string questionText(ga->T("Are you sure you want to reset the played time counter?"));
questionText += "\n";
questionText += timeStr;
screenManager()->push(
new UI::MessagePopupScreen(title, questionText, di->T("Reset"), di->T("Cancel"), [this, id](bool reset) {
if (reset) {
g_Config.TimeTracker().Reset(id);
RecreateViews();
}
}));
});
}
LinearLayout *crcHoriz = infoLayout->Add(new LinearLayout(ORIENT_HORIZONTAL));
if (fileTypeSupportCRC) {
if (Reporting::HasCRC(gamePath_)) {
auto rp = GetI18NCategory(I18NCat::REPORTING);
uint32_t crcVal = Reporting::RetrieveCRC(gamePath_);
std::string crc = StringFromFormat("%08X", crcVal);
// CRC button makes sense.
TextView *tvCRC = crcHoriz->Add(new TextView(ReplaceAll(rp->T("FeedbackCRCValue", "Disc CRC: %1"), "%1", crc), ALIGN_LEFT, true, new LinearLayoutParams(0.0, Gravity::G_VCENTER)));
tvCRC->SetShadow(true);
if (System_GetPropertyBool(SYSPROP_HAS_TEXT_CLIPBOARD)) {
Choice *tvCRCCopy = crcHoriz->Add(new Choice(ImageID("I_FILE_COPY"), new LinearLayoutParams(0.0, Gravity::G_VCENTER)));
tvCRCCopy->OnClick.Add([this](UI::EventParams &) {
auto di = GetI18NCategory(I18NCat::DIALOG);
u32 crc = Reporting::RetrieveCRC(gamePath_);
char buffer[16];
snprintf(buffer, sizeof(buffer), "%08X", crc);
System_CopyStringToClipboard(buffer);
// Success indication. Not worth a translatable string.
g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions(di->T("Copied to clipboard: %1"), buffer), 0.0f, "copyToClip");
});
}
// Let's check the CRC in the game database, looking up the ID and also matching the crc.
std::vector<GameDBInfo> dbInfos;
if ((knownFlags_ & GameInfoFlags::PARAM_SFO) && g_gameDB.GetGameInfos(info_->id_version, &dbInfos)) {
bool found = false;
for (const auto &dbInfo : dbInfos) {
if (dbInfo.crc == crcVal) {
found = true;
}
}
if (found) {
NoticeView *tvVerified = infoLayout->Add(new NoticeView(NoticeLevel::INFO, ga->T("ISO OK according to the ReDump project"), "", new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)));
tvVerified->SetLevel(NoticeLevel::SUCCESS);
}
}
} else if (!isHomebrew_) {
if ((knownFlags_ & GameInfoFlags::PARAM_SFO) && !inGameDB) {
// tvVerified_->SetText(ga->T("Game ID unknown - not in the ReDump database"));
// tvVerified_->SetVisibility(UI::V_VISIBLE);
// tvVerified_->SetLevel(NoticeLevel::WARN);
} else if ((knownFlags_ & GameInfoFlags::UNCOMPRESSED_SIZE) && info_->gameSizeUncompressed != 0) { // don't do this check if info_ still pending
bool found = false;
for (auto &dbInfo : dbInfos) {
// TODO: Doesn't take CSO/CHD into account.
if (info_->gameSizeUncompressed == dbInfo.size) {
found = true;
}
}
if (!found) {
// tvVerified_->SetText(ga->T("File size incorrect, bad or modified ISO"));
// tvVerified_->SetVisibility(UI::V_VISIBLE);
// tvVerified_->SetLevel(NoticeLevel::ERROR);
// INFO_LOG(Log::Loader, "File size %d not matching game DB", (int)info_->gameSizeUncompressed);
}
NoticeView *tvVerified = infoLayout->Add(new NoticeView(NoticeLevel::INFO, ga->T("Click \"Calculate CRC\" to verify ISO"), "", new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)));
tvVerified->SetVisibility(UI::V_VISIBLE);
tvVerified->SetLevel(NoticeLevel::INFO);
}
}
}
// An installed game update replaces the disc's executable, so it's worth saying so here -
// otherwise there's nothing in the UI to explain why a patched game is running.
RefreshInstalledUpdate();
if (hasInstalledUpdate_) {
infoLayout->Add(new ItemHeader(ga->T("Game update")));
std::string updateLine = installedUpdate_.title;
if (!installedUpdate_.appVer.empty()) {
const std::string version = ApplySafeSubstitutions(ga->T("Version %1"), installedUpdate_.appVer);
updateLine = updateLine.empty() ? version : updateLine + " - " + version;
}
if (updateLine.empty()) {
updateLine = ga->T("Installed");
}
updateLine += " - " + NiceSizeFormat(installedUpdate_.sizeOnDisk);
infoLayout->Add(new TextView(updateLine, ALIGN_LEFT, true))->SetBullet(true);
infoLayout->Add(new TextView(GetFriendlyPath(installedUpdate_.folder), ALIGN_LEFT | FLAG_WRAP_TEXT, true))->SetBullet(true);
}
// Show plugin info_, if any. Later might add checkboxes.
auto plugins = HLEPlugins::FindPlugins(info_->id, g_Config.sLanguageIni);
if (!plugins.empty()) {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
infoLayout->Add(new ItemHeader(sy->T("Plugins")));
for (const auto &plugin : plugins) {
infoLayout->Add(new TextView(plugin.name, ALIGN_LEFT, true))->SetBullet(true);
}
}
infoLayout->Add(new GameImageView(gamePath_, GameInfoFlags::PIC0, 2.0f, new LinearLayoutParams(UI::Margins(0))));
}
void GameScreen::CreateSettingsViews(UI::ViewGroup *rightColumn) {
using namespace UI;
auto di = GetI18NCategory(I18NCat::DIALOG);
auto ga = GetI18NCategory(I18NCat::GAME);
const bool fileTypeSupportCRC = FileTypeSupportsCRC(info_->fileType);
const bool portrait = GetDeviceOrientation() == DeviceOrientation::Portrait;
LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL);
rightColumnItems->SetSpacing(0.0f);
rightColumn->Add(rightColumnItems);
if (!inGame_ && FileTypeIsPlayable(info_->fileType)) {
rightColumnItems->Add(new Choice(ga->T("Play"), ImageID("I_PLAY")))->OnClick.Add([this](UI::EventParams &e) {
screenManager()->switchScreen(new EmuScreen(gamePath_));
});
}
if (!info_->id.empty() && !inGame_) {
if (info_->hasConfig) {
// Only show the Game Settings button here if the game has a config. Always showing it
// is confusing since it'll just control the global settings.
Choice *btnGameSettings = rightColumnItems->Add(new Choice(ga->T("Game Settings"), ImageID("I_GEAR")));
btnGameSettings->OnClick.Handle(this, &GameScreen::OnGameSettings);
Choice *btnDeleteGameConfig = rightColumnItems->Add(new Choice(ga->T("Delete Game Config"), ImageID("I_TRASHCAN")));
btnDeleteGameConfig->OnClick.Handle(this, &GameScreen::OnDeleteConfig);
} else {
Choice *btnCreateGameConfig = rightColumnItems->Add(new Choice(ga->T("Create Game Config"), ImageID("I_GEAR_STAR")));
btnCreateGameConfig->OnClick.Handle(this, &GameScreen::OnCreateConfig);
}
}
if (g_Config.bEnableCheats) {
auto pa = GetI18NCategory(I18NCat::PAUSE);
rightColumnItems->Add(new Choice(pa->T("Cheats"), ImageID("I_CHEAT")))->OnClick.Add([this](UI::EventParams &e) {
screenManager()->push(new CwCheatScreen(gamePath_));
});
}
isHomebrew_ = info_ && info_->region == GameRegion::HOMEBREW;
if (fileTypeSupportCRC && !isHomebrew_ && !Reporting::HasCRC(gamePath_) ) {
rightColumnItems->Add(new Choice(ga->T("Calculate CRC"), ImageID("I_CHECKMARK")))->OnClick.Add([this](UI::EventParams &) {
Reporting::QueueCRC(gamePath_);
CRC32string = "..."; // signal that we're waiting for it. Kinda ugly.
});
}
}
void GameScreen::CreateContextMenu(UI::ViewGroup *parent) {
using namespace UI;
auto di = GetI18NCategory(I18NCat::DIALOG);
auto ga = GetI18NCategory(I18NCat::GAME);
if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) {
parent->Add(new Choice(di->T("Show in folder"), ImageID("I_FOLDER")))->OnClick.Add([this](UI::EventParams &e) {
System_ShowFileInFolder(gamePath_);
});
}
// TODO: This is synchronous, bad!
if (!inGame_ && g_recentFiles.ContainsFile(gamePath_.ToString())) {
Choice *removeButton = parent->Add(new Choice(ga->T("Remove From Recent"), ImageID("I_UNPIN")));
removeButton->OnClick.Add([this](UI::EventParams &e) {
g_recentFiles.Remove(gamePath_.ToString());
System_PostUIMessage(UIMessage::GAME_SELECTED, "");
// TODO: We should be able to do TriggerFinish here, but unfortunately
// the screen manager still considers the popup dialog the current dialog.
screenManager()->switchScreen(new MainScreen());
});
}
if (System_GetPropertyBool(SYSPROP_HAS_DEEP_LINKS)) {
Choice *btnCopyDeepLink = parent->Add(new Choice(di->T("Copy deep link"), ImageID("I_FILE_COPY")));
btnCopyDeepLink->OnClick.Add([this](UI::EventParams &e) {
auto di = GetI18NCategory(I18NCat::DIALOG);
const std::string deepLink = gamePath_.ToString();
copyDeepLinkForPath(deepLink);
// Success indication. Not worth a translatable string.
g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions(di->T("Copied to clipboard: %1"), deepLink), 0.0f, "copyToClip");
});
}
if (info_->saveDataSize) {
Choice *btnDeleteSaveData = new Choice(ga->T("Delete Save Data"), ImageID("I_TRASHCAN"));
parent->Add(btnDeleteSaveData)->OnClick.Handle(this, &GameScreen::OnDeleteSaveData);
}
if (info_->pic1.texture) {
Choice *btnSetBackground = parent->Add(new Choice(ga->T("Use background as UI background"), ImageID("I_DISPLAY")));
btnSetBackground->OnClick.Handle(this, &GameScreen::OnSetBackground);
}
if ((knownFlags_ & GameInfoFlags::PARAM_SFO) && System_GetPropertyBool(SYSPROP_CAN_CREATE_SHORTCUT)) {
parent->Add(new Choice(ga->T("Create Shortcut"), ImageID("I_SHORTCUT")))->OnClick.Add([this](UI::EventParams &e) {
GameInfoFlags hasFlags;
std::shared_ptr<GameInfo> info_ = g_gameInfoCache->GetInfo(NULL, gamePath_, GameInfoFlags::PARAM_SFO, &hasFlags);
if (hasFlags & GameInfoFlags::PARAM_SFO) {
System_CreateGameShortcut(gamePath_, info_->GetTitle());
g_OSD.Show(OSDType::MESSAGE_SUCCESS, GetI18NCategory(I18NCat::DIALOG)->T("Desktop shortcut created"), 2.0f);
}
});
}
RefreshInstalledUpdate();
if (!inGame_ && hasInstalledUpdate_) {
Choice *btnDeleteUpdate = parent->Add(new Choice(ga->T("Delete Game Update"), ImageID("I_TRASHCAN")));
btnDeleteUpdate->OnClick.Handle(this, &GameScreen::OnDeleteGameUpdate);
}
// Don't want to be able to delete the game while it's running.
if (!inGame_) {
Choice *deleteChoice = parent->Add(new Choice(ga->T("Delete Game"), ImageID("I_WARNING")));
deleteChoice->OnClick.Handle(this, &GameScreen::OnDeleteGame);
}
}
void GameScreen::OnDeleteGameUpdate(UI::EventParams &e) {
if (!hasInstalledUpdate_) {
return;
}
auto di = GetI18NCategory(I18NCat::DIALOG);
auto ga = GetI18NCategory(I18NCat::GAME);
std::string prompt(ga->T("DeleteConfirmGameUpdate", "Do you really want to remove the installed update?\nThe game will go back to running the version on the disc."));
prompt += "\n\n";
// Say exactly what disappears - for a digital game the folder holds the game itself, so only
// the update's executable goes.
prompt += GetFriendlyPath(installedUpdate_.sharesFolderWithGame ? installedUpdate_.pbootPath : installedUpdate_.folder);
const bool trashAvailable = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN);
const InstalledGameUpdate update = installedUpdate_;
screenManager()->push(
new UI::MessagePopupScreen(ga->T("Delete Game Update"), prompt, trashAvailable ? di->T("Move to trash") : di->T("Delete"), di->T("Cancel"),
[this, update](bool yes) {
if (!yes) {
return;
}
if (!DeleteInstalledGameUpdate(update)) {
auto er = GetI18NCategory(I18NCat::ERRORS);
g_OSD.Show(OSDType::MESSAGE_ERROR, er->T("Failed to delete the game update"));
}
RecreateViews();
}));
}
void GameScreen::OnCreateConfig(UI::EventParams &e) {
if (!info_->Ready(GameInfoFlags::PARAM_SFO)) {
return;
}
g_Config.CreateGameConfig(info_->id);
g_Config.SaveGameConfig(info_->id, info_->GetTitle());
info_->hasConfig = true;
screenManager()->topScreen()->RecreateViews();
}
std::string_view GameScreen::GetTitle() const {
if (!info_->Ready(GameInfoFlags::PARAM_SFO)) {
return "";
}
// Don't look up homebrew in the database, sometimes they take IDs from games.
if (info_->fileType != IdentifiedFileType::PSP_PBP_DIRECTORY) {
if (knownFlags_ & GameInfoFlags::PARAM_SFO) {
titleCache_ = info_->GetDBTitle();
}
} else {
titleCache_ = info_->GetTitle();
}
return titleCache_;
}
void GameScreen::OnDeleteConfig(UI::EventParams &e) {
auto di = GetI18NCategory(I18NCat::DIALOG);
const bool trashAvailable = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN);
screenManager()->push(
new UI::MessagePopupScreen(di->T("Delete"), di->T("DeleteConfirmGameConfig", "Do you really want to delete the settings for this game?"),
trashAvailable ? di->T("Move to trash") : di->T("Delete"), di->T("Cancel"), [this](bool result) {
if (!result) {
return;
}
std::shared_ptr<GameInfo> info = g_gameInfoCache->GetInfo(nullptr, gamePath_, GameInfoFlags::PARAM_SFO);
if (!info->Ready(GameInfoFlags::PARAM_SFO)) {
return;
}
g_Config.DeleteGameConfig(info->id);
info->hasConfig = false;
RecreateViews();
}));
}
void GameScreen::OnGameSettings(UI::EventParams &e) {
std::shared_ptr<GameInfo> info_ = g_gameInfoCache->GetInfo(NULL, gamePath_, GameInfoFlags::PARAM_SFO);
if (info_ && info_->Ready(GameInfoFlags::PARAM_SFO)) {
std::string discID = info_->GetParamSFO().GetValueString("DISC_ID");
if ((discID.empty() || !info_->disc_total) && gamePath_.FilePathContainsNoCase("PSP/GAME/"))
discID = g_paramSFO.GenerateFakeID(gamePath_);
screenManager()->push(new GameSettingsScreen(gamePath_, discID, true));
}
}
void GameScreen::OnDeleteSaveData(UI::EventParams &e) {
std::shared_ptr<GameInfo> info_ = g_gameInfoCache->GetInfo(NULL, gamePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::SIZE);
if (info_) {
// Check that there's any savedata to delete
if (info_->saveDataSize) {
const bool trashAvailable = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN);
auto di = GetI18NCategory(I18NCat::DIALOG);
Path gamePath = gamePath_;
screenManager()->push(
new UI::MessagePopupScreen(di->T("Delete"), di->T("DeleteConfirmAll", "Do you really want to delete all\nyour save data for this game?"), trashAvailable ? di->T("Move to trash") : di->T("Delete"), di->T("Cancel"),
[this, gamePath](bool yes) {
if (yes) {
std::shared_ptr<GameInfo> info_ = g_gameInfoCache->GetInfo(NULL, gamePath, GameInfoFlags::PARAM_SFO);
info_->DeleteAllSaveData();
info_->saveDataSize = 0;
info_->installDataSize = 0;
RecreateViews();
}
}));
}
}
}
void GameScreen::OnDeleteGame(UI::EventParams &e) {
std::shared_ptr<GameInfo> info_ = g_gameInfoCache->GetInfo(NULL, gamePath_, GameInfoFlags::PARAM_SFO);
if (info_->Ready(GameInfoFlags::PARAM_SFO)) {
auto di = GetI18NCategory(I18NCat::DIALOG);
auto ga = GetI18NCategory(I18NCat::GAME);
std::string prompt;
prompt = di->T("DeleteConfirmGame", "Do you really want to delete this game\nfrom your device? You can't undo this.");
prompt += "\n\n" + GetFriendlyPath(gamePath_);
const bool trashAvailable = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN);
Path gamePath = gamePath_;
ScreenManager *sm = screenManager();
screenManager()->push(
new UI::MessagePopupScreen(ga->T("Delete Game"), prompt, trashAvailable ? di->T("Move to trash") : di->T("Delete"), di->T("Cancel"),
[sm, gamePath](bool yes) {
if (yes) {
std::shared_ptr<GameInfo> info_ = g_gameInfoCache->GetInfo(NULL, gamePath, GameInfoFlags::PARAM_SFO);
info_->Delete();
g_gameInfoCache->Clear();
g_recentFiles.Remove(gamePath.c_str());
sm->switchScreen(new MainScreen());
}
}));
}
}
void GameScreen::OnSetBackground(UI::EventParams &e) {
auto ga = GetI18NCategory(I18NCat::GAME);
std::shared_ptr<GameInfo> info = g_gameInfoCache->GetInfo(nullptr, gamePath_, GameInfoFlags::PIC1);
if (!info->Ready(GameInfoFlags::PIC1)) {
return;
}
GameInfoTex *pic = nullptr;
if (info->pic1.dataLoaded && info->pic1.data.size()) {
pic = &info->pic1;
}
if (pic) {
const Path bgPng = GetSysDirectory(DIRECTORY_SYSTEM) / "background.png";
File::WriteStringToFile(false, pic->data, bgPng);
}
// Reinitializes the UI background.
UIBackgroundShutdown();
}