Windows default to non-portable + Reworked MLC handling and related UI (#1252)

This commit is contained in:
Exzap 2024-07-23 03:13:36 +02:00 committed by GitHub
parent 7522c8470e
commit 64232ffdbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 515 additions and 651 deletions

View file

@ -3,11 +3,11 @@
#include "gui/wxgui.h"
#include "config/CemuConfig.h"
#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h"
#include "Cafe/HW/Latte/Core/LatteOverlay.h"
#include "gui/guiWrapper.h"
#include "config/ActiveSettings.h"
#include "config/LaunchSettings.h"
#include "gui/GettingStartedDialog.h"
#include "config/PermanentConfig.h"
#include "config/PermanentStorage.h"
#include "input/InputManager.h"
#include "gui/helpers/wxHelpers.h"
#include "Cemu/ncrypto/ncrypto.h"
@ -30,7 +30,10 @@ wxIMPLEMENT_APP_NO_MAIN(CemuApp);
extern WindowInfo g_window_info;
extern std::shared_mutex g_mutex;
int mainEmulatorHLE();
// forward declarations from main.cpp
void UnitTests();
void CemuCommonInit();
void HandlePostUpdate();
// Translation strings to extract for gettext:
void unused_translation_dummy()
@ -54,34 +57,86 @@ void unused_translation_dummy()
void(_("unknown"));
}
bool CemuApp::OnInit()
#if BOOST_OS_WINDOWS
#include <shlobj_core.h>
fs::path GetAppDataRoamingPath()
{
PWSTR path = nullptr;
HRESULT result = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &path);
if (result != S_OK || !path)
{
if (path)
CoTaskMemFree(path);
return {};
}
std::string appDataPath = boost::nowide::narrow(path);
CoTaskMemFree(path);
return _utf8ToPath(appDataPath);
}
#endif
#if BOOST_OS_WINDOWS
void CemuApp::DeterminePaths(std::set<fs::path>& failedWriteAccess) // for Windows
{
std::error_code ec;
bool isPortable = false;
fs::path user_data_path, config_path, cache_path, data_path;
auto standardPaths = wxStandardPaths::Get();
fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath()));
fs::path portablePath = exePath.parent_path() / "portable";
data_path = exePath.parent_path(); // the data path is always the same as the exe path
if (fs::exists(portablePath, ec))
{
isPortable = true;
user_data_path = config_path = cache_path = portablePath;
}
else
{
fs::path roamingPath = GetAppDataRoamingPath() / "Cemu";
user_data_path = config_path = cache_path = roamingPath;
}
// on Windows Cemu used to be portable by default prior to 2.0-89
// to remain backwards compatible with old installations we check for settings.xml in the Cemu directory
// if it exists, we use the exe path as the portable directory
if(!isPortable) // lower priority than portable directory
{
if (fs::exists(exePath.parent_path() / "settings.xml", ec))
{
isPortable = true;
user_data_path = config_path = cache_path = exePath.parent_path();
}
}
ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess);
}
#endif
#if BOOST_OS_LINUX
void CemuApp::DeterminePaths(std::set<fs::path>& failedWriteAccess) // for Linux
{
std::error_code ec;
bool isPortable = false;
fs::path user_data_path, config_path, cache_path, data_path;
auto standardPaths = wxStandardPaths::Get();
fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath()));
fs::path portablePath = exePath.parent_path() / "portable";
// GetExecutablePath returns the AppImage's temporary mount location
wxString appImagePath;
if (wxGetEnv(("APPIMAGE"), &appImagePath))
exePath = wxHelper::MakeFSPath(appImagePath);
#endif
// Try a portable path first, if it exists.
user_data_path = config_path = cache_path = data_path = exePath.parent_path() / "portable";
#if BOOST_OS_MACOS
// If run from an app bundle, use its parent directory.
fs::path appPath = exePath.parent_path().parent_path().parent_path();
if (appPath.extension() == ".app")
user_data_path = config_path = cache_path = data_path = appPath.parent_path() / "portable";
#endif
if (!fs::exists(user_data_path))
{
#if BOOST_OS_WINDOWS
user_data_path = config_path = cache_path = data_path = exePath.parent_path();
#else
exePath = wxHelper::MakeFSPath(appImagePath);
portablePath = exePath.parent_path() / "portable";
}
if (fs::exists(portablePath, ec))
{
isPortable = true;
user_data_path = config_path = cache_path = portablePath;
// in portable mode assume the data directories (resources, gameProfiles/default/) are next to the executable
data_path = exePath.parent_path();
}
else
{
SetAppName("Cemu");
wxString appName=GetAppName();
#if BOOST_OS_LINUX
wxString appName = GetAppName();
standardPaths.SetFileLayout(wxStandardPaths::FileLayout::FileLayout_XDG);
auto getEnvDir = [&](const wxString& varName, const wxString& defaultValue)
{
@ -90,33 +145,151 @@ bool CemuApp::OnInit()
return defaultValue;
return dir;
};
wxString homeDir=wxFileName::GetHomeDir();
wxString homeDir = wxFileName::GetHomeDir();
user_data_path = (getEnvDir(wxS("XDG_DATA_HOME"), homeDir + wxS("/.local/share")) + "/" + appName).ToStdString();
config_path = (getEnvDir(wxS("XDG_CONFIG_HOME"), homeDir + wxS("/.config")) + "/" + appName).ToStdString();
#else
user_data_path = config_path = standardPaths.GetUserDataDir().ToStdString();
#endif
data_path = standardPaths.GetDataDir().ToStdString();
cache_path = standardPaths.GetUserDir(wxStandardPaths::Dir::Dir_Cache).ToStdString();
cache_path /= appName.ToStdString();
#endif
}
ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess);
}
#endif
auto failed_write_access = ActiveSettings::LoadOnce(exePath, user_data_path, config_path, cache_path, data_path);
for (auto&& path : failed_write_access)
wxMessageBox(formatWxString(_("Cemu can't write to {}!"), wxString::FromUTF8(_pathToUtf8(path))),
_("Warning"), wxOK | wxCENTRE | wxICON_EXCLAMATION, nullptr);
#if BOOST_OS_MACOS
void CemuApp::DeterminePaths(std::set<fs::path>& failedWriteAccess) // for MacOS
{
std::error_code ec;
bool isPortable = false;
fs::path user_data_path, config_path, cache_path, data_path;
auto standardPaths = wxStandardPaths::Get();
fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath()));
// If run from an app bundle, use its parent directory
fs::path appPath = exePath.parent_path().parent_path().parent_path();
fs::path portablePath = appPath.extension() == ".app" ? appPath.parent_path() / "portable" : exePath.parent_path() / "portable";
if (fs::exists(portablePath, ec))
{
isPortable = true;
user_data_path = config_path = cache_path = portablePath;
data_path = exePath.parent_path();
}
else
{
SetAppName("Cemu");
wxString appName = GetAppName();
user_data_path = config_path = standardPaths.GetUserDataDir().ToStdString();
data_path = standardPaths.GetDataDir().ToStdString();
cache_path = standardPaths.GetUserDir(wxStandardPaths::Dir::Dir_Cache).ToStdString();
cache_path /= appName.ToStdString();
}
ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess);
}
#endif
// create default MLC files or quit if it fails
void CemuApp::InitializeNewMLCOrFail(fs::path mlc)
{
if( CemuApp::CreateDefaultMLCFiles(mlc) )
return; // all good
cemu_assert_debug(!ActiveSettings::IsCustomMlcPath()); // should not be possible?
if(ActiveSettings::IsCommandLineMlcPath() || ActiveSettings::IsCustomMlcPath())
{
// tell user that the custom path is not writable
wxMessageBox(formatWxString(_("Cemu failed to write to the custom mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR);
exit(0);
}
wxMessageBox(formatWxString(_("Cemu failed to write to the mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR);
exit(0);
}
void CemuApp::InitializeExistingMLCOrFail(fs::path mlc)
{
if(CreateDefaultMLCFiles(mlc))
return; // all good
// failed to write mlc files
if(ActiveSettings::IsCommandLineMlcPath() || ActiveSettings::IsCustomMlcPath())
{
// tell user that the custom path is not writable
// if it's a command line path then just quit. Otherwise ask if user wants to reset the path
if(ActiveSettings::IsCommandLineMlcPath())
{
wxMessageBox(formatWxString(_("Cemu failed to write to the custom mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR);
exit(0);
}
// ask user if they want to reset the path
const wxString message = formatWxString(_("Cemu failed to write to the custom mlc directory.\n\nThe path is:\n{}\n\nCemu cannot start without a valid mlc path. Do you want to reset the path? You can later change it again in the General Settings."),
_pathToUtf8(mlc));
wxMessageDialog dialog(nullptr, message, _("Error"), wxCENTRE | wxYES_NO | wxICON_WARNING);
dialog.SetYesNoLabels(_("Reset path"), _("Exit"));
const auto dialogResult = dialog.ShowModal();
if (dialogResult == wxID_NO)
exit(0);
else // reset path
{
GetConfig().mlc_path = "";
g_config.Save();
}
}
}
bool CemuApp::OnInit()
{
std::set<fs::path> failedWriteAccess;
DeterminePaths(failedWriteAccess);
// make sure default cemu directories exist
CreateDefaultCemuFiles();
g_config.SetFilename(ActiveSettings::GetConfigPath("settings.xml").generic_wstring());
std::error_code ec;
bool isFirstStart = !fs::exists(ActiveSettings::GetConfigPath("settings.xml"), ec);
NetworkConfig::LoadOnce();
g_config.Load();
if(!isFirstStart)
{
g_config.Load();
LocalizeUI(static_cast<wxLanguage>(GetConfig().language == wxLANGUAGE_DEFAULT ? wxLocale::GetSystemLanguage() : GetConfig().language.GetValue()));
}
else
{
LocalizeUI(static_cast<wxLanguage>(wxLocale::GetSystemLanguage()));
}
for (auto&& path : failedWriteAccess)
{
wxMessageBox(formatWxString(_("Cemu can't write to {}!"), wxString::FromUTF8(_pathToUtf8(path))),
_("Warning"), wxOK | wxCENTRE | wxICON_EXCLAMATION, nullptr);
}
if (isFirstStart)
{
// show the getting started dialog
GettingStartedDialog dia(nullptr);
dia.ShowModal();
// make sure config is created. Gfx pack UI and input UI may create it earlier already, but we still want to update it
g_config.Save();
// create mlc, on failure the user can quit here. So do this after the Getting Started dialog
InitializeNewMLCOrFail(ActiveSettings::GetMlcPath());
}
else
{
// check if mlc is valid and recreate default files
InitializeExistingMLCOrFail(ActiveSettings::GetMlcPath());
}
ActiveSettings::Init(); // this is a bit of a misnomer, right now this call only loads certs for online play. In the future we should move the logic to a more appropriate place
HandlePostUpdate();
mainEmulatorHLE();
LatteOverlay_init();
// run a couple of tests if in non-release mode
#ifdef CEMU_DEBUG_ASSERT
UnitTests();
#endif
CemuCommonInit();
wxInitAllImageHandlers();
LocalizeUI();
// fill colour db
wxTheColourDatabase->AddColour("ERROR", wxColour(0xCC, 0, 0));
wxTheColourDatabase->AddColour("SUCCESS", wxColour(0, 0xbb, 0));
@ -135,15 +308,8 @@ bool CemuApp::OnInit()
Bind(wxEVT_ACTIVATE_APP, &CemuApp::ActivateApp, this);
auto& config = GetConfig();
const bool first_start = !config.did_show_graphic_pack_download;
CreateDefaultFiles(first_start);
m_mainFrame = new MainWindow();
if (first_start)
m_mainFrame->ShowGettingStartedDialog();
std::unique_lock lock(g_mutex);
g_window_info.app_active = true;
@ -230,22 +396,22 @@ std::vector<const wxLanguageInfo *> CemuApp::GetLanguages() const {
return availableLanguages;
}
void CemuApp::LocalizeUI()
void CemuApp::LocalizeUI(wxLanguage languageToUse)
{
std::unique_ptr<wxTranslations> translationsMgr(new wxTranslations());
m_availableTranslations = GetAvailableTranslationLanguages(translationsMgr.get());
const sint32 configuredLanguage = GetConfig().language;
bool isTranslationAvailable = std::any_of(m_availableTranslations.begin(), m_availableTranslations.end(),
[configuredLanguage](const wxLanguageInfo* info) { return info->Language == configuredLanguage; });
if (configuredLanguage == wxLANGUAGE_DEFAULT || isTranslationAvailable)
[languageToUse](const wxLanguageInfo* info) { return info->Language == languageToUse; });
if (languageToUse == wxLANGUAGE_DEFAULT || isTranslationAvailable)
{
translationsMgr->SetLanguage(static_cast<wxLanguage>(configuredLanguage));
translationsMgr->SetLanguage(static_cast<wxLanguage>(languageToUse));
translationsMgr->AddCatalog("cemu");
if (translationsMgr->IsLoaded("cemu") && wxLocale::IsAvailable(configuredLanguage))
m_locale.Init(configuredLanguage);
if (translationsMgr->IsLoaded("cemu") && wxLocale::IsAvailable(languageToUse))
{
m_locale.Init(languageToUse);
}
// This must be run after wxLocale::Init, as the latter sets up its own wxTranslations instance which we want to override
wxTranslations::Set(translationsMgr.release());
}
@ -264,55 +430,47 @@ std::vector<const wxLanguageInfo*> CemuApp::GetAvailableTranslationLanguages(wxT
return languages;
}
void CemuApp::CreateDefaultFiles(bool first_start)
bool CemuApp::CheckMLCPath(const fs::path& mlc)
{
std::error_code ec;
fs::path mlc = ActiveSettings::GetMlcPath();
// check for mlc01 folder missing if custom path has been set
if (!fs::exists(mlc, ec) && !first_start)
{
const wxString message = formatWxString(_("Your mlc01 folder seems to be missing.\n\nThis is where Cemu stores save files, game updates and other Wii U files.\n\nThe expected path is:\n{}\n\nDo you want to create the folder at the expected path?"),
_pathToUtf8(mlc));
wxMessageDialog dialog(nullptr, message, _("Error"), wxCENTRE | wxYES_NO | wxCANCEL| wxICON_WARNING);
dialog.SetYesNoCancelLabels(_("Yes"), _("No"), _("Select a custom path"));
const auto dialogResult = dialog.ShowModal();
if (dialogResult == wxID_NO)
exit(0);
else if(dialogResult == wxID_CANCEL)
{
if (!SelectMLCPath())
return;
mlc = ActiveSettings::GetMlcPath();
}
else
{
GetConfig().mlc_path = "";
g_config.Save();
}
}
if (!fs::exists(mlc, ec))
return false;
if (!fs::exists(mlc / "usr", ec) || !fs::exists(mlc / "sys", ec))
return false;
return true;
}
bool CemuApp::CreateDefaultMLCFiles(const fs::path& mlc)
{
auto CreateDirectoriesIfNotExist = [](const fs::path& path)
{
std::error_code ec;
if (!fs::exists(path, ec))
return fs::create_directories(path, ec);
return true;
};
// list of directories to create
const fs::path directories[] = {
mlc,
mlc / "sys",
mlc / "usr",
mlc / "usr/title/00050000", // base
mlc / "usr/title/0005000c", // dlc
mlc / "usr/title/0005000e", // update
mlc / "usr/save/00050010/1004a000/user/common/db", // Mii Maker save folders {0x500101004A000, 0x500101004A100, 0x500101004A200}
mlc / "usr/save/00050010/1004a100/user/common/db",
mlc / "usr/save/00050010/1004a200/user/common/db",
mlc / "sys/title/0005001b/1005c000/content" // lang files
};
for(auto& path : directories)
{
if(!CreateDirectoriesIfNotExist(path))
return false;
}
// create sys/usr folder in mlc01
try
{
const auto sysFolder = fs::path(mlc).append("sys");
fs::create_directories(sysFolder);
const auto usrFolder = fs::path(mlc).append("usr");
fs::create_directories(usrFolder);
fs::create_directories(fs::path(usrFolder).append("title/00050000")); // base
fs::create_directories(fs::path(usrFolder).append("title/0005000c")); // dlc
fs::create_directories(fs::path(usrFolder).append("title/0005000e")); // update
// Mii Maker save folders {0x500101004A000, 0x500101004A100, 0x500101004A200},
fs::create_directories(fs::path(mlc).append("usr/save/00050010/1004a000/user/common/db"));
fs::create_directories(fs::path(mlc).append("usr/save/00050010/1004a100/user/common/db"));
fs::create_directories(fs::path(mlc).append("usr/save/00050010/1004a200/user/common/db"));
// lang files
const auto langDir = fs::path(mlc).append("sys/title/0005001b/1005c000/content");
fs::create_directories(langDir);
auto langFile = fs::path(langDir).append("language.txt");
if (!fs::exists(langFile))
{
@ -346,18 +504,13 @@ void CemuApp::CreateDefaultFiles(bool first_start)
}
catch (const std::exception& ex)
{
wxString errorMsg = formatWxString(_("Couldn't create a required mlc01 subfolder or file!\n\nError: {0}\nTarget path:\n{1}"), ex.what(), _pathToUtf8(mlc));
#if BOOST_OS_WINDOWS
const DWORD lastError = GetLastError();
if (lastError != ERROR_SUCCESS)
errorMsg << fmt::format("\n\n{}", GetSystemErrorMessage(lastError));
#endif
wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR);
exit(0);
return false;
}
return true;
}
void CemuApp::CreateDefaultCemuFiles()
{
// cemu directories
try
{
@ -384,58 +537,6 @@ void CemuApp::CreateDefaultFiles(bool first_start)
}
}
bool CemuApp::TrySelectMLCPath(fs::path path)
{
if (path.empty())
path = ActiveSettings::GetDefaultMLCPath();
if (!TestWriteAccess(path))
return false;
GetConfig().SetMLCPath(path);
CemuApp::CreateDefaultFiles();
// update TitleList and SaveList scanner with new MLC path
CafeTitleList::SetMLCPath(path);
CafeTitleList::Refresh();
CafeSaveList::SetMLCPath(path);
CafeSaveList::Refresh();
return true;
}
bool CemuApp::SelectMLCPath(wxWindow* parent)
{
auto& config = GetConfig();
fs::path default_path;
if (fs::exists(_utf8ToPath(config.mlc_path.GetValue())))
default_path = _utf8ToPath(config.mlc_path.GetValue());
// try until users selects a valid path or aborts
while(true)
{
wxDirDialog path_dialog(parent, _("Select a mlc directory"), wxHelper::FromPath(default_path), wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST);
if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().empty())
return false;
const auto path = path_dialog.GetPath().ToStdWstring();
if (!TrySelectMLCPath(path))
{
const auto result = wxMessageBox(_("Cemu can't write to the selected mlc path!\nDo you want to select another path?"), _("Error"), wxYES_NO | wxCENTRE | wxICON_ERROR);
if (result == wxYES)
continue;
break;
}
return true;
}
return false;
}
void CemuApp::ActivateApp(wxActivateEvent& event)
{
g_window_info.app_active = event.GetActive();