aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Gtk3/UI/Widgets
diff options
context:
space:
mode:
Diffstat (limited to 'src/Ryujinx.Gtk3/UI/Widgets')
-rw-r--r--src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.Designer.cs233
-rw-r--r--src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs644
-rw-r--r--src/Ryujinx.Gtk3/UI/Widgets/GtkDialog.cs114
-rw-r--r--src/Ryujinx.Gtk3/UI/Widgets/GtkInputDialog.cs37
-rw-r--r--src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.cs57
-rw-r--r--src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.glade124
-rw-r--r--src/Ryujinx.Gtk3/UI/Widgets/RawInputToTextEntry.cs27
-rw-r--r--src/Ryujinx.Gtk3/UI/Widgets/UserErrorDialog.cs123
8 files changed, 1359 insertions, 0 deletions
diff --git a/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.Designer.cs
new file mode 100644
index 00000000..8ee1cd2f
--- /dev/null
+++ b/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.Designer.cs
@@ -0,0 +1,233 @@
+using Gtk;
+using System;
+
+namespace Ryujinx.UI.Widgets
+{
+ public partial class GameTableContextMenu : Menu
+ {
+ private MenuItem _openSaveUserDirMenuItem;
+ private MenuItem _openSaveDeviceDirMenuItem;
+ private MenuItem _openSaveBcatDirMenuItem;
+ private MenuItem _manageTitleUpdatesMenuItem;
+ private MenuItem _manageDlcMenuItem;
+ private MenuItem _manageCheatMenuItem;
+ private MenuItem _openTitleModDirMenuItem;
+ private MenuItem _openTitleSdModDirMenuItem;
+ private Menu _extractSubMenu;
+ private MenuItem _extractMenuItem;
+ private MenuItem _extractRomFsMenuItem;
+ private MenuItem _extractExeFsMenuItem;
+ private MenuItem _extractLogoMenuItem;
+ private Menu _manageSubMenu;
+ private MenuItem _manageCacheMenuItem;
+ private MenuItem _purgePtcCacheMenuItem;
+ private MenuItem _purgeShaderCacheMenuItem;
+ private MenuItem _openPtcDirMenuItem;
+ private MenuItem _openShaderCacheDirMenuItem;
+ private MenuItem _createShortcutMenuItem;
+
+ private void InitializeComponent()
+ {
+ //
+ // _openSaveUserDirMenuItem
+ //
+ _openSaveUserDirMenuItem = new MenuItem("Open User Save Directory")
+ {
+ TooltipText = "Open the directory which contains Application's User Saves.",
+ };
+ _openSaveUserDirMenuItem.Activated += OpenSaveUserDir_Clicked;
+
+ //
+ // _openSaveDeviceDirMenuItem
+ //
+ _openSaveDeviceDirMenuItem = new MenuItem("Open Device Save Directory")
+ {
+ TooltipText = "Open the directory which contains Application's Device Saves.",
+ };
+ _openSaveDeviceDirMenuItem.Activated += OpenSaveDeviceDir_Clicked;
+
+ //
+ // _openSaveBcatDirMenuItem
+ //
+ _openSaveBcatDirMenuItem = new MenuItem("Open BCAT Save Directory")
+ {
+ TooltipText = "Open the directory which contains Application's BCAT Saves.",
+ };
+ _openSaveBcatDirMenuItem.Activated += OpenSaveBcatDir_Clicked;
+
+ //
+ // _manageTitleUpdatesMenuItem
+ //
+ _manageTitleUpdatesMenuItem = new MenuItem("Manage Title Updates")
+ {
+ TooltipText = "Open the Title Update management window",
+ };
+ _manageTitleUpdatesMenuItem.Activated += ManageTitleUpdates_Clicked;
+
+ //
+ // _manageDlcMenuItem
+ //
+ _manageDlcMenuItem = new MenuItem("Manage DLC")
+ {
+ TooltipText = "Open the DLC management window",
+ };
+ _manageDlcMenuItem.Activated += ManageDlc_Clicked;
+
+ //
+ // _manageCheatMenuItem
+ //
+ _manageCheatMenuItem = new MenuItem("Manage Cheats")
+ {
+ TooltipText = "Open the Cheat management window",
+ };
+ _manageCheatMenuItem.Activated += ManageCheats_Clicked;
+
+ //
+ // _openTitleModDirMenuItem
+ //
+ _openTitleModDirMenuItem = new MenuItem("Open Mods Directory")
+ {
+ TooltipText = "Open the directory which contains Application's Mods.",
+ };
+ _openTitleModDirMenuItem.Activated += OpenTitleModDir_Clicked;
+
+ //
+ // _openTitleSdModDirMenuItem
+ //
+ _openTitleSdModDirMenuItem = new MenuItem("Open Atmosphere Mods Directory")
+ {
+ TooltipText = "Open the alternative SD card atmosphere directory which contains the Application's Mods.",
+ };
+ _openTitleSdModDirMenuItem.Activated += OpenTitleSdModDir_Clicked;
+
+ //
+ // _extractSubMenu
+ //
+ _extractSubMenu = new Menu();
+
+ //
+ // _extractMenuItem
+ //
+ _extractMenuItem = new MenuItem("Extract Data")
+ {
+ Submenu = _extractSubMenu
+ };
+
+ //
+ // _extractRomFsMenuItem
+ //
+ _extractRomFsMenuItem = new MenuItem("RomFS")
+ {
+ TooltipText = "Extract the RomFS section from Application's current config (including updates).",
+ };
+ _extractRomFsMenuItem.Activated += ExtractRomFs_Clicked;
+
+ //
+ // _extractExeFsMenuItem
+ //
+ _extractExeFsMenuItem = new MenuItem("ExeFS")
+ {
+ TooltipText = "Extract the ExeFS section from Application's current config (including updates).",
+ };
+ _extractExeFsMenuItem.Activated += ExtractExeFs_Clicked;
+
+ //
+ // _extractLogoMenuItem
+ //
+ _extractLogoMenuItem = new MenuItem("Logo")
+ {
+ TooltipText = "Extract the Logo section from Application's current config (including updates).",
+ };
+ _extractLogoMenuItem.Activated += ExtractLogo_Clicked;
+
+ //
+ // _manageSubMenu
+ //
+ _manageSubMenu = new Menu();
+
+ //
+ // _manageCacheMenuItem
+ //
+ _manageCacheMenuItem = new MenuItem("Cache Management")
+ {
+ Submenu = _manageSubMenu,
+ };
+
+ //
+ // _purgePtcCacheMenuItem
+ //
+ _purgePtcCacheMenuItem = new MenuItem("Queue PPTC Rebuild")
+ {
+ TooltipText = "Trigger PPTC to rebuild at boot time on the next game launch.",
+ };
+ _purgePtcCacheMenuItem.Activated += PurgePtcCache_Clicked;
+
+ //
+ // _purgeShaderCacheMenuItem
+ //
+ _purgeShaderCacheMenuItem = new MenuItem("Purge Shader Cache")
+ {
+ TooltipText = "Delete the Application's shader cache.",
+ };
+ _purgeShaderCacheMenuItem.Activated += PurgeShaderCache_Clicked;
+
+ //
+ // _openPtcDirMenuItem
+ //
+ _openPtcDirMenuItem = new MenuItem("Open PPTC Directory")
+ {
+ TooltipText = "Open the directory which contains the Application's PPTC cache.",
+ };
+ _openPtcDirMenuItem.Activated += OpenPtcDir_Clicked;
+
+ //
+ // _openShaderCacheDirMenuItem
+ //
+ _openShaderCacheDirMenuItem = new MenuItem("Open Shader Cache Directory")
+ {
+ TooltipText = "Open the directory which contains the Application's shader cache.",
+ };
+ _openShaderCacheDirMenuItem.Activated += OpenShaderCacheDir_Clicked;
+
+ //
+ // _createShortcutMenuItem
+ //
+ _createShortcutMenuItem = new MenuItem("Create Application Shortcut")
+ {
+ TooltipText = OperatingSystem.IsMacOS() ? "Create a shortcut in macOS's Applications folder that launches the selected Application" : "Create a Desktop Shortcut that launches the selected Application."
+ };
+ _createShortcutMenuItem.Activated += CreateShortcut_Clicked;
+
+ ShowComponent();
+ }
+
+ private void ShowComponent()
+ {
+ _extractSubMenu.Append(_extractExeFsMenuItem);
+ _extractSubMenu.Append(_extractRomFsMenuItem);
+ _extractSubMenu.Append(_extractLogoMenuItem);
+
+ _manageSubMenu.Append(_purgePtcCacheMenuItem);
+ _manageSubMenu.Append(_purgeShaderCacheMenuItem);
+ _manageSubMenu.Append(_openPtcDirMenuItem);
+ _manageSubMenu.Append(_openShaderCacheDirMenuItem);
+
+ Add(_createShortcutMenuItem);
+ Add(new SeparatorMenuItem());
+ Add(_openSaveUserDirMenuItem);
+ Add(_openSaveDeviceDirMenuItem);
+ Add(_openSaveBcatDirMenuItem);
+ Add(new SeparatorMenuItem());
+ Add(_manageTitleUpdatesMenuItem);
+ Add(_manageDlcMenuItem);
+ Add(_manageCheatMenuItem);
+ Add(_openTitleModDirMenuItem);
+ Add(_openTitleSdModDirMenuItem);
+ Add(new SeparatorMenuItem());
+ Add(_manageCacheMenuItem);
+ Add(_extractMenuItem);
+
+ ShowAll();
+ }
+ }
+}
diff --git a/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs b/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs
new file mode 100644
index 00000000..c8236223
--- /dev/null
+++ b/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs
@@ -0,0 +1,644 @@
+using Gtk;
+using LibHac;
+using LibHac.Account;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.Fs.Shim;
+using LibHac.FsSystem;
+using LibHac.Ns;
+using LibHac.Tools.Fs;
+using LibHac.Tools.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.UI.App.Common;
+using Ryujinx.UI.Common.Configuration;
+using Ryujinx.UI.Common.Helper;
+using Ryujinx.UI.Windows;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+
+namespace Ryujinx.UI.Widgets
+{
+ public partial class GameTableContextMenu : Menu
+ {
+ private readonly MainWindow _parent;
+ private readonly VirtualFileSystem _virtualFileSystem;
+ private readonly AccountManager _accountManager;
+ private readonly HorizonClient _horizonClient;
+ private readonly BlitStruct<ApplicationControlProperty> _controlData;
+
+ private readonly string _titleFilePath;
+ private readonly string _titleName;
+ private readonly string _titleIdText;
+ private readonly ulong _titleId;
+
+ private MessageDialog _dialog;
+ private bool _cancel;
+
+ public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct<ApplicationControlProperty> controlData)
+ {
+ _parent = parent;
+
+ InitializeComponent();
+
+ _virtualFileSystem = virtualFileSystem;
+ _accountManager = accountManager;
+ _horizonClient = horizonClient;
+ _titleFilePath = titleFilePath;
+ _titleName = titleName;
+ _titleIdText = titleId;
+ _controlData = controlData;
+
+ if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId))
+ {
+ GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id");
+
+ return;
+ }
+
+ _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
+ _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
+ _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0;
+
+ string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower();
+ bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
+
+ _extractRomFsMenuItem.Sensitive = hasNca;
+ _extractExeFsMenuItem.Sensitive = hasNca;
+ _extractLogoMenuItem.Sensitive = hasNca;
+
+ _createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild;
+
+ PopupAtPointer(null);
+ }
+
+ private bool TryFindSaveData(string titleName, ulong titleId, BlitStruct<ApplicationControlProperty> controlHolder, in SaveDataFilter filter, out ulong saveDataId)
+ {
+ saveDataId = default;
+
+ Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, in filter);
+
+ if (ResultFs.TargetNotFound.Includes(result))
+ {
+ ref ApplicationControlProperty control = ref controlHolder.Value;
+
+ Logger.Info?.Print(LogClass.Application, $"Creating save directory for Title: {titleName} [{titleId:x16}]");
+
+ if (Utilities.IsZeros(controlHolder.ByteSpan))
+ {
+ // If the current application doesn't have a loaded control property, create a dummy one
+ // and set the savedata sizes so a user savedata will be created.
+ control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
+
+ // The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
+ control.UserAccountSaveDataSize = 0x4000;
+ control.UserAccountSaveDataJournalSize = 0x4000;
+
+ Logger.Warning?.Print(LogClass.Application, "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
+ }
+
+ Uid user = new((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
+
+ result = _horizonClient.Fs.EnsureApplicationSaveData(out _, new LibHac.Ncm.ApplicationId(titleId), in control, in user);
+
+ if (result.IsFailure())
+ {
+ GtkDialog.CreateErrorDialog($"There was an error creating the specified savedata: {result.ToStringWithName()}");
+
+ return false;
+ }
+
+ // Try to find the savedata again after creating it
+ result = _horizonClient.Fs.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, in filter);
+ }
+
+ if (result.IsSuccess())
+ {
+ saveDataId = saveDataInfo.SaveDataId;
+
+ return true;
+ }
+
+ GtkDialog.CreateErrorDialog($"There was an error finding the specified savedata: {result.ToStringWithName()}");
+
+ return false;
+ }
+
+ private void OpenSaveDir(in SaveDataFilter saveDataFilter)
+ {
+ if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId))
+ {
+ return;
+ }
+
+ string saveRootPath = System.IO.Path.Combine(VirtualFileSystem.GetNandPath(), $"user/save/{saveDataId:x16}");
+
+ if (!Directory.Exists(saveRootPath))
+ {
+ // Inconsistent state. Create the directory
+ Directory.CreateDirectory(saveRootPath);
+ }
+
+ string committedPath = System.IO.Path.Combine(saveRootPath, "0");
+ string workingPath = System.IO.Path.Combine(saveRootPath, "1");
+
+ // If the committed directory exists, that path will be loaded the next time the savedata is mounted
+ if (Directory.Exists(committedPath))
+ {
+ OpenHelper.OpenFolder(committedPath);
+ }
+ else
+ {
+ // If the working directory exists and the committed directory doesn't,
+ // the working directory will be loaded the next time the savedata is mounted
+ if (!Directory.Exists(workingPath))
+ {
+ Directory.CreateDirectory(workingPath);
+ }
+
+ OpenHelper.OpenFolder(workingPath);
+ }
+ }
+
+ private void ExtractSection(NcaSectionType ncaSectionType, int programIndex = 0)
+ {
+ FileChooserNative fileChooser = new("Choose the folder to extract into", _parent, FileChooserAction.SelectFolder, "Extract", "Cancel");
+
+ ResponseType response = (ResponseType)fileChooser.Run();
+ string destination = fileChooser.Filename;
+
+ fileChooser.Dispose();
+
+ if (response == ResponseType.Accept)
+ {
+ Thread extractorThread = new(() =>
+ {
+ Gtk.Application.Invoke(delegate
+ {
+ _dialog = new MessageDialog(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Cancel, null)
+ {
+ Title = "Ryujinx - NCA Section Extractor",
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Gtk3.UI.Common.Resources.Logo_Ryujinx.png"),
+ SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...",
+ WindowPosition = WindowPosition.Center,
+ };
+
+ int dialogResponse = _dialog.Run();
+ if (dialogResponse == (int)ResponseType.Cancel || dialogResponse == (int)ResponseType.DeleteEvent)
+ {
+ _cancel = true;
+ _dialog.Dispose();
+ }
+ });
+
+ using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read);
+
+ Nca mainNca = null;
+ Nca patchNca = null;
+
+ if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") ||
+ (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") ||
+ (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci"))
+ {
+ IFileSystem pfs;
+
+ if (System.IO.Path.GetExtension(_titleFilePath) == ".xci")
+ {
+ Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
+
+ pfs = xci.OpenPartition(XciPartitionType.Secure);
+ }
+ else
+ {
+ var pfsTemp = new PartitionFileSystem();
+ pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+ pfs = pfsTemp;
+ }
+
+ foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
+ {
+ using var ncaFile = new UniqueRef<IFile>();
+
+ pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Release().AsStorage());
+
+ if (nca.Header.ContentType == NcaContentType.Program)
+ {
+ int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
+
+ if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())
+ {
+ patchNca = nca;
+ }
+ else
+ {
+ mainNca = nca;
+ }
+ }
+ }
+ }
+ else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca")
+ {
+ mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
+ }
+
+ if (mainNca == null)
+ {
+ Logger.Error?.Print(LogClass.Application, "Extraction failure. The main NCA is not present in the selected file.");
+
+ Gtk.Application.Invoke(delegate
+ {
+ GtkDialog.CreateErrorDialog("Extraction failure. The main NCA is not present in the selected file.");
+ });
+
+ return;
+ }
+
+ (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
+
+ if (updatePatchNca != null)
+ {
+ patchNca = updatePatchNca;
+ }
+
+ int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType);
+
+ bool sectionExistsInPatch = false;
+
+ if (patchNca != null)
+ {
+ sectionExistsInPatch = patchNca.CanOpenSection(index);
+ }
+
+ IFileSystem ncaFileSystem = sectionExistsInPatch ? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid)
+ : mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid);
+
+ FileSystemClient fsClient = _horizonClient.Fs;
+
+ string source = DateTime.Now.ToFileTime().ToString()[10..];
+ string output = DateTime.Now.ToFileTime().ToString()[10..];
+
+ using var uniqueSourceFs = new UniqueRef<IFileSystem>(ncaFileSystem);
+ using var uniqueOutputFs = new UniqueRef<IFileSystem>(new LocalFileSystem(destination));
+
+ fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref);
+ fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref);
+
+ (Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/");
+
+ if (!canceled)
+ {
+ if (resultCode.Value.IsFailure())
+ {
+ Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}");
+
+ Gtk.Application.Invoke(delegate
+ {
+ _dialog?.Dispose();
+
+ GtkDialog.CreateErrorDialog("Extraction failed. Read the log file for further information.");
+ });
+ }
+ else if (resultCode.Value.IsSuccess())
+ {
+ Gtk.Application.Invoke(delegate
+ {
+ _dialog?.Dispose();
+
+ MessageDialog dialog = new(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null)
+ {
+ Title = "Ryujinx - NCA Section Extractor",
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png"),
+ SecondaryText = "Extraction completed successfully.",
+ WindowPosition = WindowPosition.Center,
+ };
+
+ dialog.Run();
+ dialog.Dispose();
+ });
+ }
+ }
+
+ fsClient.Unmount(source.ToU8Span());
+ fsClient.Unmount(output.ToU8Span());
+ })
+ {
+ Name = "GUI.NcaSectionExtractorThread",
+ IsBackground = true,
+ };
+ extractorThread.Start();
+ }
+ }
+
+ private (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath)
+ {
+ Result rc = fs.OpenDirectory(out DirectoryHandle sourceHandle, sourcePath.ToU8Span(), OpenDirectoryMode.All);
+ if (rc.IsFailure())
+ {
+ return (rc, false);
+ }
+
+ using (sourceHandle)
+ {
+ foreach (DirectoryEntryEx entry in fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default))
+ {
+ if (_cancel)
+ {
+ return (null, true);
+ }
+
+ string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name));
+ string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name));
+
+ if (entry.Type == DirectoryEntryType.Directory)
+ {
+ fs.EnsureDirectoryExists(subDstPath);
+
+ (Result? result, bool canceled) = CopyDirectory(fs, subSrcPath, subDstPath);
+ if (canceled || result.Value.IsFailure())
+ {
+ return (result, canceled);
+ }
+ }
+
+ if (entry.Type == DirectoryEntryType.File)
+ {
+ fs.CreateOrOverwriteFile(subDstPath, entry.Size);
+
+ rc = CopyFile(fs, subSrcPath, subDstPath);
+ if (rc.IsFailure())
+ {
+ return (rc, false);
+ }
+ }
+ }
+ }
+
+ return (Result.Success, false);
+ }
+
+ public static Result CopyFile(FileSystemClient fs, string sourcePath, string destPath)
+ {
+ Result rc = fs.OpenFile(out FileHandle sourceHandle, sourcePath.ToU8Span(), OpenMode.Read);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+
+ using (sourceHandle)
+ {
+ rc = fs.OpenFile(out FileHandle destHandle, destPath.ToU8Span(), OpenMode.Write | OpenMode.AllowAppend);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+
+ using (destHandle)
+ {
+ const int MaxBufferSize = 1024 * 1024;
+
+ rc = fs.GetFileSize(out long fileSize, sourceHandle);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+
+ int bufferSize = (int)Math.Min(MaxBufferSize, fileSize);
+
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
+ try
+ {
+ for (long offset = 0; offset < fileSize; offset += bufferSize)
+ {
+ int toRead = (int)Math.Min(fileSize - offset, bufferSize);
+ Span<byte> buf = buffer.AsSpan(0, toRead);
+
+ rc = fs.ReadFile(out long _, sourceHandle, offset, buf);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+
+ rc = fs.WriteFile(destHandle, offset, buf, WriteOption.None);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+ }
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
+
+ rc = fs.FlushFile(destHandle);
+ if (rc.IsFailure())
+ {
+ return rc;
+ }
+ }
+ }
+
+ return Result.Success;
+ }
+
+ //
+ // Events
+ //
+ private void OpenSaveUserDir_Clicked(object sender, EventArgs args)
+ {
+ var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
+ var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default);
+
+ OpenSaveDir(in saveDataFilter);
+ }
+
+ private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
+ {
+ var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default);
+
+ OpenSaveDir(in saveDataFilter);
+ }
+
+ private void OpenSaveBcatDir_Clicked(object sender, EventArgs args)
+ {
+ var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
+
+ OpenSaveDir(in saveDataFilter);
+ }
+
+ private void ManageTitleUpdates_Clicked(object sender, EventArgs args)
+ {
+ new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show();
+ }
+
+ private void ManageDlc_Clicked(object sender, EventArgs args)
+ {
+ new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show();
+ }
+
+ private void ManageCheats_Clicked(object sender, EventArgs args)
+ {
+ new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show();
+ }
+
+ private void OpenTitleModDir_Clicked(object sender, EventArgs args)
+ {
+ string modsBasePath = ModLoader.GetModsBasePath();
+ string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, _titleIdText);
+
+ OpenHelper.OpenFolder(titleModsPath);
+ }
+
+ private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
+ {
+ string sdModsBasePath = ModLoader.GetSdModsBasePath();
+ string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, _titleIdText);
+
+ OpenHelper.OpenFolder(titleModsPath);
+ }
+
+ private void ExtractRomFs_Clicked(object sender, EventArgs args)
+ {
+ ExtractSection(NcaSectionType.Data);
+ }
+
+ private void ExtractExeFs_Clicked(object sender, EventArgs args)
+ {
+ ExtractSection(NcaSectionType.Code);
+ }
+
+ private void ExtractLogo_Clicked(object sender, EventArgs args)
+ {
+ ExtractSection(NcaSectionType.Logo);
+ }
+
+ private void OpenPtcDir_Clicked(object sender, EventArgs args)
+ {
+ string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu");
+
+ string mainPath = System.IO.Path.Combine(ptcDir, "0");
+ string backupPath = System.IO.Path.Combine(ptcDir, "1");
+
+ if (!Directory.Exists(ptcDir))
+ {
+ Directory.CreateDirectory(ptcDir);
+ Directory.CreateDirectory(mainPath);
+ Directory.CreateDirectory(backupPath);
+ }
+
+ OpenHelper.OpenFolder(ptcDir);
+ }
+
+ private void OpenShaderCacheDir_Clicked(object sender, EventArgs args)
+ {
+ string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader");
+
+ if (!Directory.Exists(shaderCacheDir))
+ {
+ Directory.CreateDirectory(shaderCacheDir);
+ }
+
+ OpenHelper.OpenFolder(shaderCacheDir);
+ }
+
+ private void PurgePtcCache_Clicked(object sender, EventArgs args)
+ {
+ DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0"));
+ DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1"));
+
+ MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n<b>{_titleName}</b>\n\nAre you sure you want to proceed?");
+
+ List<FileInfo> cacheFiles = new();
+
+ if (mainDir.Exists)
+ {
+ cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache"));
+ }
+
+ if (backupDir.Exists)
+ {
+ cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache"));
+ }
+
+ if (cacheFiles.Count > 0 && warningDialog.Run() == (int)ResponseType.Yes)
+ {
+ foreach (FileInfo file in cacheFiles)
+ {
+ try
+ {
+ file.Delete();
+ }
+ catch (Exception e)
+ {
+ GtkDialog.CreateErrorDialog($"Error purging PPTC cache {file.Name}: {e}");
+ }
+ }
+ }
+
+ warningDialog.Dispose();
+ }
+
+ private void PurgeShaderCache_Clicked(object sender, EventArgs args)
+ {
+ DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"));
+
+ using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n<b>{_titleName}</b>\n\nAre you sure you want to proceed?");
+
+ List<DirectoryInfo> oldCacheDirectories = new();
+ List<FileInfo> newCacheFiles = new();
+
+ if (shaderCacheDir.Exists)
+ {
+ oldCacheDirectories.AddRange(shaderCacheDir.EnumerateDirectories("*"));
+ newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.toc"));
+ newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.data"));
+ }
+
+ if ((oldCacheDirectories.Count > 0 || newCacheFiles.Count > 0) && warningDialog.Run() == (int)ResponseType.Yes)
+ {
+ foreach (DirectoryInfo directory in oldCacheDirectories)
+ {
+ try
+ {
+ directory.Delete(true);
+ }
+ catch (Exception e)
+ {
+ GtkDialog.CreateErrorDialog($"Error purging shader cache at {directory.Name}: {e}");
+ }
+ }
+
+ foreach (FileInfo file in newCacheFiles)
+ {
+ try
+ {
+ file.Delete();
+ }
+ catch (Exception e)
+ {
+ GtkDialog.CreateErrorDialog($"Error purging shader cache at {file.Name}: {e}");
+ }
+ }
+ }
+ }
+
+ private void CreateShortcut_Clicked(object sender, EventArgs args)
+ {
+ byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
+ ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
+ }
+ }
+}
diff --git a/src/Ryujinx.Gtk3/UI/Widgets/GtkDialog.cs b/src/Ryujinx.Gtk3/UI/Widgets/GtkDialog.cs
new file mode 100644
index 00000000..567f9ad6
--- /dev/null
+++ b/src/Ryujinx.Gtk3/UI/Widgets/GtkDialog.cs
@@ -0,0 +1,114 @@
+using Gtk;
+using Ryujinx.Common.Logging;
+using Ryujinx.UI.Common.Configuration;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace Ryujinx.UI.Widgets
+{
+ internal class GtkDialog : MessageDialog
+ {
+ private static bool _isChoiceDialogOpen;
+
+ private GtkDialog(string title, string mainText, string secondaryText, MessageType messageType = MessageType.Other, ButtonsType buttonsType = ButtonsType.Ok)
+ : base(null, DialogFlags.Modal, messageType, buttonsType, null)
+ {
+ Title = title;
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png");
+ Text = mainText;
+ SecondaryText = secondaryText;
+ WindowPosition = WindowPosition.Center;
+ SecondaryUseMarkup = true;
+
+ Response += GtkDialog_Response;
+
+ SetSizeRequest(200, 20);
+ }
+
+ private void GtkDialog_Response(object sender, ResponseArgs args)
+ {
+ Dispose();
+ }
+
+ internal static void CreateInfoDialog(string mainText, string secondaryText)
+ {
+ new GtkDialog("Ryujinx - Info", mainText, secondaryText, MessageType.Info).Run();
+ }
+
+ internal static void CreateUpdaterInfoDialog(string mainText, string secondaryText)
+ {
+ new GtkDialog("Ryujinx - Updater", mainText, secondaryText, MessageType.Info).Run();
+ }
+
+ internal static MessageDialog CreateWaitingDialog(string mainText, string secondaryText)
+ {
+ return new GtkDialog("Ryujinx - Waiting", mainText, secondaryText, MessageType.Info, ButtonsType.None);
+ }
+
+ internal static void CreateWarningDialog(string mainText, string secondaryText)
+ {
+ new GtkDialog("Ryujinx - Warning", mainText, secondaryText, MessageType.Warning).Run();
+ }
+
+ internal static void CreateErrorDialog(string errorMessage)
+ {
+ Logger.Error?.Print(LogClass.Application, errorMessage);
+
+ new GtkDialog("Ryujinx - Error", "Ryujinx has encountered an error", errorMessage, MessageType.Error).Run();
+ }
+
+ internal static MessageDialog CreateConfirmationDialog(string mainText, string secondaryText = "")
+ {
+ return new GtkDialog("Ryujinx - Confirmation", mainText, secondaryText, MessageType.Question, ButtonsType.YesNo);
+ }
+
+ internal static bool CreateChoiceDialog(string title, string mainText, string secondaryText)
+ {
+ if (_isChoiceDialogOpen)
+ {
+ return false;
+ }
+
+ _isChoiceDialogOpen = true;
+
+ ResponseType response = (ResponseType)new GtkDialog(title, mainText, secondaryText, MessageType.Question, ButtonsType.YesNo).Run();
+
+ _isChoiceDialogOpen = false;
+
+ return response == ResponseType.Yes;
+ }
+
+ internal static ResponseType CreateCustomDialog(string title, string mainText, string secondaryText, Dictionary<int, string> buttons, MessageType messageType = MessageType.Other)
+ {
+ GtkDialog gtkDialog = new(title, mainText, secondaryText, messageType, ButtonsType.None);
+
+ foreach (var button in buttons)
+ {
+ gtkDialog.AddButton(button.Value, button.Key);
+ }
+
+ return (ResponseType)gtkDialog.Run();
+ }
+
+ internal static string CreateInputDialog(Window parent, string title, string mainText, uint inputMax)
+ {
+ GtkInputDialog gtkDialog = new(parent, title, mainText, inputMax);
+ ResponseType response = (ResponseType)gtkDialog.Run();
+ string responseText = gtkDialog.InputEntry.Text.TrimEnd();
+
+ gtkDialog.Dispose();
+
+ if (response == ResponseType.Ok)
+ {
+ return responseText;
+ }
+
+ return "";
+ }
+
+ internal static bool CreateExitDialog()
+ {
+ return CreateChoiceDialog("Ryujinx - Exit", "Are you sure you want to close Ryujinx?", "All unsaved data will be lost!");
+ }
+ }
+}
diff --git a/src/Ryujinx.Gtk3/UI/Widgets/GtkInputDialog.cs b/src/Ryujinx.Gtk3/UI/Widgets/GtkInputDialog.cs
new file mode 100644
index 00000000..fd85248b
--- /dev/null
+++ b/src/Ryujinx.Gtk3/UI/Widgets/GtkInputDialog.cs
@@ -0,0 +1,37 @@
+using Gtk;
+
+namespace Ryujinx.UI.Widgets
+{
+ public class GtkInputDialog : MessageDialog
+ {
+ public Entry InputEntry { get; }
+
+ public GtkInputDialog(Window parent, string title, string mainText, uint inputMax) : base(parent, DialogFlags.Modal | DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.OkCancel, null)
+ {
+ SetDefaultSize(300, 0);
+
+ Title = title;
+
+ Label mainTextLabel = new()
+ {
+ Text = mainText,
+ };
+
+ InputEntry = new Entry
+ {
+ MaxLength = (int)inputMax,
+ };
+
+ Label inputMaxTextLabel = new()
+ {
+ Text = $"(Max length: {inputMax})",
+ };
+
+ ((Box)MessageArea).PackStart(mainTextLabel, true, true, 0);
+ ((Box)MessageArea).PackStart(InputEntry, true, true, 5);
+ ((Box)MessageArea).PackStart(inputMaxTextLabel, true, true, 0);
+
+ ShowAll();
+ }
+ }
+}
diff --git a/src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.cs b/src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.cs
new file mode 100644
index 00000000..3b3e2fbb
--- /dev/null
+++ b/src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.cs
@@ -0,0 +1,57 @@
+using Gtk;
+using Ryujinx.UI.Common.Configuration;
+using System;
+using System.Reflection;
+using GUI = Gtk.Builder.ObjectAttribute;
+
+namespace Ryujinx.UI.Widgets
+{
+ public class ProfileDialog : Dialog
+ {
+ public string FileName { get; private set; }
+
+#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier
+ [GUI] Entry _profileEntry;
+ [GUI] Label _errorMessage;
+#pragma warning restore CS0649, IDE0044
+
+ public ProfileDialog() : this(new Builder("Ryujinx.Gtk3.UI.Widgets.ProfileDialog.glade")) { }
+
+ private ProfileDialog(Builder builder) : base(builder.GetRawOwnedObject("_profileDialog"))
+ {
+ builder.Autoconnect(this);
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png");
+ }
+
+ private void OkToggle_Activated(object sender, EventArgs args)
+ {
+ ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
+
+ bool validFileName = true;
+
+ foreach (char invalidChar in System.IO.Path.GetInvalidFileNameChars())
+ {
+ if (_profileEntry.Text.Contains(invalidChar))
+ {
+ validFileName = false;
+ }
+ }
+
+ if (validFileName && !string.IsNullOrEmpty(_profileEntry.Text))
+ {
+ FileName = $"{_profileEntry.Text}.json";
+
+ Respond(ResponseType.Ok);
+ }
+ else
+ {
+ _errorMessage.Text = "The file name contains invalid characters. Please try again.";
+ }
+ }
+
+ private void CancelToggle_Activated(object sender, EventArgs args)
+ {
+ Respond(ResponseType.Cancel);
+ }
+ }
+}
diff --git a/src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.glade b/src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.glade
new file mode 100644
index 00000000..adaf6608
--- /dev/null
+++ b/src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.glade
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkDialog" id="_profileDialog">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Ryujinx - Profile Dialog</property>
+ <property name="modal">True</property>
+ <property name="window_position">center</property>
+ <property name="default_width">400</property>
+ <property name="type_hint">dialog</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox">
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkToggleButton" id="OkToggle">
+ <property name="label" translatable="yes">OK</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="toggled" handler="OkToggle_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="CancelToggle">
+ <property name="label" translatable="yes">Cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="toggled" handler="CancelToggle_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">20</property>
+ <property name="margin_bottom">10</property>
+ <property name="label" translatable="yes">Enter a name for the new profile:</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="_profileEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_left">20</property>
+ <property name="margin_right">20</property>
+ <property name="margin_top">20</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="_errorMessage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin_left">20</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <attributes>
+ <attribute name="foreground" value="#ffff00000000"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/src/Ryujinx.Gtk3/UI/Widgets/RawInputToTextEntry.cs b/src/Ryujinx.Gtk3/UI/Widgets/RawInputToTextEntry.cs
new file mode 100644
index 00000000..6470790e
--- /dev/null
+++ b/src/Ryujinx.Gtk3/UI/Widgets/RawInputToTextEntry.cs
@@ -0,0 +1,27 @@
+using Gtk;
+
+namespace Ryujinx.UI.Widgets
+{
+ public class RawInputToTextEntry : Entry
+ {
+ public void SendKeyPressEvent(object o, KeyPressEventArgs args)
+ {
+ base.OnKeyPressEvent(args.Event);
+ }
+
+ public void SendKeyReleaseEvent(object o, KeyReleaseEventArgs args)
+ {
+ base.OnKeyReleaseEvent(args.Event);
+ }
+
+ public void SendButtonPressEvent(object o, ButtonPressEventArgs args)
+ {
+ base.OnButtonPressEvent(args.Event);
+ }
+
+ public void SendButtonReleaseEvent(object o, ButtonReleaseEventArgs args)
+ {
+ base.OnButtonReleaseEvent(args.Event);
+ }
+ }
+}
diff --git a/src/Ryujinx.Gtk3/UI/Widgets/UserErrorDialog.cs b/src/Ryujinx.Gtk3/UI/Widgets/UserErrorDialog.cs
new file mode 100644
index 00000000..f0b55cd8
--- /dev/null
+++ b/src/Ryujinx.Gtk3/UI/Widgets/UserErrorDialog.cs
@@ -0,0 +1,123 @@
+using Gtk;
+using Ryujinx.UI.Common;
+using Ryujinx.UI.Common.Helper;
+
+namespace Ryujinx.UI.Widgets
+{
+ internal class UserErrorDialog : MessageDialog
+ {
+ private const string SetupGuideUrl = "https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide";
+ private const int OkResponseId = 0;
+ private const int SetupGuideResponseId = 1;
+
+ private readonly UserError _userError;
+
+ private UserErrorDialog(UserError error) : base(null, DialogFlags.Modal, MessageType.Error, ButtonsType.None, null)
+ {
+ _userError = error;
+
+ WindowPosition = WindowPosition.Center;
+ SecondaryUseMarkup = true;
+
+ Response += UserErrorDialog_Response;
+
+ SetSizeRequest(120, 50);
+
+ AddButton("OK", OkResponseId);
+
+ bool isInSetupGuide = IsCoveredBySetupGuide(error);
+
+ if (isInSetupGuide)
+ {
+ AddButton("Open the Setup Guide", SetupGuideResponseId);
+ }
+
+ string errorCode = GetErrorCode(error);
+
+ SecondaryUseMarkup = true;
+
+ Title = $"Ryujinx error ({errorCode})";
+ Text = $"{errorCode}: {GetErrorTitle(error)}";
+ SecondaryText = GetErrorDescription(error);
+
+ if (isInSetupGuide)
+ {
+ SecondaryText += "\n<b>For more information on how to fix this error, follow our Setup Guide.</b>";
+ }
+ }
+
+ private static string GetErrorCode(UserError error)
+ {
+ return $"RYU-{(uint)error:X4}";
+ }
+
+ private static string GetErrorTitle(UserError error)
+ {
+ return error switch
+ {
+ UserError.NoKeys => "Keys not found",
+ UserError.NoFirmware => "Firmware not found",
+ UserError.FirmwareParsingFailed => "Firmware parsing error",
+ UserError.ApplicationNotFound => "Application not found",
+ UserError.Unknown => "Unknown error",
+ _ => "Undefined error",
+ };
+ }
+
+ private static string GetErrorDescription(UserError error)
+ {
+ return error switch
+ {
+ UserError.NoKeys => "Ryujinx was unable to find your 'prod.keys' file",
+ UserError.NoFirmware => "Ryujinx was unable to find any firmwares installed",
+ UserError.FirmwareParsingFailed => "Ryujinx was unable to parse the provided firmware. This is usually caused by outdated keys.",
+ UserError.ApplicationNotFound => "Ryujinx couldn't find a valid application at the given path.",
+ UserError.Unknown => "An unknown error occured!",
+ _ => "An undefined error occured! This shouldn't happen, please contact a dev!",
+ };
+ }
+
+ private static bool IsCoveredBySetupGuide(UserError error)
+ {
+ return error switch
+ {
+ UserError.NoKeys or
+ UserError.NoFirmware or
+ UserError.FirmwareParsingFailed => true,
+ _ => false,
+ };
+ }
+
+ private static string GetSetupGuideUrl(UserError error)
+ {
+ if (!IsCoveredBySetupGuide(error))
+ {
+ return null;
+ }
+
+ return error switch
+ {
+ UserError.NoKeys => SetupGuideUrl + "#initial-setup---placement-of-prodkeys",
+ UserError.NoFirmware => SetupGuideUrl + "#initial-setup-continued---installation-of-firmware",
+ _ => SetupGuideUrl,
+ };
+ }
+
+ private void UserErrorDialog_Response(object sender, ResponseArgs args)
+ {
+ int responseId = (int)args.ResponseId;
+
+ if (responseId == SetupGuideResponseId)
+ {
+ OpenHelper.OpenUrl(GetSetupGuideUrl(_userError));
+ }
+
+ Dispose();
+ }
+
+ public static void CreateUserErrorDialog(UserError error)
+ {
+ new UserErrorDialog(error).Run();
+ }
+ }
+}