From 1040ec4e53c539e798ae66b9a292cdc0629e55bb Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Wed, 23 Oct 2024 09:38:41 -0500 Subject: [PATCH 1/5] Update dependabot.yml Stop trying to bother berry --- .github/dependabot.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 20bdc19d1..441d4f83c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,8 +6,6 @@ updates: interval: weekly labels: - "infra" - reviewers: - - TSRBerry commit-message: prefix: "ci" @@ -18,8 +16,6 @@ updates: interval: daily labels: - "infra" - reviewers: - - TSRBerry commit-message: prefix: nuget groups: -- 2.47.1 From b883de5d67d0415293d51e1a13c38e11f3b4c5d0 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Wed, 23 Oct 2024 09:47:56 -0500 Subject: [PATCH 2/5] Update dependabot.yml STOP OPENING THE DAMN AVALONIA PRS --- .github/dependabot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 441d4f83c..c07345f88 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,9 +19,9 @@ updates: commit-message: prefix: nuget groups: - Avalonia: - patterns: - - "*Avalonia*" + #Avalonia: + #patterns: + #- "*Avalonia*" Silk.NET: patterns: - "Silk.NET*" -- 2.47.1 From 8e8ad0f37e6e8f48b8fcb505a4125a46d48c342a Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Wed, 23 Oct 2024 09:54:39 -0500 Subject: [PATCH 3/5] go away dependabot --- .github/dependabot.yml | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index c07345f88..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: weekly - labels: - - "infra" - commit-message: - prefix: "ci" - - - package-ecosystem: nuget - directory: / - open-pull-requests-limit: 10 - schedule: - interval: daily - labels: - - "infra" - commit-message: - prefix: nuget - groups: - #Avalonia: - #patterns: - #- "*Avalonia*" - Silk.NET: - patterns: - - "Silk.NET*" - OpenTK: - patterns: - - "OpenTK*" - SixLabors: - patterns: - - "SixLabors*" - NUnit: - patterns: - - "NUnit*" -- 2.47.1 From 3fdc707cd45848125d0a9d9d333e04b2560d7e2f Mon Sep 17 00:00:00 2001 From: Aaron Murgatroyd Date: Thu, 24 Oct 2024 01:13:21 +1000 Subject: [PATCH 4/5] Add ability to trim XCI files from the application context menu --- src/Ryujinx.Common/Logging/LogClass.cs | 1 + .../Logging/XCIFileTrimmerLog.cs | 30 ++ .../Utilities/XCIFileTrimmer.cs | 507 ++++++++++++++++++ src/Ryujinx/Assets/Locales/en_US.json | 14 + src/Ryujinx/Common/XCIFileTrimmerLog.cs | 24 + .../UI/Controls/ApplicationContextMenu.axaml | 6 + .../Controls/ApplicationContextMenu.axaml.cs | 13 + .../UI/ViewModels/MainWindowViewModel.cs | 136 +++++ .../UI/Views/Main/MainStatusBarView.axaml | 11 +- 9 files changed, 740 insertions(+), 2 deletions(-) create mode 100644 src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs create mode 100644 src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs create mode 100644 src/Ryujinx/Common/XCIFileTrimmerLog.cs diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index 1b404a06a..a4117580e 100644 --- a/src/Ryujinx.Common/Logging/LogClass.cs +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging TamperMachine, UI, Vic, + XCIFileTrimmer } } diff --git a/src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs b/src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs new file mode 100644 index 000000000..fb11432b0 --- /dev/null +++ b/src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs @@ -0,0 +1,30 @@ +using Ryujinx.Common.Utilities; + +namespace Ryujinx.Common.Logging +{ + public class XCIFileTrimmerLog : XCIFileTrimmer.ILog + { + public virtual void Progress(long current, long total, string text, bool complete) + { + } + + public void Write(XCIFileTrimmer.LogType logType, string text) + { + switch (logType) + { + case XCIFileTrimmer.LogType.Info: + Logger.Notice.Print(LogClass.XCIFileTrimmer, text); + break; + case XCIFileTrimmer.LogType.Warn: + Logger.Warning?.Print(LogClass.XCIFileTrimmer, text); + break; + case XCIFileTrimmer.LogType.Error: + Logger.Error?.Print(LogClass.XCIFileTrimmer, text); + break; + case XCIFileTrimmer.LogType.Progress: + Logger.Info?.Print(LogClass.XCIFileTrimmer, text); + break; + } + } + } +} diff --git a/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs b/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs new file mode 100644 index 000000000..e33863bd7 --- /dev/null +++ b/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs @@ -0,0 +1,507 @@ +using Ryujinx.Common.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Ryujinx.Common.Utilities +{ + internal static class Performance + { + internal static TimeSpan Measure(Action action) + { + var sw = new Stopwatch(); + sw.Start(); + + try + { + action(); + } + finally + { + sw.Stop(); + } + + return sw.Elapsed; + } + } + + public sealed class XCIFileTrimmer + { + private const long BytesInAMegabyte = 1024 * 1024; + private const int BufferSize = 8 * (int)BytesInAMegabyte; + + private const long CartSizeMBinFormattedGB = 952; + private const int CartKeyAreaSize = 0x1000; + private const byte PaddingByte = 0xFF; + private const int HeaderFilePos = 0x100; + private const int CartSizeFilePos = 0x10D; + private const int DataSizeFilePos = 0x118; + private const string HeaderMagicValue = "HEAD"; + + /// + /// Cartridge Sizes (ByteIdentifier, SizeInGB) + /// + private static readonly Dictionary _cartSizesGB = new() + { + { 0xFA, 1 }, + { 0xF8, 2 }, + { 0xF0, 4 }, + { 0xE0, 8 }, + { 0xE1, 16 }, + { 0xE2, 32 } + }; + + private static long RecordsToByte(long records) + { + return 512 + (records * 512); + } + + public static bool CanTrim(string filename, ILog log = null) + { + if (Path.GetExtension(filename).Equals(".XCI", StringComparison.InvariantCultureIgnoreCase)) + { + var trimmer = new XCIFileTrimmer(filename, log); + return trimmer.CanBeTrimmed; + } + + return false; + } + + public static bool CanUntrim(string filename, ILog log = null) + { + if (Path.GetExtension(filename).Equals(".XCI", StringComparison.InvariantCultureIgnoreCase)) + { + var trimmer = new XCIFileTrimmer(filename, log); + return trimmer.CanBeUntrimmed; + } + + return false; + } + + private ILog _log; + private string _filename; + private FileStream _fileStream; + private BinaryReader _binaryReader; + private long _offsetB, _dataSizeB, _cartSizeB, _fileSizeB; + private bool _fileOK = true; + private bool _freeSpaceChecked = false; + private bool _freeSpaceValid = false; + + public enum OperationOutcome + { + InvalidXCIFile, + NoTrimNecessary, + NoUntrimPossible, + FreeSpaceCheckFailed, + FileIOWriteError, + ReadOnlyFileCannotFix, + FileSizeChanged, + Successful + } + + public enum LogType + { + Info, + Warn, + Error, + Progress + } + + public interface ILog + { + public void Write(LogType logType, string text); + public void Progress(long current, long total, string text, bool complete); + } + + public bool FileOK => _fileOK; + public bool Trimmed => _fileOK && FileSizeB < UntrimmedFileSizeB; + public bool ContainsKeyArea => _offsetB != 0; + public bool CanBeTrimmed => _fileOK && FileSizeB > TrimmedFileSizeB; + public bool CanBeUntrimmed => _fileOK && FileSizeB < UntrimmedFileSizeB; + public bool FreeSpaceChecked => _fileOK && _freeSpaceChecked; + public bool FreeSpaceValid => _fileOK && _freeSpaceValid; + public long DataSizeB => _dataSizeB; + public long CartSizeB => _cartSizeB; + public long FileSizeB => _fileSizeB; + public long DiskSpaceSavedB => CartSizeB - FileSizeB; + public long DiskSpaceSavingsB => CartSizeB - DataSizeB; + public long TrimmedFileSizeB => _offsetB + _dataSizeB; + public long UntrimmedFileSizeB => _offsetB + _cartSizeB; + + public ILog Log + { + get => _log; + set => _log = value; + } + + public String Filename + { + get => _filename; + set + { + _filename = value; + Reset(); + } + } + + public long Pos + { + get => _fileStream.Position; + set => _fileStream.Position = value; + } + + public XCIFileTrimmer(string path, ILog log = null) + { + Log = log; + Filename = path; + ReadHeader(); + } + + public void CheckFreeSpace() + { + if (FreeSpaceChecked) + return; + + try + { + if (CanBeTrimmed) + { + _freeSpaceValid = false; + + OpenReaders(); + + try + { + Pos = TrimmedFileSizeB; + bool freeSpaceValid = true; + long readSizeB = FileSizeB - TrimmedFileSizeB; + + TimeSpan time = Performance.Measure(() => + { + freeSpaceValid = CheckPadding(readSizeB); + }); + + if (time.TotalSeconds > 0) + { + Log?.Write(LogType.Info, $"Checked at {readSizeB / (double)XCIFileTrimmer.BytesInAMegabyte / time.TotalSeconds:N} Mb/sec"); + } + + if (freeSpaceValid) + Log?.Write(LogType.Info, "Free space is valid"); + + _freeSpaceValid = freeSpaceValid; + } + finally + { + CloseReaders(); + } + + } + else + { + Log?.Write(LogType.Warn, "There is no free space to check."); + _freeSpaceValid = false; + } + } + finally + { + _freeSpaceChecked = true; + } + } + + private bool CheckPadding(long readSizeB) + { + long maxReads = readSizeB / XCIFileTrimmer.BufferSize; + long read = 0; + var buffer = new byte[BufferSize]; + + while (true) + { + int bytes = _fileStream.Read(buffer, 0, XCIFileTrimmer.BufferSize); + if (bytes == 0) + break; + + Log?.Progress(read, maxReads, "Verifying file can be trimmed", false); + if (buffer.Take(bytes).AsParallel().Any(b => b != XCIFileTrimmer.PaddingByte)) + { + Log?.Write(LogType.Warn, "Free space is NOT valid"); + return false; + } + + read++; + } + + return true; + } + + private void Reset() + { + _freeSpaceChecked = false; + _freeSpaceValid = false; + ReadHeader(); + } + + public OperationOutcome Trim() + { + if (!FileOK) + { + return OperationOutcome.InvalidXCIFile; + } + + if (!CanBeTrimmed) + { + return OperationOutcome.NoTrimNecessary; + } + + if (!FreeSpaceChecked) + { + CheckFreeSpace(); + } + + if (!FreeSpaceValid) + { + return OperationOutcome.FreeSpaceCheckFailed; + } + + Log?.Write(LogType.Info, "Trimming..."); + + try + { + var info = new FileInfo(Filename); + if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + try + { + Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute"); + File.SetAttributes(Filename, info.Attributes & ~FileAttributes.ReadOnly); + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.ReadOnlyFileCannotFix; + } + } + + if (info.Length != FileSizeB) + { + Log?.Write(LogType.Error, "File size has changed, cannot safely trim."); + return OperationOutcome.FileSizeChanged; + } + + var outfileStream = new FileStream(_filename, FileMode.Open, FileAccess.Write, FileShare.Write); + + try + { + outfileStream.SetLength(TrimmedFileSizeB); + return OperationOutcome.Successful; + } + finally + { + outfileStream.Close(); + Reset(); + } + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.FileIOWriteError; + } + } + + public OperationOutcome Untrim() + { + if (!FileOK) + { + return OperationOutcome.InvalidXCIFile; + } + + if (!CanBeUntrimmed) + { + return OperationOutcome.NoUntrimPossible; + } + + try + { + Log?.Write(LogType.Info, "Untrimming..."); + + var info = new FileInfo(Filename); + if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + try + { + Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute"); + File.SetAttributes(Filename, info.Attributes & ~FileAttributes.ReadOnly); + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.ReadOnlyFileCannotFix; + } + } + + if (info.Length != FileSizeB) + { + Log?.Write(LogType.Error, "File size has changed, cannot safely untrim."); + return OperationOutcome.FileSizeChanged; + } + + var outfileStream = new FileStream(_filename, FileMode.Append, FileAccess.Write, FileShare.Write); + long bytesToWriteB = UntrimmedFileSizeB - FileSizeB; + + try + { + TimeSpan time = Performance.Measure(() => + { + WritePadding(outfileStream, bytesToWriteB); + }); + + if (time.TotalSeconds > 0) + { + Log?.Write(LogType.Info, $"Wrote at {bytesToWriteB / (double)XCIFileTrimmer.BytesInAMegabyte / time.TotalSeconds:N} Mb/sec"); + } + + return OperationOutcome.Successful; + } + finally + { + outfileStream.Close(); + Reset(); + } + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.FileIOWriteError; + } + } + + private void WritePadding(FileStream outfileStream, long bytesToWriteB) + { + long bytesLeftToWriteB = bytesToWriteB; + long writes = bytesLeftToWriteB / XCIFileTrimmer.BufferSize; + int write = 0; + + try + { + var buffer = new byte[BufferSize]; + Array.Fill(buffer, XCIFileTrimmer.PaddingByte); + + while (bytesLeftToWriteB > 0) + { + long bytesToWrite = Math.Min(XCIFileTrimmer.BufferSize, bytesLeftToWriteB); + outfileStream.Write(buffer, 0, (int)bytesToWrite); + bytesLeftToWriteB -= bytesToWrite; + Log?.Progress(write, writes, "Writing padding data...", false); + write++; + } + } + finally + { + Log?.Progress(write, writes, "Writing padding data...", true); + } + } + + private void OpenReaders() + { + if (_binaryReader == null) + { + _fileStream = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.Read); + _binaryReader = new BinaryReader(_fileStream); + } + } + + private void CloseReaders() + { + if (_binaryReader != null && _binaryReader.BaseStream != null) + _binaryReader.Close(); + _binaryReader = null; + _fileStream = null; + GC.Collect(); + } + + private void ReadHeader() + { + try + { + OpenReaders(); + + try + { + // Attempt without key area + bool success = CheckAndReadHeader(false); + + if (!success) + { + // Attempt with key area + success = CheckAndReadHeader(true); + } + + _fileOK = success; + } + finally + { + CloseReaders(); + } + } + catch (Exception ex) + { + Log?.Write(LogType.Error, ex.Message); + _fileOK = false; + _dataSizeB = 0; + _cartSizeB = 0; + _fileSizeB = 0; + _offsetB = 0; + } + } + + private bool CheckAndReadHeader(bool assumeKeyArea) + { + // Read file size + _fileSizeB = _fileStream.Length; + if (_fileSizeB < 32 * 1024) + { + Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the data size is too small"); + return false; + } + + // Setup offset + _offsetB = (long)(assumeKeyArea ? XCIFileTrimmer.CartKeyAreaSize : 0); + + // Check header + Pos = _offsetB + XCIFileTrimmer.HeaderFilePos; + string head = System.Text.Encoding.ASCII.GetString(_binaryReader.ReadBytes(4)); + if (head != XCIFileTrimmer.HeaderMagicValue) + { + if (!assumeKeyArea) + { + Log?.Write(LogType.Warn, $"Incorrect header found, file mat contain a key area..."); + } + else + { + Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the header is corrupted"); + } + + return false; + } + + // Read Cart Size + Pos = _offsetB + XCIFileTrimmer.CartSizeFilePos; + byte cartSizeId = _binaryReader.ReadByte(); + if (!_cartSizesGB.TryGetValue(cartSizeId, out long cartSizeNGB)) + { + Log?.Write(LogType.Error, $"The source file doesn't look like an XCI file as the Cartridge Size is incorrect (0x{cartSizeId:X2})"); + return false; + } + _cartSizeB = cartSizeNGB * XCIFileTrimmer.CartSizeMBinFormattedGB * XCIFileTrimmer.BytesInAMegabyte; + + // Read data size + Pos = _offsetB + XCIFileTrimmer.DataSizeFilePos; + long records = (long)BitConverter.ToUInt32(_binaryReader.ReadBytes(4), 0); + _dataSizeB = RecordsToByte(records); + + return true; + } + } +} diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 1dfbb81f1..012a800c4 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -84,8 +84,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods", "GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory", "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} Games Loaded", "StatusBarSystemVersion": "System Version: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected", "LinuxVmMaxMapCountDialogTextPrimary": "Would you like to increase the value of vm.max_map_count to {0}", "LinuxVmMaxMapCountDialogTextSecondary": "Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.", @@ -718,6 +721,16 @@ "SelectDlcDialogTitle": "Select DLC files", "SelectUpdateDialogTitle": "Select update files", "SelectModDialogTitle": "Select mod directory", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", "UserProfileWindowTitle": "User Profiles Manager", "CheatWindowTitle": "Cheats Manager", "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})", @@ -735,6 +748,7 @@ "AutoloadDlcAndUpdateAddedMessage": "{0} new downloadable content(s) and {1} new update(s) added", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Edit Selected", + "Continue": "Continue", "Cancel": "Cancel", "Save": "Save", "Discard": "Discard", diff --git a/src/Ryujinx/Common/XCIFileTrimmerLog.cs b/src/Ryujinx/Common/XCIFileTrimmerLog.cs new file mode 100644 index 000000000..aed7f89aa --- /dev/null +++ b/src/Ryujinx/Common/XCIFileTrimmerLog.cs @@ -0,0 +1,24 @@ +using Ryujinx.Ava.UI.ViewModels; + +namespace Ryujinx.Ava.Common +{ + internal class XCIFileTrimmerLog : Ryujinx.Common.Logging.XCIFileTrimmerLog + { + private readonly MainWindowViewModel _viewModel; + + public XCIFileTrimmerLog(MainWindowViewModel viewModel) + { + _viewModel = viewModel; + } + + public override void Progress(long current, long total, string text, bool complete) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + _viewModel.StatusBarProgressMaximum = (int)(total); + _viewModel.StatusBarProgressValue = (int)(current); + }); + } + } + +} diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 4a6c99d1d..bc8b177cf 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -70,6 +70,12 @@ Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}" Icon="{icon:Icon mdi-folder-file}" ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" /> + + !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0; + public bool TrimXCIEnabled => Ryujinx.Common.Utilities.XCIFileTrimmer.CanTrim(SelectedApplication.Path, new Common.XCIFileTrimmerLog(this)); + public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild; @@ -485,6 +491,28 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool StatusBarProgressStatusVisible + { + get => _statusBarProgressStatusVisible; + set + { + _statusBarProgressStatusVisible = value; + + OnPropertyChanged(); + } + } + + public string StatusBarProgressStatusText + { + get => _statusBarProgressStatusText; + set + { + _statusBarProgressStatusText = value; + + OnPropertyChanged(); + } + } + public string FifoStatusText { get => _fifoStatusText; @@ -1802,6 +1830,114 @@ namespace Ryujinx.Ava.UI.ViewModels } } } + + public async void ProcessTrimResult(String filename, Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome operationOutcome) + { + string notifyUser = null; + + switch (operationOutcome) + { + case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.NoTrimNecessary: + notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileNoTrimNecessary]; + break; + case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.ReadOnlyFileCannotFix: + notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileReadOnlyFileCannotFix]; + break; + case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FreeSpaceCheckFailed: + notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileFreeSpaceCheckFailed]; + break; + case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.InvalidXCIFile: + notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileInvalidXCIFile]; + break; + case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FileIOWriteError: + notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileFileIOWriteError]; + break; + case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FileSizeChanged: + notifyUser = LocaleManager.Instance[LocaleKeys.TrimXCIFileFileSizeChanged]; + break; + case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.Successful: + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow is MainWindow mainWindow) + mainWindow.LoadApplications(); + } + break; + } + + if (notifyUser != null) + { + await ContentDialogHelper.CreateWarningDialog( + LocaleManager.Instance[LocaleKeys.TrimXCIFileFailedPrimaryText], + notifyUser + ); + } + } + + public async Task TrimXCIFile(string filename) + { + if (filename == null) + { + return; + } + + var trimmer = new XCIFileTrimmer(filename, new Common.XCIFileTrimmerLog(this)); + + if (trimmer.CanBeTrimmed) + { + var savings = (double)trimmer.DiskSpaceSavingsB / 1024.0 / 1024.0; + var currentFileSize = (double)trimmer.FileSizeB / 1024.0 / 1024.0; + var cartDataSize = (double)trimmer.DataSizeB / 1024.0 / 1024.0; + string secondaryText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TrimXCIFileDialogSecondaryText, currentFileSize, cartDataSize, savings); + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.TrimXCIFileDialogPrimaryText], + secondaryText, + LocaleManager.Instance[LocaleKeys.Continue], + LocaleManager.Instance[LocaleKeys.Cancel], + LocaleManager.Instance[LocaleKeys.TrimXCIFileDialogTitle] + ); + + if (result == UserResult.Yes) + { + Thread XCIFileTrimThread = new(() => + { + Dispatcher.UIThread.Post(() => + { + StatusBarProgressStatusText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarXCIFileTrimming, Path.GetFileName(filename)); + StatusBarProgressStatusVisible = true; + StatusBarProgressMaximum = 1; + StatusBarProgressValue = 0; + StatusBarVisible = true; + }); + + try + { + XCIFileTrimmer.OperationOutcome operationOutcome = trimmer.Trim(); + + Dispatcher.UIThread.Post(() => + { + ProcessTrimResult(filename, operationOutcome); + }); + } + finally + { + Dispatcher.UIThread.Post(() => + { + StatusBarProgressStatusVisible = false; + StatusBarProgressStatusText = string.Empty; + StatusBarVisible = false; + }); + } + }) + { + Name = "GUI.XCFileTrimmerThread", + IsBackground = true, + }; + XCIFileTrimThread.Start(); + } + } + } + #endregion } } diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml index b1db08b2a..ded7e2949 100644 --- a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml @@ -29,7 +29,7 @@ Margin="5" VerticalAlignment="Center" IsVisible="{Binding EnableNonGameRunningControls}"> - +