Compare commits
15 Commits
r.cfa287f
...
mirror/mas
Author | SHA1 | Date | |
---|---|---|---|
|
fec4f4ada9 | ||
|
49574a99f5 | ||
|
68092bf00b | ||
|
6253fe143a | ||
|
7e9a293dab | ||
|
d3619bc6fb | ||
|
e5fdbd0b83 | ||
|
c4ee9c7555 | ||
|
80fa93faef | ||
|
b4cac89c1f | ||
|
0e5ce0bd20 | ||
|
dc545c33e4 | ||
|
51b956ac7f | ||
|
2880892c2c | ||
|
9c5dda1848 |
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Latte Softworks Discord
|
||||
url: https://latte.to/discord
|
||||
- name: ryujinx-mirror (Discord)
|
||||
url: https://discord.gg/xmHPGDfVCa
|
||||
about: This is the home of development for the ryujinx-mirror fork, feel free to make a post in `#ryujinx-help` for general support & technical issues
|
||||
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -93,15 +93,19 @@ jobs:
|
||||
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x tools/appimagetool
|
||||
|
||||
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
|
||||
if [ "$PLATFORM_NAME" = "linux-x64" ]; then
|
||||
ARCH_NAME=x64
|
||||
export ARCH=x86_64
|
||||
elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
|
||||
ARCH_NAME=arm64
|
||||
export ARCH=aarch64
|
||||
else
|
||||
echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
|
||||
BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh
|
||||
shell: bash
|
||||
|
||||
|
55
.github/workflows/release.yml
vendored
55
.github/workflows/release.yml
vendored
@ -106,30 +106,71 @@ jobs:
|
||||
- name: Packing Windows builds
|
||||
if: matrix.platform.os == 'windows-latest'
|
||||
run: |
|
||||
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
|
||||
ZIP_OS_NAME="${{ matrix.platform.zip_os_name }}"
|
||||
|
||||
pushd publish_ava
|
||||
cp Ryujinx.exe Ryujinx.Ava.exe
|
||||
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip *
|
||||
7z a ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip *
|
||||
7z a ../release_output/ryujinx-$BUILD_VERSION-$ZIP_OS_NAME.zip *
|
||||
popd
|
||||
|
||||
pushd publish_sdl2_headless
|
||||
7z a ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip *
|
||||
7z a ../release_output/sdl2-ryujinx-headless-$BUILD_VERSION-$ZIP_OS_NAME.zip *
|
||||
popd
|
||||
shell: bash
|
||||
|
||||
- name: Build AppImage (Linux)
|
||||
if: matrix.platform.os == 'ubuntu-latest'
|
||||
run: |
|
||||
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
|
||||
PLATFORM_NAME="${{ matrix.platform.name }}"
|
||||
|
||||
sudo apt install -y zsync desktop-file-utils appstream
|
||||
|
||||
mkdir -p tools
|
||||
export PATH="$PATH:$(readlink -f tools)"
|
||||
|
||||
# Setup appimagetool
|
||||
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x tools/appimagetool
|
||||
|
||||
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
|
||||
if [ "$PLATFORM_NAME" = "linux-x64" ]; then
|
||||
ARCH_NAME=x64
|
||||
export ARCH=x86_64
|
||||
elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
|
||||
ARCH_NAME=arm64
|
||||
export ARCH=aarch64
|
||||
else
|
||||
echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
|
||||
BUILDDIR=publish_ava OUTDIR=publish_ava_appimage distribution/linux/appimage/build-appimage.sh
|
||||
|
||||
# Add to release output
|
||||
pushd publish_ava_appimage
|
||||
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
|
||||
mv Ryujinx.AppImage.zsync ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync
|
||||
popd
|
||||
shell: bash
|
||||
|
||||
- name: Packing Linux builds
|
||||
if: matrix.platform.os == 'ubuntu-latest'
|
||||
run: |
|
||||
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
|
||||
ZIP_OS_NAME="${{ matrix.platform.zip_os_name }}"
|
||||
|
||||
pushd publish_ava
|
||||
cp Ryujinx Ryujinx.Ava
|
||||
chmod +x Ryujinx.sh Ryujinx Ryujinx.Ava
|
||||
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz *
|
||||
tar -czvf ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz *
|
||||
tar -czvf ../release_output/ryujinx-$BUILD_VERSION-$ZIP_OS_NAME.tar.gz *
|
||||
popd
|
||||
|
||||
pushd publish_sdl2_headless
|
||||
chmod +x Ryujinx.sh Ryujinx.Headless.SDL2
|
||||
tar -czvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz *
|
||||
tar -czvf ../release_output/sdl2-ryujinx-headless-$BUILD_VERSION-$ZIP_OS_NAME.tar.gz *
|
||||
popd
|
||||
shell: bash
|
||||
|
||||
@ -138,7 +179,7 @@ jobs:
|
||||
with:
|
||||
name: ${{ steps.version_info.outputs.build_version }}
|
||||
tag: ${{ steps.version_info.outputs.build_version }}
|
||||
artifacts: "release_output/*.tar.gz,release_output/*.zip"
|
||||
artifacts: "release_output/*.tar.gz,release_output/*.zip,release_output/*AppImage*"
|
||||
draft: "true"
|
||||
omitBody: true
|
||||
#omitBodyDuringUpdate: true
|
||||
|
@ -3,11 +3,11 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="11.1.3" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.1.3" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.1.3" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.1.3" />
|
||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.1.3" />
|
||||
<PackageVersion Include="Avalonia" Version="11.1.4" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.1.4" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.1.4" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.1.4" />
|
||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.1.4" />
|
||||
<PackageVersion Include="Avalonia.Svg" Version="11.1.0.1" />
|
||||
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.1.0.1" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
@ -20,7 +20,7 @@
|
||||
<PackageVersion Include="LibHac" Version="0.19.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.1.2" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
|
||||
|
@ -1,13 +1,15 @@
|
||||
[links/discord]: https://discord.gg/xmHPGDfVCa
|
||||
[badges/discord]: https://img.shields.io/discord/1291765437100720243?label=ryujinx-mirror&logo=discord&logoColor=FFFFFF&color=5865F3
|
||||
|
||||
As of now, the [ryujinx-mirror/ryujinx](https://github.com/ryujinx-mirror/ryujinx) repository serves as a downstream hard-fork of the original Ryujinx project. For the time being, this fork won't be accepting any new *major* changes until further information arises. We have reconstructed the essential build infrastructure, and you can download nightly binaries for Windows, Linux, and MacOS from the [latest release](https://github.com/ryujinx-mirror/ryujinx/releases/latest).
|
||||
As of now, the [ryujinx-mirror/ryujinx](https://github.com/ryujinx-mirror/ryujinx) repository serves as a downstream hard fork of the original Ryujinx project. You can download nightly binaries for Windows, macOS, and Linux (including `AppImage`s) from the [latest release](https://github.com/ryujinx-mirror/ryujinx/releases/latest).
|
||||
|
||||
> [!NOTE]
|
||||
> This fork is not affiliated with the **original** Ryujinx project, or Nintendo whatsoever.
|
||||
|
||||
### Current Goals
|
||||
|
||||
If you would like a version with more new features & improvements, feel free to check out [GreemDev's fork](https://github.com/GreemDev/Ryujinx). We aim to keep this repository more focused on small fixes and infrastructure reconstruction, staying more true to the original Ryujinx project.
|
||||
|
||||
* ☑️ Reconstruct basic build infrastructure & workflows for this repository, based on revision hashes as opposed to semver releases (for now)
|
||||
* ☑️ To be as safe as possible, remove all previous in-app and meta references to Patreon, `ryujinx.org` etc while keeping full attribution of original authors and contributors in-tact.
|
||||
* Keep 'branding' as pure and faithful to the original project as possible.
|
||||
|
@ -1,30 +1,30 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -eu
|
||||
|
||||
ROOTDIR="$(readlink -f "$(dirname "$0")")"/../../../
|
||||
cd "$ROOTDIR"
|
||||
|
||||
BUILDDIR=${BUILDDIR:-publish}
|
||||
OUTDIR=${OUTDIR:-publish_appimage}
|
||||
UFLAG=${UFLAG:-"gh-releases-zsync|ryujinx-mirror|ryujinx|latest|*-x64.AppImage.zsync"}
|
||||
|
||||
rm -rf AppDir
|
||||
mkdir -p AppDir/usr/bin/bin
|
||||
mkdir -p AppDir/usr/bin
|
||||
|
||||
# Ensure necessary bins are set as executable
|
||||
chmod +x "$BUILDDIR"/Ryujinx*
|
||||
|
||||
# Add symlinks for the AppImage
|
||||
cp distribution/linux/Ryujinx.desktop AppDir/Ryujinx.desktop
|
||||
cp distribution/linux/appimage/AppRun AppDir/AppRun
|
||||
cp distribution/misc/Logo.svg AppDir/Ryujinx.svg
|
||||
|
||||
cp -r "$BUILDDIR"/* AppDir/usr/bin/
|
||||
|
||||
# Ensure necessary bins are set as executable
|
||||
chmod +x AppDir/AppRun AppDir/usr/bin/Ryujinx*
|
||||
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
appimagetool --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 21 \
|
||||
-u "gh-releases-zsync|$GITHUB_REPOSITORY_OWNER|Ryujinx|latest|*.AppImage.zsync" \
|
||||
-u "$UFLAG" \
|
||||
AppDir "$OUTDIR"/Ryujinx.AppImage
|
||||
|
||||
# ??
|
||||
# Move zsync file needed for delta updates
|
||||
mv ./*.AppImage.zsync "$OUTDIR"
|
||||
|
@ -110,11 +110,11 @@ gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
|
||||
rm "$RELEASE_TAR_FILE_NAME"
|
||||
|
||||
# Create legacy update package for Avalonia to not left behind old testers.
|
||||
if [ "$VERSION" != "1.1.0" ];
|
||||
then
|
||||
cp $RELEASE_TAR_FILE_NAME.gz test-ava-ryujinx-$VERSION-macos_universal.app.tar.gz
|
||||
fi
|
||||
#if [ "$VERSION" != "1.1.0" ];
|
||||
#then
|
||||
# cp $RELEASE_TAR_FILE_NAME.gz test-ava-ryujinx-$VERSION-macos_universal.app.tar.gz
|
||||
#fi
|
||||
|
||||
popd
|
||||
|
||||
echo "Done"
|
||||
echo "Done"
|
||||
|
@ -5,7 +5,7 @@ Using an IDE that supports the `.editorconfig` standard will make this much simp
|
||||
|
||||
1. We use [Allman style](http://en.wikipedia.org/wiki/Indent_style#Allman_style) braces, where each brace begins on a new line. A single line statement block can go without braces but the block must be properly indented on its own line and must not be nested in other statement blocks that use braces (See rule 18 for more details). One exception is that a `using` statement is permitted to be nested within another `using` statement by starting on the following line at the same indentation level, even if the nested `using` contains a controlled block.
|
||||
2. We use four spaces of indentation (no tabs).
|
||||
3. We use `_camelCase` for internal and private fields and use `readonly` where possible. Prefix internal and private instance fields with `_`, static fields with `s_` and thread static fields with `t_`. When used on static fields, `readonly` should come after `static` (e.g. `static readonly` not `readonly static`). Public fields should be used sparingly and should use PascalCasing with no prefix when used.
|
||||
3. We use `_camelCase` for internal and private fields and use `readonly` where possible. Prefix internal and private instance fields with `_`, thread static fields with `t_`. When used on static fields, `readonly` should come after `static` (e.g. `static readonly` not `readonly static`). Public fields should be used sparingly and should use PascalCasing with no prefix when used.
|
||||
4. We avoid `this.` unless absolutely necessary.
|
||||
5. We always specify the visibility, even if it's the default (e.g.
|
||||
`private string _foo` not `string _foo`). Visibility should be the first modifier (e.g.
|
||||
|
@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging
|
||||
TamperMachine,
|
||||
UI,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -55,8 +55,10 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
if (_handle != BufferHandle.Null)
|
||||
{
|
||||
// May need to restride the vertex buffer.
|
||||
|
||||
if (gd.NeedsVertexBufferAlignment(AttributeScalarAlignment, out int alignment) && (_stride % alignment) != 0)
|
||||
//
|
||||
// Fix divide by zero when recovering from missed draw (Oct. 16 2024)
|
||||
// (fixes crash in 'Baldo: The Guardian Owls' opening cutscene)
|
||||
if (gd.NeedsVertexBufferAlignment(AttributeScalarAlignment, out int alignment) && alignment != 0 && (_stride % alignment) != 0)
|
||||
{
|
||||
autoBuffer = gd.BufferManager.GetAlignedVertexBuffer(cbs, _handle, _offset, _size, _stride, alignment);
|
||||
|
||||
|
@ -165,6 +165,11 @@ namespace Ryujinx
|
||||
? appDataConfigurationPath
|
||||
: null;
|
||||
|
||||
if (!string.IsNullOrEmpty(CommandLineState.OverrideConfigFile) && File.Exists(CommandLineState.OverrideConfigFile))
|
||||
{
|
||||
ConfigurationPath = CommandLineState.OverrideConfigFile;
|
||||
}
|
||||
|
||||
if (ConfigurationPath == null)
|
||||
{
|
||||
// No configuration, we load the default values and save it to disk
|
||||
|
@ -134,6 +134,7 @@ namespace Ryujinx.UI
|
||||
[GUI] ScrolledWindow _gameTableWindow;
|
||||
[GUI] Label _gpuName;
|
||||
[GUI] Label _progressLabel;
|
||||
[GUI] Label _progressStatusLabel;
|
||||
[GUI] Label _firmwareVersionLabel;
|
||||
[GUI] Gtk.ProgressBar _progressBar;
|
||||
[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()
|
||||
{
|
||||
if (_updatingGameTable || _gameLoaded)
|
||||
|
@ -667,6 +667,22 @@
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</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>
|
||||
<object class="GtkProgressBar" id="_progressBar">
|
||||
<property name="width-request">200</property>
|
||||
@ -680,7 +696,7 @@
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
@ -25,6 +25,7 @@ namespace Ryujinx.UI.Widgets
|
||||
private MenuItem _openPtcDirMenuItem;
|
||||
private MenuItem _openShaderCacheDirMenuItem;
|
||||
private MenuItem _createShortcutMenuItem;
|
||||
private MenuItem _trimXCIMenuItem;
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
@ -198,6 +199,15 @@ namespace Ryujinx.UI.Widgets
|
||||
};
|
||||
_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();
|
||||
}
|
||||
|
||||
@ -224,6 +234,8 @@ namespace Ryujinx.UI.Widgets
|
||||
Add(_openTitleModDirMenuItem);
|
||||
Add(_openTitleSdModDirMenuItem);
|
||||
Add(new SeparatorMenuItem());
|
||||
Add(_trimXCIMenuItem);
|
||||
Add(new SeparatorMenuItem());
|
||||
Add(_manageCacheMenuItem);
|
||||
Add(_extractMenuItem);
|
||||
|
||||
|
@ -13,6 +13,7 @@ using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
@ -75,6 +76,7 @@ namespace Ryujinx.UI.Widgets
|
||||
_extractLogoMenuItem.Sensitive = hasNca;
|
||||
|
||||
_createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild;
|
||||
_trimXCIMenuItem.Sensitive = _applicationData != null && Ryujinx.Common.Utilities.XCIFileTrimmer.CanTrim(_applicationData.Path, new XCIFileTrimmerLog(_parent));
|
||||
|
||||
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);
|
||||
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;
|
||||
CodeGenerator generator = new CodeGenerator();
|
||||
|
||||
generator.AppendLine("#nullable enable");
|
||||
generator.AppendLine("using System;");
|
||||
generator.EnterScope($"namespace Ryujinx.HLE.HOS.Services.Sm");
|
||||
generator.EnterScope($"partial class IUserInterface");
|
||||
@ -58,6 +59,7 @@ namespace Ryujinx.HLE.Generators
|
||||
|
||||
generator.LeaveScope();
|
||||
generator.LeaveScope();
|
||||
generator.AppendLine("#nullable disable");
|
||||
context.AddSource($"IUserInterface.g.cs", generator.ToString());
|
||||
}
|
||||
|
||||
|
@ -313,6 +313,32 @@ namespace Ryujinx.Input.SDL2
|
||||
return value * ConvertRate;
|
||||
}
|
||||
|
||||
private JoyconConfigControllerStick<GamepadInputId, Common.Configuration.Hid.Controller.StickInputId> GetLogicalJoyStickConfig(StickInputId inputId)
|
||||
{
|
||||
switch (inputId)
|
||||
{
|
||||
case StickInputId.Left:
|
||||
if (_configuration.RightJoyconStick.Joystick == Common.Configuration.Hid.Controller.StickInputId.Left)
|
||||
{
|
||||
return _configuration.RightJoyconStick;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _configuration.LeftJoyconStick;
|
||||
}
|
||||
case StickInputId.Right:
|
||||
if (_configuration.LeftJoyconStick.Joystick == Common.Configuration.Hid.Controller.StickInputId.Right)
|
||||
{
|
||||
return _configuration.LeftJoyconStick;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _configuration.RightJoyconStick;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
public (float, float) GetStick(StickInputId inputId)
|
||||
{
|
||||
if (inputId == StickInputId.Unbound)
|
||||
@ -343,24 +369,26 @@ namespace Ryujinx.Input.SDL2
|
||||
|
||||
if (HasConfiguration)
|
||||
{
|
||||
if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.InvertStickX) ||
|
||||
(inputId == StickInputId.Right && _configuration.RightJoyconStick.InvertStickX))
|
||||
{
|
||||
resultX = -resultX;
|
||||
}
|
||||
var joyconStickConfig = GetLogicalJoyStickConfig(inputId);
|
||||
|
||||
if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.InvertStickY) ||
|
||||
(inputId == StickInputId.Right && _configuration.RightJoyconStick.InvertStickY))
|
||||
if (joyconStickConfig != null)
|
||||
{
|
||||
resultY = -resultY;
|
||||
}
|
||||
if (joyconStickConfig.InvertStickX)
|
||||
{
|
||||
resultX = -resultX;
|
||||
}
|
||||
|
||||
if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.Rotate90CW) ||
|
||||
(inputId == StickInputId.Right && _configuration.RightJoyconStick.Rotate90CW))
|
||||
{
|
||||
float temp = resultX;
|
||||
resultX = resultY;
|
||||
resultY = -temp;
|
||||
if (joyconStickConfig.InvertStickY)
|
||||
{
|
||||
resultY = -resultY;
|
||||
}
|
||||
|
||||
if (joyconStickConfig.Rotate90CW)
|
||||
{
|
||||
float temp = resultX;
|
||||
resultX = resultY;
|
||||
resultY = -temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.UI.Common.Helper
|
||||
{
|
||||
@ -16,6 +17,7 @@ namespace Ryujinx.UI.Common.Helper
|
||||
public static string LaunchPathArg { get; private set; }
|
||||
public static string LaunchApplicationId { get; private set; }
|
||||
public static bool StartFullscreenArg { get; private set; }
|
||||
public static string OverrideConfigFile { get; private set; }
|
||||
|
||||
public static void ParseArguments(string[] args)
|
||||
{
|
||||
@ -96,6 +98,29 @@ namespace Ryujinx.UI.Common.Helper
|
||||
case "--software-gui":
|
||||
OverrideHardwareAcceleration = false;
|
||||
break;
|
||||
case "-c":
|
||||
case "--config":
|
||||
if (i + 1 >= args.Length)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
string configFile = args[++i];
|
||||
|
||||
if (Path.GetExtension(configFile).ToLower() != ".json")
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
OverrideConfigFile = configFile;
|
||||
|
||||
arguments.Add(arg);
|
||||
arguments.Add(args[i]);
|
||||
break;
|
||||
default:
|
||||
LaunchPathArg = arg;
|
||||
break;
|
||||
|
@ -82,8 +82,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.",
|
||||
@ -704,6 +707,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})",
|
||||
@ -714,6 +727,7 @@
|
||||
"DlcWindowHeading": "{0} Downloadable Content(s)",
|
||||
"ModWindowHeading": "{0} Mod(s)",
|
||||
"UserProfilesEditProfile": "Edit Selected",
|
||||
"Continue": "Continue",
|
||||
"Cancel": "Cancel",
|
||||
"Save": "Save",
|
||||
"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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -150,6 +150,11 @@ namespace Ryujinx.Ava
|
||||
ConfigurationPath = appDataConfigurationPath;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(CommandLineState.OverrideConfigFile) && File.Exists(CommandLineState.OverrideConfigFile))
|
||||
{
|
||||
ConfigurationPath = CommandLineState.OverrideConfigFile;
|
||||
}
|
||||
|
||||
if (ConfigurationPath == null)
|
||||
{
|
||||
// No configuration, we load the default values and save it to disk
|
||||
|
@ -59,6 +59,12 @@
|
||||
Click="OpenSdModsDirectory_Click"
|
||||
Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="TrimXCI_Click"
|
||||
Header="{locale:Locale GameListContextMenuTrimXCI}"
|
||||
IsEnabled="{Binding TrimXCIEnabled}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuTrimXCIToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
|
||||
<MenuItem
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.Common;
|
||||
@ -15,6 +16,8 @@ using Ryujinx.UI.Common.Helper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
@ -355,5 +358,15 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
private PlayerIndex _playerId;
|
||||
private int _controller;
|
||||
private int _controllerNumber;
|
||||
private string _controllerImage;
|
||||
private int _device;
|
||||
private object _configViewModel;
|
||||
@ -439,6 +438,24 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
public void LoadDevices()
|
||||
{
|
||||
string GetGamepadName(IGamepad gamepad, int controllerNumber)
|
||||
{
|
||||
return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
|
||||
}
|
||||
|
||||
string GetUniqueGamepadName(IGamepad gamepad, ref int controllerNumber)
|
||||
{
|
||||
string name = GetGamepadName(gamepad, controllerNumber);
|
||||
|
||||
if (Devices.Any(controller => controller.Name == name))
|
||||
{
|
||||
controllerNumber++;
|
||||
name = GetGamepadName(gamepad, controllerNumber);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
lock (Devices)
|
||||
{
|
||||
Devices.Clear();
|
||||
@ -455,23 +472,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
}
|
||||
}
|
||||
|
||||
int controllerNumber = 0;
|
||||
foreach (string id in _mainWindow.InputManager.GamepadDriver.GamepadsIds)
|
||||
{
|
||||
using IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
|
||||
|
||||
if (gamepad != null)
|
||||
{
|
||||
if (Devices.Any(controller => GetShortGamepadId(controller.Id) == GetShortGamepadId(gamepad.Id)))
|
||||
{
|
||||
_controllerNumber++;
|
||||
}
|
||||
|
||||
Devices.Add((DeviceType.Controller, id, $"{GetShortGamepadName(gamepad.Name)} ({_controllerNumber})"));
|
||||
string name = GetUniqueGamepadName(gamepad, ref controllerNumber);
|
||||
Devices.Add((DeviceType.Controller, id, name));
|
||||
}
|
||||
}
|
||||
|
||||
_controllerNumber = 0;
|
||||
|
||||
DeviceList.AddRange(Devices.Select(x => x.Name));
|
||||
Device = Math.Min(Device, DeviceList.Count);
|
||||
}
|
||||
@ -685,7 +697,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
var index = ProfilesList.IndexOf(ProfileName);
|
||||
int index = ProfilesList.IndexOf(ProfileName);
|
||||
if (index != -1)
|
||||
{
|
||||
ProfilesList.RemoveAt(index);
|
||||
|
@ -20,6 +20,7 @@ using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Cpu;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
@ -36,6 +37,7 @@ using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
@ -78,6 +80,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private bool _isAppletMenuActive;
|
||||
private int _statusBarProgressMaximum;
|
||||
private int _statusBarProgressValue;
|
||||
private string _statusBarProgressStatusText;
|
||||
private bool _statusBarProgressStatusVisible;
|
||||
private bool _isPaused;
|
||||
private bool _showContent = 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 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;
|
||||
@ -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
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@
|
||||
IsVisible="{Binding EnableNonGameRunningControls}">
|
||||
<Grid Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
@ -60,9 +61,16 @@
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding EnableNonGameRunningControls}"
|
||||
Text="{locale:Locale StatusBarGamesLoaded}" />
|
||||
<TextBlock
|
||||
Name="StatusBarProgressStatus"
|
||||
Grid.Column="2"
|
||||
Margin="10,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding StatusBarProgressStatusVisible}"
|
||||
Text="{Binding StatusBarProgressStatusText}" />
|
||||
<ProgressBar
|
||||
Name="LoadProgressBar"
|
||||
Grid.Column="2"
|
||||
Grid.Column="3"
|
||||
Height="6"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource SystemAccentColorLight2}"
|
||||
|
Loading…
x
Reference in New Issue
Block a user