diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index a0880bc6fa..3993210bbd 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -22,6 +22,7 @@ #include "zlib.h" +#include "Common/File/DirListing.h" #include "Common/File/FileUtil.h" #include "Common/File/Path.h" #include "Common/Log.h" @@ -1049,6 +1050,120 @@ static bool ReadUpdaterPSAR(const Path &filename, std::vector *psar, std::st return false; } +// Adds up everything under a directory. Returns false if the directory isn't there at all, which +// is how the caller tells "no firmware" from "an empty one". +static bool ScanDirRecursive(const Path &dir, int *fileCount, u64 *totalSize) { + std::vector files; + if (!File::GetFilesInDir(dir, &files)) { + return false; + } + for (const File::FileInfo &file : files) { + if (file.isDirectory) { + ScanDirRecursive(file.fullName, fileCount, totalSize); + } else { + (*fileCount)++; + *totalSize += file.size; + } + } + return true; +} + +static int CountFilesInDir(const Path &dir) { + std::vector files; + if (!File::GetFilesInDir(dir, &files)) { + return 0; + } + int count = 0; + for (const File::FileInfo &file : files) { + if (!file.isDirectory) { + count++; + } + } + return count; +} + +// flash0:/vsh/etc/version.txt, as the firmware itself writes it: +// release:6.60: +// build:5455,0,3,1,0:builder@vsh-build6 +// system:57716@release_660,0x06060010: +// vsh:p6616@release_660,v58533@release_660,20110727: +// target:1:WorldWide +// The date at the end of the vsh line is the only date in there, and the target line's last +// field is the region the firmware was built for. +static void ParseVersionTxt(std::string_view contents, InstalledFirmwareInfo *info) { + std::vector lines; + SplitString(contents, '\n', lines); + for (std::string_view line : lines) { + line = StripSpaces(line); + if (startsWith(line, "release:")) { + std::string_view rest = line.substr(strlen("release:")); + const size_t colon = rest.find(':'); + info->version = std::string(colon == std::string_view::npos ? rest : rest.substr(0, colon)); + } else if (startsWith(line, "vsh:")) { + // The build date is the last comma-separated field, e.g. "...,20110727:". + std::string_view rest = line.substr(strlen("vsh:")); + if (!rest.empty() && rest.back() == ':') { + rest = rest.substr(0, rest.size() - 1); + } + const size_t comma = rest.rfind(','); + if (comma != std::string_view::npos) { + const std::string_view date = rest.substr(comma + 1); + if (date.size() == 8 && std::all_of(date.begin(), date.end(), [](char c) { return c >= '0' && c <= '9'; })) { + info->buildDate = StringFromFormat("%.*s-%.*s-%.*s", + 4, date.data(), 2, date.data() + 4, 2, date.data() + 6); + } + } + } else if (startsWith(line, "target:")) { + const size_t colon = line.rfind(':'); + if (colon != std::string_view::npos && colon + 1 < line.size()) { + info->target = std::string(line.substr(colon + 1)); + } + } + } +} + +void ReadInstalledFirmwareInfo(const Path &nandRoot, InstalledFirmwareInfo *info) { + *info = InstalledFirmwareInfo{}; + + const Path flash0 = nandRoot / "flash0"; + for (const char *dir : { "flash0", "flash1", "ipl" }) { + ScanDirRecursive(nandRoot / dir, &info->fileCount, &info->totalSize); + } + info->anythingInstalled = info->fileCount > 0; + if (!info->anythingInstalled) { + return; + } + + info->fontCount = CountFilesInDir(flash0 / "font"); + info->kernelModuleCount = CountFilesInDir(flash0 / "kd"); + info->hasVsh = File::Exists(flash0 / "vsh/module/vshmain.prx"); + + std::string versionTxt; + if (File::ReadTextFileToString(flash0 / "vsh/etc/version.txt", &versionTxt)) { + ParseVersionTxt(versionTxt, info); + } +} + +bool EraseInstalledFirmware(const Path &nandRoot, std::string *error) { + bool success = true; + for (const char *dir : { "flash0", "flash1", "ipl" }) { + const Path path = nandRoot / dir; + if (File::Exists(path) && !File::DeleteDirRecursively(path)) { + ERROR_LOG(Log::Loader, "Failed to erase %s", path.c_str()); + if (error) { + *error = "Couldn't erase " + path.ToString(); + } + success = false; + } + } + return success; +} + +bool FirmwareVersionSupportsVSH(std::string_view version) { + // See the module patches in sceKernelModule.cpp - they're tied to 6.61's exact offsets. + return version == "6.61"; +} + std::string BundledUpdateInfo::Describe() const { if (!present) { return std::string(); diff --git a/Core/Util/PSARUnpack.h b/Core/Util/PSARUnpack.h index f3ef8a5d18..b200955ed3 100644 --- a/Core/Util/PSARUnpack.h +++ b/Core/Util/PSARUnpack.h @@ -141,6 +141,35 @@ bool ReadBundledUpdateInfo(IFileSystem *fs, std::string_view pathPrefix, Bundled // decryption needed, so it's cheap enough to check every disc with. Empty if there's no updater. std::string ReadUpdaterVersion(const Path &filename); +// What's actually in the NAND directory right now. That can be anything from a handful of fonts +// we pulled off a game disc to a full firmware unpacked from an updater, so this reports what's +// there rather than assuming one or the other. +struct InstalledFirmwareInfo { + bool anythingInstalled = false; // flash0/flash1 exist and hold at least one file. + // From flash0:/vsh/etc/version.txt, which only a full firmware install has. Empty if all + // that's there is a partial install like the fonts. + std::string version; // "6.60" + std::string buildDate; // "2011-07-27". Empty if the file doesn't spell one out. + std::string target; // "WorldWide" + bool hasVsh = false; // flash0:/vsh/module/vshmain.prx - what launching the XMB needs. + int fontCount = 0; // Files in flash0:/font, which is all sceFont wants. + int kernelModuleCount = 0; // Files in flash0:/kd, which is what --disable-hle wants. + int fileCount = 0; + u64 totalSize = 0; +}; + +// Walks the NAND directory (the one holding flash0/flash1). A full firmware is only a few +// hundred files, so this is cheap, but it does read the whole tree. +void ReadInstalledFirmwareInfo(const Path &nandRoot, InstalledFirmwareInfo *info); + +// Wipes what's in the NAND directory: flash0, flash1 and ipl. Two firmwares can't be merged - +// files a newer one dropped would linger and still get loaded - so an install starts from empty. +bool EraseInstalledFirmware(const Path &nandRoot, std::string *error); + +// The firmware versions we can actually boot the VSH (XMB) on. Every other version loads, but +// the module patches it needs are version-specific, so it won't get anywhere. +bool FirmwareVersionSupportsVSH(std::string_view version); + // The same three, for the disc mounted as disc0: - i.e. the game that's running. These read // through the mounted filesystem instead of opening the image a second time, which also means // they work for the shapes that aren't an image at all, like a folder-based "disc". diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 3a43190dc3..d163777075 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -99,6 +99,8 @@ list(APPEND UISource UploadScreen.cpp CwCheatScreen.h CwCheatScreen.cpp + FirmwareScreen.h + FirmwareScreen.cpp InstallUpdateScreen.h InstallUpdateScreen.cpp InstallZipScreen.h diff --git a/UI/FirmwareScreen.cpp b/UI/FirmwareScreen.cpp new file mode 100644 index 0000000000..ab042c7376 --- /dev/null +++ b/UI/FirmwareScreen.cpp @@ -0,0 +1,181 @@ +// Copyright (c) 2026- 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 "Common/Data/Text/I18n.h" +#include "Common/Data/Text/Parsers.h" +#include "Common/Log.h" +#include "Common/StringUtils.h" +#include "Common/System/Request.h" +#include "Common/System/System.h" +#include "Common/UI/Notice.h" +#include "Common/UI/PopupScreens.h" +#include "Common/UI/ScreenManager.h" +#include "Common/UI/View.h" +#include "Common/UI/ViewGroup.h" + +#include "Core/Config.h" +#include "Core/System.h" +#include "Core/Util/PathUtil.h" + +#include "UI/FirmwareScreen.h" +#include "UI/InstallUpdateScreen.h" +#include "UI/MiscViews.h" + +std::string_view FirmwareScreen::GetTitle() const { + auto sy = GetI18NCategory(I18NCat::SYSTEM); + return sy->T("PSP Firmware"); +} + +void FirmwareScreen::BeforeCreateViews() { + nandRoot_ = GetSysDirectory(DIRECTORY_NAND); + ReadInstalledFirmwareInfo(nandRoot_, &info_); +} + +void FirmwareScreen::CreateContentViews(UI::ViewGroup *parent) { + using namespace UI; + + auto sy = GetI18NCategory(I18NCat::SYSTEM); + auto iz = GetI18NCategory(I18NCat::INSTALLZIP); + auto di = GetI18NCategory(I18NCat::DIALOG); + auto st = GetI18NCategory(I18NCat::STORE); // Borrow "Size" from here, like GameScreen does. + + LinearLayout *content = parent->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + + if (!info_.anythingInstalled) { + content->Add(new NoticeView(NoticeLevel::INFO, sy->T("No firmware installed"), + sy->T("FirmwareSources", "PPSSPP has its own replacements for the PSP's system files, so a firmware is optional. Installing one gets you the real fonts, and lets you launch the XMB. Most game discs carry a firmware updater - open a game's info screen to install from it."))); + content->Add(new TextWithImage(ImageID("I_FOLDER"), GetFriendlyPath(nandRoot_))); + return; + } + + if (!info_.version.empty()) { + std::string versionLine = std::string(sy->T("Firmware version")) + ": " + info_.version; + content->Add(new TextView(versionLine, ALIGN_LEFT, false))->SetBig(true); + if (!info_.buildDate.empty() || !info_.target.empty()) { + std::string details = info_.buildDate; + if (!info_.target.empty()) { + if (!details.empty()) { + details += " - "; + } + details += info_.target; + } + content->Add(new TextView(details, ALIGN_LEFT, false)); + } + } else { + // No flash0:/vsh/etc/version.txt, so this isn't a full firmware - most likely just the + // fonts, which is what installing from a running game's disc leaves behind. + content->Add(new NoticeView(NoticeLevel::INFO, sy->T("Partial firmware installed"), + sy->T("Some system files are present, but not a complete firmware"))); + } + + content->Add(new ItemHeader(sy->T("Contents"))); + content->Add(new InfoItem(sy->T("Fonts"), info_.fontCount)); + content->Add(new InfoItem(sy->T("Kernel modules"), info_.kernelModuleCount)); + content->Add(new InfoItem(sy->T("XMB (VSH)"), info_.hasVsh ? di->T("Yes") : di->T("No"))); + content->Add(new InfoItem(sy->T("Files"), info_.fileCount)); + content->Add(new InfoItem(st->T("Size"), NiceSizeFormat(info_.totalSize))); + + content->Add(new ItemHeader(iz->T("Install into folder"))); + content->Add(new TextView(GetFriendlyPath(nandRoot_), ALIGN_LEFT | FLAG_WRAP_TEXT, false)); +} + +void FirmwareScreen::CreateSettingsViews(UI::ViewGroup *parent) { + using namespace UI; + + auto sy = GetI18NCategory(I18NCat::SYSTEM); + auto iz = GetI18NCategory(I18NCat::INSTALLZIP); + + // Booting anything replaces the running game, and erasing the NAND out from under a game + // that has flash0 mounted is worse - so while a game is running, this screen is read-only. + const bool gameRunning = PSP_IsInited(); + + Choice *launch = parent->Add(new Choice(sy->T("Launch XMB"), ImageID("I_PLAY"))); + launch->OnClick.Add([this](UI::EventParams &) { + LaunchVSH(); + }); + launch->SetEnabled(!gameRunning && info_.hasVsh && FirmwareVersionSupportsVSH(info_.version)); + + if (info_.hasVsh && !FirmwareVersionSupportsVSH(info_.version)) { + parent->Add(new NoticeView(NoticeLevel::WARN, sy->T("XMB unsupported"), + sy->T("XMBUnsupportedVersion", "Only firmware 6.61 can boot the XMB so far"))); + } + + if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) { + Choice *install = parent->Add(new Choice(iz->T("Install PSP firmware update"), ImageID("I_FOLDER_UPLOAD"))); + install->OnClick.Add([this](UI::EventParams &) { + BrowseForUpdater(); + }); + install->SetEnabled(!gameRunning); + } + + if (info_.anythingInstalled) { + Choice *erase = parent->Add(new Choice(sy->T("Erase firmware"), ImageID("I_TRASHCAN"))); + erase->OnClick.Add([this](UI::EventParams &) { + AskToErase(); + }); + erase->SetEnabled(!gameRunning); + } + + if (System_GetPropertyBool(SYSPROP_HAS_OPEN_DIRECTORY)) { + parent->Add(new Choice(sy->T("Show firmware folder")))->OnClick.Add([this](UI::EventParams &) { + System_LaunchUrl(LaunchUrlType::LOCAL_FOLDER, nandRoot_.ToString()); + }); + } + + if (gameRunning) { + parent->Add(new NoticeView(NoticeLevel::INFO, + sy->T("FirmwareNeedsNoGame", "Stop the game to change the installed firmware"), "")); + } +} + +void FirmwareScreen::dialogFinished(const Screen *dialog, DialogResult result) { + // Coming back from the installer (or the erase confirmation), the NAND may well look + // different than it did when we scanned it. Rescanning is cheap, so just always redo it - + // BeforeCreateViews does the scan. + RecreateViews(); + UITwoPaneBaseDialogScreen::dialogFinished(dialog, result); +} + +void FirmwareScreen::LaunchVSH() { + System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, (nandRoot_ / "flash0/vsh/module/vshmain.prx").ToString()); +} + +void FirmwareScreen::BrowseForUpdater() { + auto iz = GetI18NCategory(I18NCat::INSTALLZIP); + System_BrowseForFile(GetRequesterToken(), iz->T("Install PSP firmware update"), BrowseFileType::BOOTABLE, + [this](std::string_view value, int) { + screenManager()->push(new InstallUpdateScreen(Path(value), "", false)); + }); +} + +void FirmwareScreen::AskToErase() { + auto sy = GetI18NCategory(I18NCat::SYSTEM); + auto di = GetI18NCategory(I18NCat::DIALOG); + + std::string question(sy->T("EraseFirmwareConfirm", "This deletes everything in the NAND folder, including the fonts.")); + screenManager()->push(new UI::MessagePopupScreen(sy->T("Erase firmware"), question, di->T("Delete"), di->T("Cancel"), + [this](bool erase) { + if (!erase) { + return; + } + std::string error; + if (!EraseInstalledFirmware(nandRoot_, &error)) { + ERROR_LOG(Log::Loader, "Failed to erase the firmware: %s", error.c_str()); + } + RecreateViews(); + })); +} diff --git a/UI/FirmwareScreen.h b/UI/FirmwareScreen.h new file mode 100644 index 0000000000..9f3e495207 --- /dev/null +++ b/UI/FirmwareScreen.h @@ -0,0 +1,52 @@ +// Copyright (c) 2026- 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/. + +#pragma once + +#include "Common/File/Path.h" +#include "Common/UI/UIScreen.h" +#include "Common/UI/ViewGroup.h" + +#include "Core/Util/PSARUnpack.h" + +#include "UI/SimpleDialogScreen.h" + +// What's in PSP/NAND - which can be nothing at all, just the fonts we pulled off some game's +// disc, or a full firmware unpacked from an updater. Also the place to launch the XMB from, +// since that's what a full firmware buys you. +class FirmwareScreen : public UITwoPaneBaseDialogScreen { +public: + FirmwareScreen(const Path &gamePath) + : UITwoPaneBaseDialogScreen(gamePath, TwoPaneFlags::SettingsToTheRight | TwoPaneFlags::ContentsCanScroll) {} + + const char *tag() const override { return "Firmware"; } + +protected: + std::string_view GetTitle() const override; + void BeforeCreateViews() override; + void CreateSettingsViews(UI::ViewGroup *parent) override; + void CreateContentViews(UI::ViewGroup *parent) override; + void dialogFinished(const Screen *dialog, DialogResult result) override; + +private: + void LaunchVSH(); + void BrowseForUpdater(); + void AskToErase(); + + Path nandRoot_; + InstalledFirmwareInfo info_; +}; diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 5d158d31ce..31116c339a 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -50,6 +50,7 @@ #include "UI/DevScreens.h" #include "UI/DeveloperToolsScreen.h" #include "UI/DisplayLayoutScreen.h" +#include "UI/FirmwareScreen.h" #include "UI/RemoteISOScreen.h" #include "UI/SavedataScreen.h" #include "UI/SystemInfoScreen.h" @@ -1138,6 +1139,9 @@ void GameSettingsScreen::CreateToolsSettings(UI::ViewGroup *tools) { tools->Add(new Choice(ri->T("Remote disc streaming")))->OnClick.Add([=](UI::EventParams &) { screenManager()->push(new RemoteISOScreen(gamePath_)); }); + tools->Add(new Choice(sy->T("PSP Firmware")))->OnClick.Add([=](UI::EventParams &) { + screenManager()->push(new FirmwareScreen(gamePath_)); + }); } void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) { diff --git a/UI/InstallUpdateScreen.cpp b/UI/InstallUpdateScreen.cpp index abe8a4de2b..6a0e98e453 100644 --- a/UI/InstallUpdateScreen.cpp +++ b/UI/InstallUpdateScreen.cpp @@ -36,8 +36,8 @@ #include "UI/MiscViews.h" #include "UI/EmuScreen.h" -InstallUpdateScreen::InstallUpdateScreen(const Path &path, std::string_view title) - : UISimpleBaseDialogScreen(Path(), SimpleDialogFlags::ContentsCanScroll), path_(path), title_(title) { +InstallUpdateScreen::InstallUpdateScreen(const Path &path, std::string_view title, bool allowRun) + : UISimpleBaseDialogScreen(Path(), SimpleDialogFlags::ContentsCanScroll), path_(path), title_(title), allowRun_(allowRun) { destination_ = GetSysDirectory(DIRECTORY_NAND); File::FileInfo fileInfo; @@ -80,7 +80,8 @@ void InstallUpdateScreen::CreateDialogViews(UI::ViewGroup *parent) { container->Add(new TextView(GetFriendlyPath(destination_)))->SetAlign(FLAG_WRAP_TEXT); if (overwrites_) { - container->Add(new NoticeView(NoticeLevel::WARN, di->T("Confirm Overwrite"), "")); + container->Add(new NoticeView(NoticeLevel::WARN, di->T("Confirm Overwrite"), + iz->T("The firmware already installed will be erased first"))); } container->Add(new Spacer(12.0f)); @@ -90,10 +91,12 @@ void InstallUpdateScreen::CreateDialogViews(UI::ViewGroup *parent) { StartInstall(); }); - Choice *runChoice = container->Add(new Choice(dev->T("Run"), ImageID("I_PLAY"))); - runChoice->OnClick.Add([this](UI::EventParams &e) { - screenManager()->switchScreen(new EmuScreen(path_)); - }); + if (allowRun_) { + Choice *runChoice = container->Add(new Choice(dev->T("Run"), ImageID("I_PLAY"))); + runChoice->OnClick.Add([this](UI::EventParams &e) { + screenManager()->switchScreen(new EmuScreen(path_)); + }); + } progressBar_ = container->Add(new ProgressBar()); progressBar_->SetVisibility(V_GONE); @@ -125,6 +128,13 @@ void InstallUpdateScreen::StartInstall() { options.progress = [state](float progress) { state->progress = progress; }; + // Two firmwares can't be merged - a file the new one doesn't have would linger and still + // get loaded - so start from an empty NAND. + if (!EraseInstalledFirmware(destination, &state->error)) { + state->success = false; + state->done = true; + return; + } state->success = UnpackUpdater(path, destination, options, &state->stats, &state->error); if (state->success && state->stats.written == 0) { // Nothing came out, so the archive had no file list for the model we asked for - diff --git a/UI/InstallUpdateScreen.h b/UI/InstallUpdateScreen.h index e4fe1c5b5a..7c7933fc6e 100644 --- a/UI/InstallUpdateScreen.h +++ b/UI/InstallUpdateScreen.h @@ -39,7 +39,9 @@ class InstallUpdateScreen : public UISimpleBaseDialogScreen { public: // title is the updater's SFO title, which already carries the version ("PSP Update ver 6.61"). - InstallUpdateScreen(const Path &path, std::string_view title); + // allowRun offers to boot the updater instead of unpacking it - which makes sense when the + // user picked the updater to launch it, but not when they came here to install a firmware. + InstallUpdateScreen(const Path &path, std::string_view title, bool allowRun); void CreateDialogViews(UI::ViewGroup *parent) override; void update() override; @@ -70,6 +72,7 @@ private: std::string title_; u64 fileSize_ = 0; bool overwrites_ = false; + bool allowRun_ = false; std::shared_ptr state_; bool reportedDone_ = false; diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 3419450e2b..13fafba12a 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -101,7 +101,7 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons std::string title = info->GetTitle(); // includes the version. // The unpacker wants the PBP itself, not the folder it happens to sit in. const Path pbpPath = info->fileType == IdentifiedFileType::PSP_PBP ? path : path / "EBOOT.PBP"; - screenManager->push(new InstallUpdateScreen(pbpPath, title)); + screenManager->push(new InstallUpdateScreen(pbpPath, title, true)); return; } break; diff --git a/UI/UI.vcxproj b/UI/UI.vcxproj index e74c8805fe..13d1600cd5 100644 --- a/UI/UI.vcxproj +++ b/UI/UI.vcxproj @@ -80,6 +80,7 @@ + @@ -139,6 +140,7 @@ + diff --git a/UI/UI.vcxproj.filters b/UI/UI.vcxproj.filters index c56a1b7fb3..1d4f450d53 100644 --- a/UI/UI.vcxproj.filters +++ b/UI/UI.vcxproj.filters @@ -43,6 +43,9 @@ Screens + + Screens + Screens @@ -200,6 +203,9 @@ Screens + + Screens + Screens diff --git a/UWP/UI_UWP/UI_UWP.vcxproj b/UWP/UI_UWP/UI_UWP.vcxproj index 3da7949f38..de59f9088d 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj +++ b/UWP/UI_UWP/UI_UWP.vcxproj @@ -115,6 +115,7 @@ + @@ -175,6 +176,7 @@ + diff --git a/UWP/UI_UWP/UI_UWP.vcxproj.filters b/UWP/UI_UWP/UI_UWP.vcxproj.filters index 4274cc244d..a2cc4904b0 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj.filters +++ b/UWP/UI_UWP/UI_UWP.vcxproj.filters @@ -76,6 +76,9 @@ Screens + + Screens + Screens @@ -225,6 +228,9 @@ Screens + + Screens + Screens diff --git a/android/jni/Android.mk b/android/jni/Android.mk index ef5b0c577a..d7c5912df0 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -989,6 +989,7 @@ LOCAL_SRC_FILES := \ $(SRC)/UI/Background.cpp \ $(SRC)/UI/CwCheatScreen.cpp \ $(SRC)/UI/InstallPkgScreen.cpp \ + $(SRC)/UI/FirmwareScreen.cpp \ $(SRC)/UI/InstallUpdateScreen.cpp \ $(SRC)/UI/InstallZipScreen.cpp \ $(SRC)/UI/JitCompareScreen.cpp \