diff options
Diffstat (limited to 'src/Ryujinx.Gtk3/UI/Widgets')
| -rw-r--r-- | src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.Designer.cs | 233 | ||||
| -rw-r--r-- | src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs | 644 | ||||
| -rw-r--r-- | src/Ryujinx.Gtk3/UI/Widgets/GtkDialog.cs | 114 | ||||
| -rw-r--r-- | src/Ryujinx.Gtk3/UI/Widgets/GtkInputDialog.cs | 37 | ||||
| -rw-r--r-- | src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.cs | 57 | ||||
| -rw-r--r-- | src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.glade | 124 | ||||
| -rw-r--r-- | src/Ryujinx.Gtk3/UI/Widgets/RawInputToTextEntry.cs | 27 | ||||
| -rw-r--r-- | src/Ryujinx.Gtk3/UI/Widgets/UserErrorDialog.cs | 123 |
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(); + } + } +} |
