mirror of
https://github.com/ryujinx-mirror/ryujinx.git
synced 2024-12-22 17:05:45 +00:00
Add ability to trim XCI files from the application context menu (#33)
This commit is contained in:
parent
7e9a293dab
commit
6253fe143a
15 changed files with 915 additions and 2 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
507
src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs
Normal file
507
src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs
Normal file
|
@ -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";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cartridge Sizes (ByteIdentifier, SizeInGB)
|
||||||
|
/// </summary>
|
||||||
|
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 (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<byte>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -134,6 +134,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;
|
||||||
|
@ -727,6 +728,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,6 +234,8 @@ namespace Ryujinx.UI.Widgets
|
||||||
Add(_openTitleModDirMenuItem);
|
Add(_openTitleModDirMenuItem);
|
||||||
Add(_openTitleSdModDirMenuItem);
|
Add(_openTitleSdModDirMenuItem);
|
||||||
Add(new SeparatorMenuItem());
|
Add(new SeparatorMenuItem());
|
||||||
|
Add(_trimXCIMenuItem);
|
||||||
|
Add(new SeparatorMenuItem());
|
||||||
Add(_manageCacheMenuItem);
|
Add(_manageCacheMenuItem);
|
||||||
Add(_extractMenuItem);
|
Add(_extractMenuItem);
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
|
@ -75,6 +76,7 @@ namespace Ryujinx.UI.Widgets
|
||||||
_extractLogoMenuItem.Sensitive = hasNca;
|
_extractLogoMenuItem.Sensitive = hasNca;
|
||||||
|
|
||||||
_createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild;
|
_createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild;
|
||||||
|
_trimXCIMenuItem.Sensitive = _applicationData != null && Ryujinx.Common.Utilities.XCIFileTrimmer.CanTrim(_applicationData.Path, new XCIFileTrimmerLog(_parent));
|
||||||
|
|
||||||
PopupAtPointer(null);
|
PopupAtPointer(null);
|
||||||
}
|
}
|
||||||
|
@ -630,5 +632,91 @@ namespace Ryujinx.UI.Widgets
|
||||||
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem, checkLevel).GetApplicationIcon(_applicationData.Path, ConfigurationState.Instance.System.Language, _applicationData.Id);
|
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem, checkLevel).GetApplicationIcon(_applicationData.Path, ConfigurationState.Instance.System.Language, _applicationData.Id);
|
||||||
ShortcutHelper.CreateAppShortcut(_applicationData.Path, _applicationData.Name, _applicationData.IdString, appIcon);
|
ShortcutHelper.CreateAppShortcut(_applicationData.Path, _applicationData.Name, _applicationData.IdString, 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;
|
||||||
|
case Ryujinx.Common.Utilities.XCIFileTrimmer.OperationOutcome.Successful:
|
||||||
|
_parent.UpdateGameTable();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyUser != null)
|
||||||
|
{
|
||||||
|
GtkDialog.CreateWarningDialog("Trimming of the XCI file failed", notifyUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrimXCI_Clicked(object sender, EventArgs args)
|
||||||
|
{
|
||||||
|
if (_applicationData?.Path == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmer = new XCIFileTrimmer(_applicationData.Path, 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 '{_applicationData.Path}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
XCIFileTrimmer.OperationOutcome operationOutcome = trimmer.Trim();
|
||||||
|
|
||||||
|
Gtk.Application.Invoke(delegate
|
||||||
|
{
|
||||||
|
ProcessTrimResult(_applicationData.Path, operationOutcome);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_parent.EndProgress();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Name = "GUI.XCIFileTrimmerThread",
|
||||||
|
IsBackground = true,
|
||||||
|
};
|
||||||
|
xciFileTrimmerThread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
src/Ryujinx.Gtk3/UI/XCIFileTrimmerLog.cs
Normal file
27
src/Ryujinx.Gtk3/UI/XCIFileTrimmerLog.cs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.UI
|
||||||
|
{
|
||||||
|
internal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ namespace Ryujinx.HLE.Generators
|
||||||
var syntaxReceiver = (ServiceSyntaxReceiver)context.SyntaxReceiver;
|
var syntaxReceiver = (ServiceSyntaxReceiver)context.SyntaxReceiver;
|
||||||
CodeGenerator generator = new CodeGenerator();
|
CodeGenerator generator = new CodeGenerator();
|
||||||
|
|
||||||
|
generator.AppendLine("#nullable enable");
|
||||||
generator.AppendLine("using System;");
|
generator.AppendLine("using System;");
|
||||||
generator.EnterScope($"namespace Ryujinx.HLE.HOS.Services.Sm");
|
generator.EnterScope($"namespace Ryujinx.HLE.HOS.Services.Sm");
|
||||||
generator.EnterScope($"partial class IUserInterface");
|
generator.EnterScope($"partial class IUserInterface");
|
||||||
|
@ -58,6 +59,7 @@ namespace Ryujinx.HLE.Generators
|
||||||
|
|
||||||
generator.LeaveScope();
|
generator.LeaveScope();
|
||||||
generator.LeaveScope();
|
generator.LeaveScope();
|
||||||
|
generator.AppendLine("#nullable disable");
|
||||||
context.AddSource($"IUserInterface.g.cs", generator.ToString());
|
context.AddSource($"IUserInterface.g.cs", generator.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,8 +82,11 @@
|
||||||
"GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods",
|
"GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods",
|
||||||
"GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory",
|
"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.",
|
"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",
|
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
|
||||||
"StatusBarSystemVersion": "System Version: {0}",
|
"StatusBarSystemVersion": "System Version: {0}",
|
||||||
|
"StatusBarXCIFileTrimming": "Trimming XCI File '{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}",
|
||||||
"LinuxVmMaxMapCountDialogTextSecondary": "Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.",
|
"LinuxVmMaxMapCountDialogTextSecondary": "Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.",
|
||||||
|
@ -704,6 +707,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})",
|
||||||
|
@ -714,6 +727,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/Common/XCIFileTrimmerLog.cs
Normal file
24
src/Ryujinx/Common/XCIFileTrimmerLog.cs
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -59,6 +59,12 @@
|
||||||
Click="OpenSdModsDirectory_Click"
|
Click="OpenSdModsDirectory_Click"
|
||||||
Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
|
Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem
|
||||||
|
Click="TrimXCI_Click"
|
||||||
|
Header="{locale:Locale GameListContextMenuTrimXCI}"
|
||||||
|
IsEnabled="{Binding TrimXCIEnabled}"
|
||||||
|
ToolTip.Tip="{locale:Locale GameListContextMenuTrimXCIToolTip}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
|
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Threading;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
|
@ -15,6 +16,8 @@ using Ryujinx.UI.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Controls
|
namespace Ryujinx.Ava.UI.Controls
|
||||||
|
@ -355,5 +358,15 @@ namespace Ryujinx.Ava.UI.Controls
|
||||||
await viewModel.LoadApplication(viewModel.SelectedApplication);
|
await viewModel.LoadApplication(viewModel.SelectedApplication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.Cpu;
|
using Ryujinx.Cpu;
|
||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
@ -36,6 +37,7 @@ using SkiaSharp;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -78,6 +80,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;
|
||||||
|
@ -366,6 +370,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||||
|
|
||||||
public bool OpenDeviceSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0;
|
public bool OpenDeviceSaveDirectoryEnabled => !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 OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
||||||
|
|
||||||
public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild;
|
public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild;
|
||||||
|
@ -480,6 +486,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;
|
||||||
|
@ -1747,6 +1775,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
|
#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}"
|
||||||
|
|
Loading…
Reference in a new issue