mirror of
https://git.naxdy.org/Mirror/Ryujinx.git
synced 2025-01-11 21:29:14 +00:00
Redid all code changes as against latest master
This commit is contained in:
parent
4505a7f162
commit
bf23d183b2
14 changed files with 869 additions and 2 deletions
|
@ -73,7 +73,10 @@
|
||||||
"GameListContextMenuCreateShortcut": "Create Application Shortcut",
|
"GameListContextMenuCreateShortcut": "Create Application Shortcut",
|
||||||
"GameListContextMenuCreateShortcutToolTip": "Create a Desktop Shortcut that launches the selected Application",
|
"GameListContextMenuCreateShortcutToolTip": "Create a Desktop Shortcut that launches the selected Application",
|
||||||
"GameListContextMenuCreateShortcutToolTipMacOS": "Create a shortcut in macOS's Applications folder that launches the selected Application",
|
"GameListContextMenuCreateShortcutToolTipMacOS": "Create a shortcut in macOS's Applications folder that launches the selected Application",
|
||||||
|
"GameListContextMenuTrimXCI": "Check and Trim XCI File",
|
||||||
|
"GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space",
|
||||||
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
|
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
|
||||||
|
"StatusBarXCIFileTrimming": "Trimming XCI File '{0}'",
|
||||||
"StatusBarSystemVersion": "System Version: {0}",
|
"StatusBarSystemVersion": "System Version: {0}",
|
||||||
"LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
|
"LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
|
||||||
"LinuxVmMaxMapCountDialogTextPrimary": "Would you like to increase the value of vm.max_map_count to {0}",
|
"LinuxVmMaxMapCountDialogTextPrimary": "Would you like to increase the value of vm.max_map_count to {0}",
|
||||||
|
@ -592,6 +595,16 @@
|
||||||
"SelectDlcDialogTitle": "Select DLC files",
|
"SelectDlcDialogTitle": "Select DLC files",
|
||||||
"SelectUpdateDialogTitle": "Select update files",
|
"SelectUpdateDialogTitle": "Select update files",
|
||||||
"SelectModDialogTitle": "Select mod directory",
|
"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",
|
"UserProfileWindowTitle": "User Profiles Manager",
|
||||||
"CheatWindowTitle": "Cheats Manager",
|
"CheatWindowTitle": "Cheats Manager",
|
||||||
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
||||||
|
@ -601,6 +614,7 @@
|
||||||
"DlcWindowHeading": "{0} Downloadable Content(s)",
|
"DlcWindowHeading": "{0} Downloadable Content(s)",
|
||||||
"ModWindowHeading": "{0} Mod(s)",
|
"ModWindowHeading": "{0} Mod(s)",
|
||||||
"UserProfilesEditProfile": "Edit Selected",
|
"UserProfilesEditProfile": "Edit Selected",
|
||||||
|
"Continue": "Continue",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Discard": "Discard",
|
"Discard": "Discard",
|
||||||
|
|
24
src/Ryujinx.Ava/Common/XCIFileTrimmerLog.cs
Normal file
24
src/Ryujinx.Ava/Common/XCIFileTrimmerLog.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Common
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,6 +17,11 @@
|
||||||
Header="{locale:Locale GameListContextMenuCreateShortcut}"
|
Header="{locale:Locale GameListContextMenuCreateShortcut}"
|
||||||
IsEnabled="{Binding CreateShortcutEnabled}"
|
IsEnabled="{Binding CreateShortcutEnabled}"
|
||||||
ToolTip.Tip="{OnPlatform Default={locale:Locale GameListContextMenuCreateShortcutToolTip}, macOS={locale:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" />
|
ToolTip.Tip="{OnPlatform Default={locale:Locale GameListContextMenuCreateShortcutToolTip}, macOS={locale:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" />
|
||||||
|
<MenuItem
|
||||||
|
Click="TrimXCI_Click"
|
||||||
|
Header="{locale:Locale GameListContextMenuTrimXCI}"
|
||||||
|
IsEnabled="{Binding TrimXCIEnabled}"
|
||||||
|
ToolTip.Tip="{locale:Locale GameListContextMenuTrimXCIToolTip}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Click="OpenUserSaveDirectory_Click"
|
Click="OpenUserSaveDirectory_Click"
|
||||||
|
|
|
@ -341,5 +341,15 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
|
await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void TrimXCI_Click(object sender, RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
|
||||||
|
|
||||||
|
if (viewModel?.SelectedApplication != null)
|
||||||
|
{
|
||||||
|
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
private bool _isAppletMenuActive;
|
private bool _isAppletMenuActive;
|
||||||
private int _statusBarProgressMaximum;
|
private int _statusBarProgressMaximum;
|
||||||
private int _statusBarProgressValue;
|
private int _statusBarProgressValue;
|
||||||
|
private string _statusBarProgressStatusText;
|
||||||
|
private bool _statusBarProgressStatusVisible;
|
||||||
private bool _isPaused;
|
private bool _isPaused;
|
||||||
private bool _showContent = true;
|
private bool _showContent = true;
|
||||||
private bool _isLoadingIndeterminate = true;
|
private bool _isLoadingIndeterminate = true;
|
||||||
|
@ -357,6 +359,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
||||||
|
|
||||||
|
public bool TrimXCIEnabled => Ryujinx.Common.Utilities.XCIFileTrimmer.CanTrim(SelectedApplication.Path, new Common.XCIFileTrimmerLog(this));
|
||||||
|
|
||||||
public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild;
|
public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild;
|
||||||
|
|
||||||
public string LoadHeading
|
public string LoadHeading
|
||||||
|
@ -469,6 +473,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
|
public string FifoStatusText
|
||||||
{
|
{
|
||||||
get => _fifoStatusText;
|
get => _fifoStatusText;
|
||||||
|
@ -1708,6 +1734,107 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyUser != null)
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateWarningDialog(
|
||||||
|
LocaleManager.Instance[LocaleKeys.TrimXCIFileFailedPrimaryText],
|
||||||
|
notifyUser
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task TrimXCIFile(string filename)
|
||||||
|
{
|
||||||
|
if (filename == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmer = new Ryujinx.Common.Utilities.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;
|
||||||
|
var 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
|
||||||
|
{
|
||||||
|
var 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
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
IsVisible="{Binding EnableNonGameRunningControls}">
|
IsVisible="{Binding EnableNonGameRunningControls}">
|
||||||
<Grid Margin="0">
|
<Grid Margin="0">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition />
|
<ColumnDefinition />
|
||||||
|
@ -60,9 +61,16 @@
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
IsVisible="{Binding EnableNonGameRunningControls}"
|
IsVisible="{Binding EnableNonGameRunningControls}"
|
||||||
Text="{locale:Locale StatusBarGamesLoaded}" />
|
Text="{locale:Locale StatusBarGamesLoaded}" />
|
||||||
|
<TextBlock
|
||||||
|
Name="StatusBarProgressStatus"
|
||||||
|
Grid.Column="2"
|
||||||
|
Margin="10,0,5,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding StatusBarProgressStatusVisible}"
|
||||||
|
Text="{Binding StatusBarProgressStatusText}" />
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
Name="LoadProgressBar"
|
Name="LoadProgressBar"
|
||||||
Grid.Column="2"
|
Grid.Column="3"
|
||||||
Height="6"
|
Height="6"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Foreground="{DynamicResource SystemAccentColorLight2}"
|
Foreground="{DynamicResource SystemAccentColorLight2}"
|
||||||
|
|
|
@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging
|
||||||
TamperMachine,
|
TamperMachine,
|
||||||
Ui,
|
Ui,
|
||||||
Vic,
|
Vic,
|
||||||
|
XCIFileTrimmer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs
Normal file
30
src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
481
src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs
Normal file
481
src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs
Normal file
|
@ -0,0 +1,481 @@
|
||||||
|
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 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";
|
||||||
|
|
||||||
|
private static readonly Dictionary<byte, long> _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 (System.IO.Path.GetExtension(filename).ToUpperInvariant() == ".XCI")
|
||||||
|
{
|
||||||
|
var trimmer = new XCIFileTrimmer(filename, log);
|
||||||
|
return trimmer.CanBeTrimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => this._filename;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
this._filename = value;
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long Pos
|
||||||
|
{
|
||||||
|
get => this._fileStream.Position;
|
||||||
|
set => this._fileStream.Position = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public XCIFileTrimmer(string path, ILog log = null)
|
||||||
|
{
|
||||||
|
this.Log = log;
|
||||||
|
this.Filename = path;
|
||||||
|
ReadHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckFreeSpace()
|
||||||
|
{
|
||||||
|
if (this.FreeSpaceChecked)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (this.CanBeTrimmed)
|
||||||
|
{
|
||||||
|
this._freeSpaceValid = false;
|
||||||
|
|
||||||
|
OpenReaders();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.Pos = this.TrimmedFileSizeB;
|
||||||
|
var buffer = new byte[BufferSize];
|
||||||
|
var readSizeB = this.FileSizeB - this.TrimmedFileSizeB;
|
||||||
|
var reads = readSizeB / XCIFileTrimmer.BufferSize;
|
||||||
|
long read = 0;
|
||||||
|
|
||||||
|
var time = Performance.Measure(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var bytes = _fileStream.Read(buffer, 0, XCIFileTrimmer.BufferSize);
|
||||||
|
if (bytes == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (buffer.Take(bytes).AsParallel().Any(b => b != XCIFileTrimmer.PaddingByte))
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Warn, "Free space is NOT valid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log?.Progress(read, reads, "Verifying file can be trimmed", false);
|
||||||
|
read++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Log?.Progress(reads, reads, "Verifying file can be trimmed", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (time.TotalSeconds > 0)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Info, $"Checked at {readSizeB / (double)XCIFileTrimmer.BytesInAMegabyte / time.TotalSeconds:N} Mb/sec");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log?.Write(LogType.Info, "Free space is valid");
|
||||||
|
this._freeSpaceValid = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Warn, "There is no free space to check.");
|
||||||
|
this._freeSpaceValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this._freeSpaceChecked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Reset()
|
||||||
|
{
|
||||||
|
this._freeSpaceChecked = false;
|
||||||
|
this._freeSpaceValid = false;
|
||||||
|
ReadHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
public OperationOutcome Trim()
|
||||||
|
{
|
||||||
|
if (!this.FileOK)
|
||||||
|
{
|
||||||
|
return OperationOutcome.InvalidXCIFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.CanBeTrimmed)
|
||||||
|
{
|
||||||
|
return OperationOutcome.NoTrimNecessary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.FreeSpaceChecked)
|
||||||
|
{
|
||||||
|
CheckFreeSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.FreeSpaceValid)
|
||||||
|
{
|
||||||
|
return OperationOutcome.FreeSpaceCheckFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log?.Write(LogType.Info, "Trimming...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = new FileInfo(this.Filename);
|
||||||
|
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute");
|
||||||
|
File.SetAttributes(this.Filename, info.Attributes & ~FileAttributes.ReadOnly);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, e.ToString());
|
||||||
|
return OperationOutcome.ReadOnlyFileCannotFix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.Length != this.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(this.TrimmedFileSizeB);
|
||||||
|
return OperationOutcome.Successful;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
outfileStream.Close();
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, e.ToString());
|
||||||
|
return OperationOutcome.FileIOWriteError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public OperationOutcome Untrim()
|
||||||
|
{
|
||||||
|
if (!this.FileOK)
|
||||||
|
{
|
||||||
|
return OperationOutcome.InvalidXCIFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.CanBeUntrimmed)
|
||||||
|
{
|
||||||
|
return OperationOutcome.NoUntrimPossible;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Info, "Untrimming...");
|
||||||
|
|
||||||
|
var info = new FileInfo(this.Filename);
|
||||||
|
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute");
|
||||||
|
File.SetAttributes(this.Filename, info.Attributes & ~FileAttributes.ReadOnly);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, e.ToString());
|
||||||
|
return OperationOutcome.ReadOnlyFileCannotFix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.Length != this.FileSizeB)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, "File size has changed, cannot safely untrim.");
|
||||||
|
return OperationOutcome.FileSizeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outfileStream = new FileStream(this._filename, FileMode.Append, FileAccess.Write, FileShare.Write);
|
||||||
|
var buffer = new byte[BufferSize];
|
||||||
|
Array.Fill<byte>(buffer, XCIFileTrimmer.PaddingByte);
|
||||||
|
var bytesToWriteB = this.UntrimmedFileSizeB - this.FileSizeB;
|
||||||
|
var bytesLeftToWriteB = bytesToWriteB;
|
||||||
|
var writes = bytesLeftToWriteB / XCIFileTrimmer.BufferSize;
|
||||||
|
var write = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var time = Performance.Measure(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (bytesLeftToWriteB > 0)
|
||||||
|
{
|
||||||
|
var 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void OpenReaders()
|
||||||
|
{
|
||||||
|
if (_binaryReader == null)
|
||||||
|
{
|
||||||
|
this._fileStream = new FileStream(this._filename, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
this._binaryReader = new BinaryReader(this._fileStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CloseReaders()
|
||||||
|
{
|
||||||
|
if (this._binaryReader != null && this._binaryReader.BaseStream != null)
|
||||||
|
this._binaryReader.Close();
|
||||||
|
this._binaryReader = null;
|
||||||
|
this._fileStream = null;
|
||||||
|
GC.Collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReadHeader()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
OpenReaders();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Attempt without key area
|
||||||
|
var success = CheckAndReadHeader(false);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
// Attempt with key area
|
||||||
|
success = CheckAndReadHeader(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._fileOK = success;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseReaders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log?.Write(LogType.Error, ex.Message);
|
||||||
|
this._fileOK = false;
|
||||||
|
this._dataSizeB = 0;
|
||||||
|
this._cartSizeB = 0;
|
||||||
|
this._fileSizeB = 0;
|
||||||
|
this._offsetB = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckAndReadHeader(bool assumeKeyArea)
|
||||||
|
{
|
||||||
|
// Read file size
|
||||||
|
this._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
|
||||||
|
this._offsetB = (long)(assumeKeyArea ? XCIFileTrimmer.CartKeyAreaSize : 0);
|
||||||
|
|
||||||
|
// Check header
|
||||||
|
this.Pos = _offsetB + XCIFileTrimmer.HeaderFilePos;
|
||||||
|
var 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
|
||||||
|
this.Pos = _offsetB + XCIFileTrimmer.CartSizeFilePos;
|
||||||
|
var 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");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._cartSizeB = cartSizeNGB * XCIFileTrimmer.CartSizeMBinFormattedGB * XCIFileTrimmer.BytesInAMegabyte;
|
||||||
|
|
||||||
|
// Read data size
|
||||||
|
this.Pos = _offsetB + XCIFileTrimmer.DataSizeFilePos;
|
||||||
|
var records = (long)BitConverter.ToUInt32(_binaryReader.ReadBytes(4), 0);
|
||||||
|
this._dataSizeB = RecordsToByte(records);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -132,6 +132,7 @@ namespace Ryujinx.Ui
|
||||||
[GUI] ScrolledWindow _gameTableWindow;
|
[GUI] ScrolledWindow _gameTableWindow;
|
||||||
[GUI] Label _gpuName;
|
[GUI] Label _gpuName;
|
||||||
[GUI] Label _progressLabel;
|
[GUI] Label _progressLabel;
|
||||||
|
[GUI] Label _progressStatusLabel;
|
||||||
[GUI] Label _firmwareVersionLabel;
|
[GUI] Label _firmwareVersionLabel;
|
||||||
[GUI] Gtk.ProgressBar _progressBar;
|
[GUI] Gtk.ProgressBar _progressBar;
|
||||||
[GUI] Box _viewBox;
|
[GUI] Box _viewBox;
|
||||||
|
@ -719,6 +720,34 @@ namespace Ryujinx.Ui
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void StartProgress(string action)
|
||||||
|
{
|
||||||
|
Application.Invoke(delegate
|
||||||
|
{
|
||||||
|
_progressStatusLabel.Text = action;
|
||||||
|
_progressStatusLabel.Visible = true;
|
||||||
|
_progressBar.Fraction = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateProgress(double percentage)
|
||||||
|
{
|
||||||
|
Application.Invoke(delegate
|
||||||
|
{
|
||||||
|
_progressBar.Fraction = percentage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndProgress()
|
||||||
|
{
|
||||||
|
Application.Invoke(delegate
|
||||||
|
{
|
||||||
|
_progressStatusLabel.Text = String.Empty;
|
||||||
|
_progressStatusLabel.Visible = false;
|
||||||
|
_progressBar.Fraction = 1.0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void UpdateGameTable()
|
public void UpdateGameTable()
|
||||||
{
|
{
|
||||||
if (_updatingGameTable || _gameLoaded)
|
if (_updatingGameTable || _gameLoaded)
|
||||||
|
|
|
@ -667,6 +667,22 @@
|
||||||
<property name="position">1</property>
|
<property name="position">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="_progressStatusLabel">
|
||||||
|
<property name="visible">False</property>
|
||||||
|
<property name="can-focus">False</property>
|
||||||
|
<property name="margin-left">10</property>
|
||||||
|
<property name="margin-right">5</property>
|
||||||
|
<property name="margin-top">2</property>
|
||||||
|
<property name="margin-bottom">2</property>
|
||||||
|
<property name="label" translatable="yes"></property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="expand">False</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkProgressBar" id="_progressBar">
|
<object class="GtkProgressBar" id="_progressBar">
|
||||||
<property name="width-request">200</property>
|
<property name="width-request">200</property>
|
||||||
|
@ -680,7 +696,7 @@
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">True</property>
|
||||||
<property name="position">2</property>
|
<property name="position">3</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
|
|
@ -25,6 +25,7 @@ namespace Ryujinx.Ui.Widgets
|
||||||
private MenuItem _openPtcDirMenuItem;
|
private MenuItem _openPtcDirMenuItem;
|
||||||
private MenuItem _openShaderCacheDirMenuItem;
|
private MenuItem _openShaderCacheDirMenuItem;
|
||||||
private MenuItem _createShortcutMenuItem;
|
private MenuItem _createShortcutMenuItem;
|
||||||
|
private MenuItem _trimXCIMenuItem;
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
|
@ -198,6 +199,15 @@ namespace Ryujinx.Ui.Widgets
|
||||||
};
|
};
|
||||||
_createShortcutMenuItem.Activated += CreateShortcut_Clicked;
|
_createShortcutMenuItem.Activated += CreateShortcut_Clicked;
|
||||||
|
|
||||||
|
//
|
||||||
|
// _trimXCIMenuItem
|
||||||
|
//
|
||||||
|
_trimXCIMenuItem = new MenuItem("Check and Trim XCI File")
|
||||||
|
{
|
||||||
|
TooltipText = "Check and Trim XCI File to Save Disk Space."
|
||||||
|
};
|
||||||
|
_trimXCIMenuItem.Activated += TrimXCI_Clicked;
|
||||||
|
|
||||||
ShowComponent();
|
ShowComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,6 +223,7 @@ namespace Ryujinx.Ui.Widgets
|
||||||
_manageSubMenu.Append(_openShaderCacheDirMenuItem);
|
_manageSubMenu.Append(_openShaderCacheDirMenuItem);
|
||||||
|
|
||||||
Add(_createShortcutMenuItem);
|
Add(_createShortcutMenuItem);
|
||||||
|
Add(_trimXCIMenuItem);
|
||||||
Add(new SeparatorMenuItem());
|
Add(new SeparatorMenuItem());
|
||||||
Add(_openSaveUserDirMenuItem);
|
Add(_openSaveUserDirMenuItem);
|
||||||
Add(_openSaveDeviceDirMenuItem);
|
Add(_openSaveDeviceDirMenuItem);
|
||||||
|
|
|
@ -79,6 +79,7 @@ namespace Ryujinx.Ui.Widgets
|
||||||
_extractLogoMenuItem.Sensitive = hasNca;
|
_extractLogoMenuItem.Sensitive = hasNca;
|
||||||
|
|
||||||
_createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild;
|
_createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild;
|
||||||
|
_trimXCIMenuItem.Sensitive = Ryujinx.Common.Utilities.XCIFileTrimmer.CanTrim(_titleFilePath, new XCIFileTrimmerLog(_parent));
|
||||||
|
|
||||||
PopupAtPointer(null);
|
PopupAtPointer(null);
|
||||||
}
|
}
|
||||||
|
@ -640,5 +641,88 @@ namespace Ryujinx.Ui.Widgets
|
||||||
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
|
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
|
||||||
ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
|
ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ProcessTrimResult(String filename, Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome operationOutcome)
|
||||||
|
{
|
||||||
|
string notifyUser = null;
|
||||||
|
|
||||||
|
switch (operationOutcome)
|
||||||
|
{
|
||||||
|
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.NoTrimNecessary:
|
||||||
|
notifyUser = "XCI File does not need to be trimmed. Check logs for further details";
|
||||||
|
break;
|
||||||
|
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.ReadOnlyFileCannotFix:
|
||||||
|
notifyUser = "XCI File is Read Only and could not be made writable. Check logs for further details";
|
||||||
|
break;
|
||||||
|
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FreeSpaceCheckFailed:
|
||||||
|
notifyUser = "XCI File has data in the free space area, it is not safe to trim";
|
||||||
|
break;
|
||||||
|
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.InvalidXCIFile:
|
||||||
|
notifyUser = "XCI File contains invalid data. Check logs for further details";
|
||||||
|
break;
|
||||||
|
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FileIOWriteError:
|
||||||
|
notifyUser = "XCI File could not be opened for writing. Check logs for further details";
|
||||||
|
break;
|
||||||
|
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.FileSizeChanged:
|
||||||
|
notifyUser = "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyUser != null)
|
||||||
|
{
|
||||||
|
GtkDialog.CreateWarningDialog("Trimming of the XCI file failed", notifyUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrimXCI_Clicked(object sender, EventArgs args)
|
||||||
|
{
|
||||||
|
if (_titleFilePath == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmer = new Ryujinx.Common.Utilities.XCIFileTrimmer(_titleFilePath, new XCIFileTrimmerLog(_parent));
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
using MessageDialog confirmationDialog = GtkDialog.CreateConfirmationDialog(
|
||||||
|
$"This function will first check the empty space and then trim the XCI File to save disk space. Continue?",
|
||||||
|
$"Current File Size: {currentFileSize:n} MB\n" +
|
||||||
|
$"Game Data Size: {cartDataSize:n} MB\n" +
|
||||||
|
$"Disk Space Savings: {savings:n} MB\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmationDialog.Run() == (int)ResponseType.Yes)
|
||||||
|
{
|
||||||
|
Thread xciFileTrimmerThread = new(() =>
|
||||||
|
{
|
||||||
|
_parent.StartProgress($"Trimming file '{_titleFilePath}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var oeprationOutcome = trimmer.Trim();
|
||||||
|
|
||||||
|
Gtk.Application.Invoke(delegate
|
||||||
|
{
|
||||||
|
ProcessTrimResult(_titleFilePath, oeprationOutcome);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_parent.EndProgress();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Name = "GUI.XCIFileTrimmerThread",
|
||||||
|
IsBackground = true,
|
||||||
|
};
|
||||||
|
xciFileTrimmerThread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
src/Ryujinx/Ui/XCIFileTrimmerLog.cs
Normal file
27
src/Ryujinx/Ui/XCIFileTrimmerLog.cs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ui
|
||||||
|
{
|
||||||
|
public class XCIFileTrimmerLog : Ryujinx.Common.Logging.XCIFileTrimmerLog
|
||||||
|
{
|
||||||
|
private readonly MainWindow _mainWindow;
|
||||||
|
|
||||||
|
public XCIFileTrimmerLog(MainWindow mainWindow)
|
||||||
|
{
|
||||||
|
_mainWindow = mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Progress(long current, long total, string text, bool complete)
|
||||||
|
{
|
||||||
|
if (!complete)
|
||||||
|
{
|
||||||
|
_mainWindow.UpdateProgress((double)current / (double)total);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_mainWindow.EndProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue