From 28dd7d80af56701887dbb538b56aa58edaf39d91 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Wed, 18 Oct 2023 01:47:22 -0300 Subject: [PATCH 01/40] Enable copy between MS and non-MS textures with different height (#5801) --- src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs index eafa50b2e..3a0efcdda 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -374,6 +374,13 @@ namespace Ryujinx.Graphics.Gpu.Image return stride == rhs.Stride ? TextureViewCompatibility.CopyOnly : TextureViewCompatibility.LayoutIncompatible; } + else if (lhs.Target.IsMultisample() != rhs.Target.IsMultisample() && alignedWidthMatches && lhsAlignedSize.Height == rhsAlignedSize.Height) + { + // Copy between multisample and non-multisample textures with mismatching size is allowed, + // as long aligned size matches. + + return TextureViewCompatibility.CopyOnly; + } else { return TextureViewCompatibility.LayoutIncompatible; From 76b53e018a2e867899dbce2f3ce5173bbc4eed22 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Fri, 20 Oct 2023 14:05:09 +0100 Subject: [PATCH 02/40] GPU: Add fallback when textureGatherOffsets is not supported (#5792) * GPU: Add fallback when textureGatherOffsets is not supported. This PR adds a fallback for GPUs or APIs that don't support an equivalent to the method `textureGatherOffsets`, where each of the 4 gathered texels has an individual offset. This is done by reusing the existing code to handle non-const offsets for texture instructions, though it has also been corrected as there were a few implementation issues. MoltenVK reports support for this capability, and it didn't error when we initially released the MacOS build, but that has since changed. MVK still reports support, but spirv-cross has been fixed in a way that it _attempts_ to use this capability, but the metal compiler errors since it doesn't exist. Some other fixes: - textureGatherOffsets emulation has been changed significantly. It now uses 4 texture sample instructions (not gather), calculates a base texel (i=0 j=0) and adds the offsets onto it before converting into a tex coord. The final result is offset into a texel center, so it shouldn't be subject to interpolation, though this isn't perfect and could have some error with floating point formats with linear sampling. It is subject to texture wrap mode as it should be, which is why texelFetch was not used. - Maybe gather should be used here with component `w` (i=0, j=0), though this multiplies number of texels fetched by 4... The way it was doing this before _was_ wrong_, but doing it right would avoid issues with texel center precision. - textureGatherOffset (singular) now performs textureGather with the offset applied to the coords, rather than the slower fallback where each texel is fetched individually. * Increment shader cache version, remove unused arg * Use base texture size for gather coord offset. Implicit LOD for gather is not supported. * Use 4 texture gathers for offsets emulation Avoids issues with interpolation at cost of performance (not sure how bad this is) * Address Feedback --- src/Ryujinx.Graphics.GAL/Capabilities.cs | 3 + .../Shader/DiskCache/DiskCacheHostStorage.cs | 2 +- .../Shader/GpuAccessorBase.cs | 2 + src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs | 1 + src/Ryujinx.Graphics.Shader/IGpuAccessor.cs | 9 +++ .../Translation/Transforms/TexturePass.cs | 64 ++++++++++++++++--- src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 1 + 7 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs index 756422049..8959bf93e 100644 --- a/src/Ryujinx.Graphics.GAL/Capabilities.cs +++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs @@ -38,6 +38,7 @@ namespace Ryujinx.Graphics.GAL public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderFloat64; + public readonly bool SupportsTextureGatherOffsets; public readonly bool SupportsTextureShadowLod; public readonly bool SupportsVertexStoreAndAtomics; public readonly bool SupportsViewportIndexVertexTessellation; @@ -92,6 +93,7 @@ namespace Ryujinx.Graphics.GAL bool supportsShaderBallot, bool supportsShaderBarrierDivergence, bool supportsShaderFloat64, + bool supportsTextureGatherOffsets, bool supportsTextureShadowLod, bool supportsVertexStoreAndAtomics, bool supportsViewportIndexVertexTessellation, @@ -142,6 +144,7 @@ namespace Ryujinx.Graphics.GAL SupportsShaderBallot = supportsShaderBallot; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderFloat64 = supportsShaderFloat64; + SupportsTextureGatherOffsets = supportsTextureGatherOffsets; SupportsTextureShadowLod = supportsTextureShadowLod; SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics; SupportsViewportIndexVertexTessellation = supportsViewportIndexVertexTessellation; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index 0f1aa6a96..0dc4b1a72 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache private const ushort FileFormatVersionMajor = 1; private const ushort FileFormatVersionMinor = 2; private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor; - private const uint CodeGenVersion = 5767; + private const uint CodeGenVersion = 5791; private const string SharedTocFileName = "shared.toc"; private const string SharedDataFileName = "shared.data"; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs index 9d030cd60..a5b31363b 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs @@ -186,6 +186,8 @@ namespace Ryujinx.Graphics.Gpu.Shader public bool QueryHostSupportsSnormBufferTextureFormat() => _context.Capabilities.SupportsSnormBufferTextureFormat; + public bool QueryHostSupportsTextureGatherOffsets() => _context.Capabilities.SupportsTextureGatherOffsets; + public bool QueryHostSupportsTextureShadowLod() => _context.Capabilities.SupportsTextureShadowLod; public bool QueryHostSupportsTransformFeedback() => _context.Capabilities.SupportsTransformFeedback; diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs index 3eba15e34..667ea7825 100644 --- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs +++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs @@ -163,6 +163,7 @@ namespace Ryujinx.Graphics.OpenGL supportsShaderBallot: HwCapabilities.SupportsShaderBallot, supportsShaderBarrierDivergence: !(intelWindows || intelUnix), supportsShaderFloat64: true, + supportsTextureGatherOffsets: true, supportsTextureShadowLod: HwCapabilities.SupportsTextureShadowLod, supportsVertexStoreAndAtomics: true, supportsViewportIndexVertexTessellation: HwCapabilities.SupportsShaderViewportLayerArray, diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs index 4dc75a3e1..29a5435e3 100644 --- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs +++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs @@ -339,6 +339,15 @@ namespace Ryujinx.Graphics.Shader return true; } + /// + /// Queries host GPU texture gather with multiple offsets support. + /// + /// True if the GPU and driver supports texture gather offsets, false otherwise + bool QueryHostSupportsTextureGatherOffsets() + { + return true; + } + /// /// Queries host GPU texture shadow LOD support. /// diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs index dbfe6269e..495ea8a94 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs @@ -303,7 +303,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms bool hasOffset = (texOp.Flags & TextureFlags.Offset) != 0; bool hasOffsets = (texOp.Flags & TextureFlags.Offsets) != 0; - bool hasInvalidOffset = (hasOffset || hasOffsets) && !gpuAccessor.QueryHostSupportsNonConstantTextureOffset(); + bool needsOffsetsEmulation = hasOffsets && !gpuAccessor.QueryHostSupportsTextureGatherOffsets(); + + bool hasInvalidOffset = needsOffsetsEmulation || ((hasOffset || hasOffsets) && !gpuAccessor.QueryHostSupportsNonConstantTextureOffset()); bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0; @@ -402,11 +404,14 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms offsets[index] = offset; } - hasInvalidOffset &= !areAllOffsetsConstant; - - if (!hasInvalidOffset) + if (!needsOffsetsEmulation) { - return node; + hasInvalidOffset &= !areAllOffsetsConstant; + + if (!hasInvalidOffset) + { + return node; + } } if (hasLodBias) @@ -434,13 +439,13 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms LinkedListNode oldNode = node; - if (isGather && !isShadow) + if (isGather && !isShadow && hasOffsets) { Operand[] newSources = new Operand[sources.Length]; sources.CopyTo(newSources, 0); - Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage); + Operand[] texSizes = InsertTextureBaseSize(node, texOp, bindlessHandle, coordsCount); int destIndex = 0; @@ -455,7 +460,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms { Operand offset = Local(); - Operand intOffset = offsets[index + (hasOffsets ? compIndex * coordsCount : 0)]; + Operand intOffset = offsets[index + compIndex * coordsCount]; node.List.AddBefore(node, new Operation( Instruction.FP32 | Instruction.Divide, @@ -478,7 +483,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms texOp.Format, texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets), texOp.Binding, - 1, + 1 << 3, // W component: i=0, j=0 new[] { dests[destIndex++] }, newSources); @@ -502,7 +507,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms } else { - Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage); + Operand[] texSizes = isGather + ? InsertTextureBaseSize(node, texOp, bindlessHandle, coordsCount) + : InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount, stage); for (int index = 0; index < coordsCount; index++) { @@ -549,6 +556,43 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms return node; } + private static Operand[] InsertTextureBaseSize( + LinkedListNode node, + TextureOperation texOp, + Operand bindlessHandle, + int coordsCount) + { + Operand[] texSizes = new Operand[coordsCount]; + + for (int index = 0; index < coordsCount; index++) + { + texSizes[index] = Local(); + + Operand[] texSizeSources; + + if (bindlessHandle != null) + { + texSizeSources = new Operand[] { bindlessHandle, Const(0) }; + } + else + { + texSizeSources = new Operand[] { Const(0) }; + } + + node.List.AddBefore(node, new TextureOperation( + Instruction.TextureQuerySize, + texOp.Type, + texOp.Format, + texOp.Flags, + texOp.Binding, + index, + new[] { texSizes[index] }, + texSizeSources)); + } + + return texSizes; + } + private static Operand[] InsertTextureLod( LinkedListNode node, TextureOperation texOp, diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index a483dc599..ab8e61371 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -605,6 +605,7 @@ namespace Ryujinx.Graphics.Vulkan supportsShaderBallot: false, supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderFloat64: Capabilities.SupportsShaderFloat64, + supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended && !IsMoltenVk, supportsTextureShadowLod: false, supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics, supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex, From 6fdf7748455b2b71f99885239f8dc31390de2687 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Fri, 20 Oct 2023 09:41:50 -0400 Subject: [PATCH 03/40] Ava UI: Update to 11.0.5 (#5815) * Bump bump bump * Missed one --- Directory.Packages.props | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index cde8742f9..b34b882b4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,13 +3,13 @@ true - - - - - - - + + + + + + + From b4bb22ba06f89168c948e6001c51972575ca968b Mon Sep 17 00:00:00 2001 From: Ahmad Tantowi Date: Fri, 20 Oct 2023 21:02:12 +0700 Subject: [PATCH 04/40] Avalonia: Make slider scrollable with mouse wheel (#5760) * Add scrollable custom control based on TickFrequency * Use custom slider to update value when pointer wheel scrolled * Remove extra xaml file * Address formatting issues * Only scroll one element at a time * Add OnPointerWheelChanged event to VolumeStatus button Co-authored-by: Ahmad Tantowi --------- Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> --- .../UI/Controls/SliderScroll.axaml.cs | 31 +++++++++++++++++++ .../UI/Views/Input/ControllerInputView.axaml | 13 ++++---- .../UI/Views/Input/MotionInputView.axaml | 11 ++++--- .../UI/Views/Input/RumbleInputView.axaml | 7 +++-- .../UI/Views/Main/MainStatusBarView.axaml | 4 ++- .../UI/Views/Main/MainStatusBarView.axaml.cs | 15 +++++++++ .../UI/Views/Main/MainViewControls.axaml | 5 +-- .../UI/Views/Settings/SettingsAudioView.axaml | 9 +++--- .../Views/Settings/SettingsGraphicsView.axaml | 3 +- 9 files changed, 76 insertions(+), 22 deletions(-) create mode 100644 src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs diff --git a/src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs b/src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs new file mode 100644 index 000000000..81d3bc303 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Controls/SliderScroll.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using Avalonia.Input; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public class SliderScroll : Slider + { + protected override Type StyleKeyOverride => typeof(Slider); + + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + var newValue = Value + e.Delta.Y * TickFrequency; + + if (newValue < Minimum) + { + Value = Minimum; + } + else if (newValue > Maximum) + { + Value = Maximum; + } + else + { + Value = newValue; + } + + e.Handled = true; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml index 2ab42e6ee..d636873a3 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml @@ -5,6 +5,7 @@ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" @@ -460,7 +461,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal"> - - - - - - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml index a98f08825..a6b587f67 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" @@ -23,11 +24,11 @@ Margin="0" HorizontalAlignment="Center" Text="{locale:Locale ControllerSettingsMotionGyroSensitivity}" /> - - - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml index f633c0ed2..5b7087a47 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml @@ -1,6 +1,7 @@ - - - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml index 32524740b..01133a4bc 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" @@ -176,6 +177,7 @@ Content="{Binding VolumeStatusText}" IsChecked="{Binding VolumeMuted}" IsVisible="{Binding !ShowLoadProgress}" + PointerWheelChanged="VolumeStatus_OnPointerWheelChanged" Background="Transparent" BorderThickness="0" CornerRadius="0"> @@ -192,7 +194,7 @@ - 0, + > 1 => 1, + _ => newValue, + }; + + e.Handled = true; + } } } diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml index 34624b222..cc21b5c60 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" @@ -50,7 +51,7 @@ VerticalAlignment="Center" Text="{locale:Locale IconSize}" ToolTip.Tip="{locale:Locale IconSizeTooltip}" /> - - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml index 5dc0fef5d..657e07ee7 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml @@ -4,6 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" @@ -63,13 +64,13 @@ Maximum="100" /> - @@ -77,4 +78,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml index f6ba0a4c0..224494786 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml @@ -4,6 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" @@ -173,7 +174,7 @@ - Date: Sat, 21 Oct 2023 05:51:15 +1100 Subject: [PATCH 05/40] Add "Create Shortcut" To app context menu (#4734) * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * added back shortcut to new contextmenu file * Replaced COM reference with ComImport for shortcut functionality * remove specific platform values and regions * Move ShortcutHelper to Ryujinx.Ui.Common.Helpers * Adjust styling and structure * code feedback changes * Added MacOS support using .app folder * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * Replaced COM reference with ComImport for shortcut functionality * remove specific platform values and regions * Move ShortcutHelper to Ryujinx.Ui.Common.Helpers * Adjust styling and structure * code feedback changes * adjust tooltip message * added shortcut-template.desktop file * set shortcut icon location to .local/share/icons * Linux code feedback changes * change InteropServices to new securifybv.ShellLink Package * added ShellLink to readme, updated shortcut comment * Code feedback changes * Added MacOS Support (As per Jose Estrada's PR) * dotnet format * Small restructuring * Embed template files into Ryujinx.Ui.Common * Disable "CreateShortcut" option for flatpak builds --------- Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Co-authored-by: Jose Estrada --- Directory.Packages.props | 1 + README.md | 1 + distribution/legal/THIRDPARTY.md | 29 +++ distribution/linux/Ryujinx.desktop | 4 +- distribution/linux/shortcut-template.desktop | 13 ++ distribution/macos/shortcut-template.plist | 35 ++++ src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 + src/Ryujinx.Ava/Ryujinx.Ava.csproj | 2 +- .../UI/Controls/ApplicationContextMenu.axaml | 7 +- .../Controls/ApplicationContextMenu.axaml.cs | 11 ++ .../UI/ViewModels/MainWindowViewModel.cs | 5 +- .../App/ApplicationLibrary.cs | 4 +- .../Helper/ShortcutHelper.cs | 171 ++++++++++++++++++ .../Ryujinx.Ui.Common.csproj | 10 + src/Ryujinx/Ryujinx.csproj | 18 +- .../Widgets/GameTableContextMenu.Designer.cs | 11 ++ .../Ui/Widgets/GameTableContextMenu.cs | 9 + 17 files changed, 316 insertions(+), 17 deletions(-) create mode 100644 distribution/linux/shortcut-template.desktop create mode 100644 distribution/macos/shortcut-template.plist create mode 100644 src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index b34b882b4..6fdaafddc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -35,6 +35,7 @@ + diff --git a/README.md b/README.md index 7021abc45..56333278f 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,4 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY - [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system. - [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation. +- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation. diff --git a/distribution/legal/THIRDPARTY.md b/distribution/legal/THIRDPARTY.md index 4cc8b7a45..b0bd5a690 100644 --- a/distribution/legal/THIRDPARTY.md +++ b/distribution/legal/THIRDPARTY.md @@ -681,4 +681,33 @@ END OF TERMS AND CONDITIONS ``` + + +# ShellLink (MIT) +
+ See License + + ``` + MIT License + + Copyright (c) 2017 Yorick Koster, Securify B.V. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ```
\ No newline at end of file diff --git a/distribution/linux/Ryujinx.desktop b/distribution/linux/Ryujinx.desktop index 19cc5d6cc..a4550d104 100644 --- a/distribution/linux/Ryujinx.desktop +++ b/distribution/linux/Ryujinx.desktop @@ -3,8 +3,8 @@ Version=1.0 Name=Ryujinx Type=Application Icon=Ryujinx -Exec=env DOTNET_EnableAlternateStackCheck=1 Ryujinx %f -Comment=A Nintendo Switch Emulator +Exec=Ryujinx.sh %f +Comment=Plays Nintendo Switch applications GenericName=Nintendo Switch Emulator Terminal=false Categories=Game;Emulator; diff --git a/distribution/linux/shortcut-template.desktop b/distribution/linux/shortcut-template.desktop new file mode 100644 index 000000000..6bee0f8d1 --- /dev/null +++ b/distribution/linux/shortcut-template.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.0 +Name={0} +Type=Application +Icon={1} +Exec={2} %f +Comment=Nintendo Switch application +GenericName=Nintendo Switch Emulator +Terminal=false +Categories=Game;Emulator; +Keywords=Switch;Nintendo;Emulator; +StartupWMClass=Ryujinx +PrefersNonDefaultGPU=true diff --git a/distribution/macos/shortcut-template.plist b/distribution/macos/shortcut-template.plist new file mode 100644 index 000000000..27a9e46a9 --- /dev/null +++ b/distribution/macos/shortcut-template.plist @@ -0,0 +1,35 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + {0} + CFBundleGetInfoString + {1} + CFBundleIconFile + {2} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleVersion + 1.0 + NSHighResolutionCapable + + CSResourcesFileMapped + + NSHumanReadableCopyright + Copyright © 2018 - 2023 Ryujinx Team and Contributors. + LSApplicationCategoryType + public.app-category.games + LSMinimumSystemVersion + 11.0 + UIPrerenderedIcon + + LSEnvironment + + DOTNET_DefaultStackSize + 200000 + + + diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 53e277ba9..a67b796bd 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -72,6 +72,8 @@ "GameListContextMenuExtractDataRomFSToolTip": "Extract the RomFS section from Application's current config (including updates)", "GameListContextMenuExtractDataLogo": "Logo", "GameListContextMenuExtractDataLogoToolTip": "Extract the Logo section from Application's current config (including updates)", + "GameListContextMenuCreateShortcut": "Create Application Shortcut", + "GameListContextMenuCreateShortcutToolTip": "Create a Desktop Shortcut that launches the selected Application", "StatusBarGamesLoaded": "{0}/{1} Games Loaded", "StatusBarSystemVersion": "System Version: {0}", "LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected", diff --git a/src/Ryujinx.Ava/Ryujinx.Ava.csproj b/src/Ryujinx.Ava/Ryujinx.Ava.csproj index a4c1ebf16..f0e99f427 100644 --- a/src/Ryujinx.Ava/Ryujinx.Ava.csproj +++ b/src/Ryujinx.Ava/Ryujinx.Ava.csproj @@ -145,4 +145,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml index 93638fc53..d81050f83 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml @@ -82,4 +82,9 @@ Header="{locale:Locale GameListContextMenuExtractDataLogo}" ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" /> - \ No newline at end of file + + diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs index d75572e65..0f0071065 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs @@ -337,6 +337,17 @@ namespace Ryujinx.Ava.UI.Controls } } + public void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args) + { + var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; + + if (viewModel?.SelectedApplication != null) + { + ApplicationData selectedApplication = viewModel.SelectedApplication; + ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon); + } + } + public async void RunApplication_Click(object sender, RoutedEventArgs args) { var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel; diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 7a9e4df14..b14905204 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -356,6 +356,8 @@ namespace Ryujinx.Ava.UI.ViewModels public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; + public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild(); + public string LoadHeading { get => _loadHeading; @@ -1488,7 +1490,7 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path); + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language); PrepareLoadScreen(); @@ -1696,7 +1698,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } } - #endregion } } diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 33e6c4aad..36b2b727d 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -546,7 +546,7 @@ namespace Ryujinx.Ui.App.Common return appMetadata; } - public byte[] GetApplicationIcon(string applicationPath) + public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage) { byte[] applicationIcon = null; @@ -600,7 +600,7 @@ namespace Ryujinx.Ui.App.Common { using var icon = new UniqueRef(); - controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + controlFs.OpenFile(ref icon.Ref, $"/icon_{desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); using MemoryStream stream = new(); diff --git a/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs new file mode 100644 index 000000000..dab473fa3 --- /dev/null +++ b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs @@ -0,0 +1,171 @@ +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using ShellLink; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.Versioning; +using Image = System.Drawing.Image; + +namespace Ryujinx.Ui.Common.Helper +{ + public static class ShortcutHelper + { + [SupportedOSPlatform("windows")] + private static void CreateShortcutWindows(string applicationFilePath, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath) + { + string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe"); + iconPath += ".ico"; + + MemoryStream iconDataStream = new(iconData); + using Image image = Image.FromStream(iconDataStream); + using Bitmap bitmap = new(128, 128); + using System.Drawing.Graphics graphic = System.Drawing.Graphics.FromImage(bitmap); + graphic.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphic.DrawImage(image, 0, 0, 128, 128); + SaveBitmapAsIcon(bitmap, iconPath); + + var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(basePath, applicationFilePath), iconPath, 0); + shortcut.StringData.NameString = cleanedAppName; + shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk")); + } + + [SupportedOSPlatform("linux")] + private static void CreateShortcutLinux(string applicationFilePath, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName) + { + string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh"); + var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.desktop"); + iconPath += ".png"; + + var image = SixLabors.ImageSharp.Image.Load(iconData); + image.SaveAsPng(iconPath); + + using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop")); + outputFile.Write(desktopFile, cleanedAppName, iconPath, GetArgsString(basePath, applicationFilePath)); + } + + [SupportedOSPlatform("macos")] + private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName) + { + string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName); + var plistFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.plist"); + // Macos .App folder + string contentFolderPath = Path.Combine(desktopPath, cleanedAppName + ".app", "Contents"); + string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS"); + + if (!Directory.Exists(scriptFolderPath)) + { + Directory.CreateDirectory(scriptFolderPath); + } + + // Runner script + const string ScriptName = "runner.sh"; + string scriptPath = Path.Combine(scriptFolderPath, ScriptName); + using StreamWriter scriptFile = new(scriptPath); + + scriptFile.WriteLine("#!/bin/sh"); + scriptFile.WriteLine(GetArgsString(basePath, appFilePath)); + + // Set execute permission + FileInfo fileInfo = new(scriptPath); + fileInfo.UnixFileMode |= UnixFileMode.UserExecute; + + // img + string resourceFolderPath = Path.Combine(contentFolderPath, "Resources"); + if (!Directory.Exists(resourceFolderPath)) + { + Directory.CreateDirectory(resourceFolderPath); + } + + const string IconName = "icon.png"; + var image = SixLabors.ImageSharp.Image.Load(iconData); + image.SaveAsPng(Path.Combine(resourceFolderPath, IconName)); + + // plist file + using StreamWriter outputFile = new(Path.Combine(contentFolderPath, "Info.plist")); + outputFile.Write(plistFile, ScriptName, cleanedAppName, IconName); + } + + public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData) + { + string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); + string cleanedAppName = string.Join("_", applicationName.Split(Path.GetInvalidFileNameChars())); + + if (OperatingSystem.IsWindows()) + { + string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app"); + + CreateShortcutWindows(applicationFilePath, iconData, iconPath, cleanedAppName, desktopPath); + + return; + } + + if (OperatingSystem.IsLinux()) + { + string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx"); + + Directory.CreateDirectory(iconPath); + CreateShortcutLinux(applicationFilePath, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName); + + return; + } + + if (OperatingSystem.IsMacOS()) + { + CreateShortcutMacos(applicationFilePath, iconData, desktopPath, cleanedAppName); + + return; + } + + throw new NotImplementedException("Shortcut support has not been implemented yet for this OS."); + } + + private static string GetArgsString(string basePath, string appFilePath) + { + // args are first defined as a list, for easier adjustments in the future + var argsList = new List + { + basePath, + }; + + if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg)) + { + argsList.Add("--root-data-dir"); + argsList.Add($"\"{CommandLineState.BaseDirPathArg}\""); + } + + argsList.Add($"\"{appFilePath}\""); + + + return String.Join(" ", argsList); + } + + /// + /// Creates a Icon (.ico) file using the source bitmap image at the specified file path. + /// + /// The source bitmap image that will be saved as an .ico file + /// The location that the new .ico file will be saved too (Make sure to include '.ico' in the path). + [SupportedOSPlatform("windows")] + private static void SaveBitmapAsIcon(Bitmap source, string filePath) + { + // Code Modified From https://stackoverflow.com/a/11448060/368354 by Benlitz + byte[] header = { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 0, 0, 0, 0, 22, 0, 0, 0 }; + using FileStream fs = new(filePath, FileMode.Create); + + fs.Write(header); + // Writing actual data + source.Save(fs, ImageFormat.Png); + // Getting data length (file length minus header) + long dataLength = fs.Length - header.Length; + // Write it in the correct place + fs.Seek(14, SeekOrigin.Begin); + fs.WriteByte((byte)dataLength); + fs.WriteByte((byte)(dataLength >> 8)); + } + } +} diff --git a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj index 511a03897..3da47431f 100644 --- a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj +++ b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj @@ -45,8 +45,18 @@
+ + + + + + + + + + diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index cf4435e57..5b5ed4637 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -63,15 +63,15 @@ - - + + Always - - - Always - mime\Ryujinx.xml - - + + + Always + mime\Ryujinx.xml + + @@ -101,4 +101,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs index 0f7b4f22b..75b166136 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs @@ -23,6 +23,7 @@ namespace Ryujinx.Ui.Widgets private MenuItem _purgeShaderCacheMenuItem; private MenuItem _openPtcDirMenuItem; private MenuItem _openShaderCacheDirMenuItem; + private MenuItem _createShortcutMenuItem; private void InitializeComponent() { @@ -187,6 +188,15 @@ namespace Ryujinx.Ui.Widgets }; _openShaderCacheDirMenuItem.Activated += OpenShaderCacheDir_Clicked; + // + // _createShortcutMenuItem + // + _createShortcutMenuItem = new MenuItem("Create Application Shortcut") + { + TooltipText = "Create a Desktop Shortcut that launches the selected Application." + }; + _createShortcutMenuItem.Activated += CreateShortcut_Clicked; + ShowComponent(); } @@ -213,6 +223,7 @@ namespace Ryujinx.Ui.Widgets Add(new SeparatorMenuItem()); Add(_manageCacheMenuItem); Add(_extractMenuItem); + Add(_createShortcutMenuItem); ShowAll(); } diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index c2e0d8ebc..ea60421f8 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -10,6 +10,7 @@ using LibHac.Ns; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; @@ -77,6 +78,8 @@ namespace Ryujinx.Ui.Widgets _extractExeFsMenuItem.Sensitive = hasNca; _extractLogoMenuItem.Sensitive = hasNca; + _createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild(); + PopupAtPointer(null); } @@ -629,5 +632,11 @@ namespace Ryujinx.Ui.Widgets } } } + + private void CreateShortcut_Clicked(object sender, EventArgs args) + { + byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language); + ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon); + } } } From 49b37550cae6b3c69f59a9c7a44b17e3c12a813b Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Sat, 21 Oct 2023 07:26:51 -0400 Subject: [PATCH 06/40] Ava UI: Input Menu Refactor (#4998) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * So much boilerplate * Slow and steady * Restructure + Ack suggestions * Restructure + Ack suggestions * Restructure * Clean * Propogate those fields i forgot about * It builds * Progress * Almost there * Fix stupid mistake * Fix more stupid mistakes * Actually fix fuck ups * Start localising * r/therestofthefuckingowl * Localise ButtonKeyAssigner * Are you feeling it now mr krabs * We’re done at last * Crimes against code * Try me in the Hague * Please be quiet * Crimes are here to stay * Dispose stuff * Cleanup a couple things * Visual fixes and improvements One weird bug * Fix rebase errors * Fixes * Ack Suggestions Remaining ack suggestions Update src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs Co-authored-by: Ac_K Update src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs Co-authored-by: Ac_K * Formatting and error More Ava 11-ness Whoops * Code style fixes * Style fixes * Analyzer fix * Remove all ReflectionBindings * Remove ambigious object * Remove redundant property * Old man yells at formatter * r e a d o n l y * Fix profiles * Use new Sliders --------- Co-authored-by: Ac_K --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 99 +++ src/Ryujinx.Ava/Assets/Styles/Styles.xaml | 5 +- .../UI/Helpers/ButtonKeyAssigner.cs | 28 +- .../UI/Helpers/KeyValueConverter.cs | 167 ++++- .../UI/Models/Input/ControllerInputConfig.cs | 580 +++++++++++++++ .../UI/Models/Input/KeyboardInputConfig.cs | 422 +++++++++++ .../UI/Models/InputConfiguration.cs | 456 ------------ .../Input/ControllerInputViewModel.cs | 84 +++ .../InputViewModel.cs} | 103 ++- .../Input/KeyboardInputViewModel.cs | 73 ++ .../{ => Input}/MotionInputViewModel.cs | 2 +- .../{ => Input}/RumbleInputViewModel.cs | 2 +- .../UI/Views/Input/ControllerInputView.axaml | 616 ++++------------ .../Views/Input/ControllerInputView.axaml.cs | 160 +++-- .../UI/Views/Input/InputView.axaml | 225 ++++++ .../UI/Views/Input/InputView.axaml.cs | 61 ++ .../UI/Views/Input/KeyboardInputView.axaml | 675 ++++++++++++++++++ .../UI/Views/Input/KeyboardInputView.axaml.cs | 210 ++++++ .../UI/Views/Input/MotionInputView.axaml | 2 +- .../UI/Views/Input/MotionInputView.axaml.cs | 8 +- .../UI/Views/Input/RumbleInputView.axaml | 2 +- .../UI/Views/Input/RumbleInputView.axaml.cs | 8 +- .../UI/Views/Settings/SettingsInputView.axaml | 4 +- .../Views/Settings/SettingsInputView.axaml.cs | 2 +- .../UI/Windows/SettingsWindow.axaml.cs | 2 +- .../Assigner/GamepadButtonAssigner.cs | 6 +- src/Ryujinx.Input/Assigner/IButtonAssigner.cs | 2 +- .../Assigner/KeyboardKeyAssigner.cs | 10 +- src/Ryujinx.Input/ButtonValue.cs | 48 ++ src/Ryujinx/Ui/Windows/ControllerWindow.cs | 2 +- 30 files changed, 2914 insertions(+), 1150 deletions(-) create mode 100644 src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs create mode 100644 src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs delete mode 100644 src/Ryujinx.Ava/UI/Models/InputConfiguration.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs rename src/Ryujinx.Ava/UI/ViewModels/{ControllerInputViewModel.cs => Input/InputViewModel.cs} (92%) create mode 100644 src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs rename src/Ryujinx.Ava/UI/ViewModels/{ => Input}/MotionInputViewModel.cs (97%) rename src/Ryujinx.Ava/UI/ViewModels/{ => Input}/RumbleInputViewModel.cs (92%) create mode 100644 src/Ryujinx.Ava/UI/Views/Input/InputView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs create mode 100644 src/Ryujinx.Input/ButtonValue.cs diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index a67b796bd..fc65fe4a0 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -263,6 +263,105 @@ "ControllerSettingsMotionGyroDeadzone": "Gyro Deadzone:", "ControllerSettingsSave": "Save", "ControllerSettingsClose": "Close", + "KeyUnknown": "Unknown", + "KeyShiftLeft": "Shift Left", + "KeyShiftRight": "Shift Right", + "KeyControlLeft": "Control Left", + "KeyControlRight": "Control Right", + "KeyAltLeft": "Alt Left", + "KeyAltRight": "Alt Right", + "KeyOptLeft": "⌥ Left", + "KeyOptRight": "⌥ Right", + "KeyWinLeft": "⊞ Left", + "KeyWinRight": "⊞ Right", + "KeyCmdLeft": "⌘ Left", + "KeyCmdRight": "⌘ Right", + "KeyMenu": "Menu", + "KeyUp": "Up", + "KeyDown": "Down", + "KeyLeft": "Left", + "KeyRight": "Right", + "KeyEnter": "Enter", + "KeyEscape": "Escape", + "KeySpace": "Space", + "KeyTab": "Tab", + "KeyBackSpace": "Backspace", + "KeyInsert": "Insert", + "KeyDelete": "Delete", + "KeyPageUp": "Page Up", + "KeyPageDown": "Page Down", + "KeyHome": "Home", + "KeyEnd": "End", + "KeyCapsLock": "Caps Lock", + "KeyScrollLock": "Scroll Lock", + "KeyPrintScreen": "Print Screen", + "KeyPause": "Pause", + "KeyNumLock": "Num Lock", + "KeyClear": "Clear", + "KeyKeypad0": "Keypad 0", + "KeyKeypad1": "Keypad 1", + "KeyKeypad2": "Keypad 2", + "KeyKeypad3": "Keypad 3", + "KeyKeypad4": "Keypad 4", + "KeyKeypad5": "Keypad 5", + "KeyKeypad6": "Keypad 6", + "KeyKeypad7": "Keypad 7", + "KeyKeypad8": "Keypad 8", + "KeyKeypad9": "Keypad 9", + "KeyKeypadDivide": "Keypad Divide", + "KeyKeypadMultiply": "Keypad Multiply", + "KeyKeypadSubtract": "Keypad Subtract", + "KeyKeypadAdd": "Keypad Add", + "KeyKeypadDecimal": "Keypad Decimal", + "KeyKeypadEnter": "Keypad Enter", + "KeyNumber0": "0", + "KeyNumber1": "1", + "KeyNumber2": "2", + "KeyNumber3": "3", + "KeyNumber4": "4", + "KeyNumber5": "5", + "KeyNumber6": "6", + "KeyNumber7": "7", + "KeyNumber8": "8", + "KeyNumber9": "9", + "KeyTilde": "~", + "KeyGrave": "`", + "KeyMinus": "-", + "KeyPlus": "+", + "KeyBracketLeft": "[", + "KeyBracketRight": "]", + "KeySemicolon": ";", + "KeyQuote": "\"", + "KeyComma": ",", + "KeyPeriod": ".", + "KeySlash": "/", + "KeyBackSlash": "\\", + "KeyUnbound": "Unbound", + "GamepadLeftStick": "Left Stick Button", + "GamepadRightStick": "Right Stick Button", + "GamepadLeftShoulder": "Left Shoulder", + "GamepadRightShoulder": "Right Shoulder", + "GamepadLeftTrigger": "Left Trigger", + "GamepadRightTrigger": "Right Trigger", + "GamepadDpadUp": "Up", + "GamepadDpadDown": "Down", + "GamepadDpadLeft": "Left", + "GamepadDpadRight": "Right", + "GamepadMinus": "-", + "GamepadPlus": "+", + "GamepadGuide": "Guide", + "GamepadMisc1": "Misc", + "GamepadPaddle1": "Paddle 1", + "GamepadPaddle2": "Paddle 2", + "GamepadPaddle3": "Paddle 3", + "GamepadPaddle4": "Paddle 4", + "GamepadTouchpad": "Touchpad", + "GamepadSingleLeftTrigger0": "Left Trigger 0", + "GamepadSingleRightTrigger0": "Right Trigger 0", + "GamepadSingleLeftTrigger1": "Left Trigger 1", + "GamepadSingleRightTrigger1": "Right Trigger 1", + "StickLeft": "Left Stick", + "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "Selected User Profile:", "UserProfilesSaveProfileName": "Save Profile Name", "UserProfilesChangeProfileImage": "Change Profile Image", diff --git a/src/Ryujinx.Ava/Assets/Styles/Styles.xaml b/src/Ryujinx.Ava/Assets/Styles/Styles.xaml index f7f64be22..b3a6f59c8 100644 --- a/src/Ryujinx.Ava/Assets/Styles/Styles.xaml +++ b/src/Ryujinx.Ava/Assets/Styles/Styles.xaml @@ -15,8 +15,7 @@ - + @@ -393,4 +392,4 @@ 600 756 - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs index 7e8ba7342..54e0918a5 100644 --- a/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs +++ b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs @@ -1,11 +1,8 @@ -using Avalonia.Controls; using Avalonia.Controls.Primitives; -using Avalonia.LogicalTree; using Avalonia.Threading; using Ryujinx.Input; using Ryujinx.Input.Assigner; using System; -using System.Linq; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Helpers @@ -15,12 +12,12 @@ namespace Ryujinx.Ava.UI.Helpers internal class ButtonAssignedEventArgs : EventArgs { public ToggleButton Button { get; } - public bool IsAssigned { get; } + public ButtonValue? ButtonValue { get; } - public ButtonAssignedEventArgs(ToggleButton button, bool isAssigned) + public ButtonAssignedEventArgs(ToggleButton button, ButtonValue? buttonValue) { Button = button; - IsAssigned = isAssigned; + ButtonValue = buttonValue; } } @@ -78,15 +75,11 @@ namespace Ryujinx.Ava.UI.Helpers await Dispatcher.UIThread.InvokeAsync(() => { - string pressedButton = assigner.GetPressedButton(); + ButtonValue? pressedButton = assigner.GetPressedButton(); if (_shouldUnbind) { - SetButtonText(ToggledButton, "Unbound"); - } - else if (pressedButton != "") - { - SetButtonText(ToggledButton, pressedButton); + pressedButton = null; } _shouldUnbind = false; @@ -94,17 +87,8 @@ namespace Ryujinx.Ava.UI.Helpers ToggledButton.IsChecked = false; - ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton != null)); + ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton)); - static void SetButtonText(ToggleButton button, string text) - { - ILogical textBlock = button.GetLogicalDescendants().First(x => x is TextBlock); - - if (textBlock != null && textBlock is TextBlock block) - { - block.Text = text; - } - } }); } diff --git a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs index 028ed6bf4..1c4aa7b21 100644 --- a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs +++ b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs @@ -1,7 +1,9 @@ using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using System; +using System.Collections.Generic; using System.Globalization; namespace Ryujinx.Ava.UI.Helpers @@ -10,37 +12,158 @@ namespace Ryujinx.Ava.UI.Helpers { public static KeyValueConverter Instance = new(); + private static readonly Dictionary _keysMap = new() + { + { Key.Unknown, LocaleKeys.KeyUnknown }, + { Key.ShiftLeft, LocaleKeys.KeyShiftLeft }, + { Key.ShiftRight, LocaleKeys.KeyShiftRight }, + { Key.ControlLeft, LocaleKeys.KeyControlLeft }, + { Key.ControlRight, LocaleKeys.KeyControlRight }, + { Key.AltLeft, OperatingSystem.IsMacOS() ? LocaleKeys.KeyOptLeft : LocaleKeys.KeyAltLeft }, + { Key.AltRight, OperatingSystem.IsMacOS() ? LocaleKeys.KeyOptRight : LocaleKeys.KeyAltRight }, + { Key.WinLeft, OperatingSystem.IsMacOS() ? LocaleKeys.KeyCmdLeft : LocaleKeys.KeyWinLeft }, + { Key.WinRight, OperatingSystem.IsMacOS() ? LocaleKeys.KeyCmdRight : LocaleKeys.KeyWinRight }, + { Key.Up, LocaleKeys.KeyUp }, + { Key.Down, LocaleKeys.KeyDown }, + { Key.Left, LocaleKeys.KeyLeft }, + { Key.Right, LocaleKeys.KeyRight }, + { Key.Enter, LocaleKeys.KeyEnter }, + { Key.Escape, LocaleKeys.KeyEscape }, + { Key.Space, LocaleKeys.KeySpace }, + { Key.Tab, LocaleKeys.KeyTab }, + { Key.BackSpace, LocaleKeys.KeyBackSpace }, + { Key.Insert, LocaleKeys.KeyInsert }, + { Key.Delete, LocaleKeys.KeyDelete }, + { Key.PageUp, LocaleKeys.KeyPageUp }, + { Key.PageDown, LocaleKeys.KeyPageDown }, + { Key.Home, LocaleKeys.KeyHome }, + { Key.End, LocaleKeys.KeyEnd }, + { Key.CapsLock, LocaleKeys.KeyCapsLock }, + { Key.ScrollLock, LocaleKeys.KeyScrollLock }, + { Key.PrintScreen, LocaleKeys.KeyPrintScreen }, + { Key.Pause, LocaleKeys.KeyPause }, + { Key.NumLock, LocaleKeys.KeyNumLock }, + { Key.Clear, LocaleKeys.KeyClear }, + { Key.Keypad0, LocaleKeys.KeyKeypad0 }, + { Key.Keypad1, LocaleKeys.KeyKeypad1 }, + { Key.Keypad2, LocaleKeys.KeyKeypad2 }, + { Key.Keypad3, LocaleKeys.KeyKeypad3 }, + { Key.Keypad4, LocaleKeys.KeyKeypad4 }, + { Key.Keypad5, LocaleKeys.KeyKeypad5 }, + { Key.Keypad6, LocaleKeys.KeyKeypad6 }, + { Key.Keypad7, LocaleKeys.KeyKeypad7 }, + { Key.Keypad8, LocaleKeys.KeyKeypad8 }, + { Key.Keypad9, LocaleKeys.KeyKeypad9 }, + { Key.KeypadDivide, LocaleKeys.KeyKeypadDivide }, + { Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply }, + { Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract }, + { Key.KeypadAdd, LocaleKeys.KeyKeypadAdd }, + { Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal }, + { Key.KeypadEnter, LocaleKeys.KeyKeypadEnter }, + { Key.Number0, LocaleKeys.KeyNumber0 }, + { Key.Number1, LocaleKeys.KeyNumber1 }, + { Key.Number2, LocaleKeys.KeyNumber2 }, + { Key.Number3, LocaleKeys.KeyNumber3 }, + { Key.Number4, LocaleKeys.KeyNumber4 }, + { Key.Number5, LocaleKeys.KeyNumber5 }, + { Key.Number6, LocaleKeys.KeyNumber6 }, + { Key.Number7, LocaleKeys.KeyNumber7 }, + { Key.Number8, LocaleKeys.KeyNumber8 }, + { Key.Number9, LocaleKeys.KeyNumber9 }, + { Key.Tilde, LocaleKeys.KeyTilde }, + { Key.Grave, LocaleKeys.KeyGrave }, + { Key.Minus, LocaleKeys.KeyMinus }, + { Key.Plus, LocaleKeys.KeyPlus }, + { Key.BracketLeft, LocaleKeys.KeyBracketLeft }, + { Key.BracketRight, LocaleKeys.KeyBracketRight }, + { Key.Semicolon, LocaleKeys.KeySemicolon }, + { Key.Quote, LocaleKeys.KeyQuote }, + { Key.Comma, LocaleKeys.KeyComma }, + { Key.Period, LocaleKeys.KeyPeriod }, + { Key.Slash, LocaleKeys.KeySlash }, + { Key.BackSlash, LocaleKeys.KeyBackSlash }, + { Key.Unbound, LocaleKeys.KeyUnbound }, + }; + + private static readonly Dictionary _gamepadInputIdMap = new() + { + { GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick }, + { GamepadInputId.RightStick, LocaleKeys.GamepadRightStick }, + { GamepadInputId.LeftShoulder, LocaleKeys.GamepadLeftShoulder }, + { GamepadInputId.RightShoulder, LocaleKeys.GamepadRightShoulder }, + { GamepadInputId.LeftTrigger, LocaleKeys.GamepadLeftTrigger }, + { GamepadInputId.RightTrigger, LocaleKeys.GamepadRightTrigger }, + { GamepadInputId.DpadUp, LocaleKeys.GamepadDpadUp}, + { GamepadInputId.DpadDown, LocaleKeys.GamepadDpadDown}, + { GamepadInputId.DpadLeft, LocaleKeys.GamepadDpadLeft}, + { GamepadInputId.DpadRight, LocaleKeys.GamepadDpadRight}, + { GamepadInputId.Minus, LocaleKeys.GamepadMinus}, + { GamepadInputId.Plus, LocaleKeys.GamepadPlus}, + { GamepadInputId.Guide, LocaleKeys.GamepadGuide}, + { GamepadInputId.Misc1, LocaleKeys.GamepadMisc1}, + { GamepadInputId.Paddle1, LocaleKeys.GamepadPaddle1}, + { GamepadInputId.Paddle2, LocaleKeys.GamepadPaddle2}, + { GamepadInputId.Paddle3, LocaleKeys.GamepadPaddle3}, + { GamepadInputId.Paddle4, LocaleKeys.GamepadPaddle4}, + { GamepadInputId.Touchpad, LocaleKeys.GamepadTouchpad}, + { GamepadInputId.SingleLeftTrigger0, LocaleKeys.GamepadSingleLeftTrigger0}, + { GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0}, + { GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1}, + { GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1}, + { GamepadInputId.Unbound, LocaleKeys.KeyUnbound}, + }; + + private static readonly Dictionary _stickInputIdMap = new() + { + { StickInputId.Left, LocaleKeys.StickLeft}, + { StickInputId.Right, LocaleKeys.StickRight}, + { StickInputId.Unbound, LocaleKeys.KeyUnbound}, + }; + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value == null) + string keyString = ""; + + if (value is Key key) { - return null; + if (_keysMap.TryGetValue(key, out LocaleKeys localeKey)) + { + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = key.ToString(); + } + } + else if (value is GamepadInputId gamepadInputId) + { + if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out LocaleKeys localeKey)) + { + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = gamepadInputId.ToString(); + } + } + else if (value is StickInputId stickInputId) + { + if (_stickInputIdMap.TryGetValue(stickInputId, out LocaleKeys localeKey)) + { + keyString = LocaleManager.Instance[localeKey]; + } + else + { + keyString = stickInputId.ToString(); + } } - return value.ToString(); + return keyString; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - object key = null; - - if (value != null) - { - if (targetType == typeof(Key)) - { - key = Enum.Parse(value.ToString()); - } - else if (targetType == typeof(GamepadInputId)) - { - key = Enum.Parse(value.ToString()); - } - else if (targetType == typeof(StickInputId)) - { - key = Enum.Parse(value.ToString()); - } - } - - return key; + throw new NotSupportedException(); } } } diff --git a/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs b/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs new file mode 100644 index 000000000..4929e582e --- /dev/null +++ b/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs @@ -0,0 +1,580 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using System; + +namespace Ryujinx.Ava.UI.Models.Input +{ + public class ControllerInputConfig : BaseModel + { + public bool EnableCemuHookMotion { get; set; } + public string DsuServerHost { get; set; } + public int DsuServerPort { get; set; } + public int Slot { get; set; } + public int AltSlot { get; set; } + public bool MirrorInput { get; set; } + public int Sensitivity { get; set; } + public double GyroDeadzone { get; set; } + + public float WeakRumble { get; set; } + public float StrongRumble { get; set; } + + public string Id { get; set; } + public ControllerType ControllerType { get; set; } + public PlayerIndex PlayerIndex { get; set; } + + private StickInputId _leftJoystick; + public StickInputId LeftJoystick + { + get => _leftJoystick; + set + { + _leftJoystick = value; + OnPropertyChanged(); + } + } + + private bool _leftInvertStickX; + public bool LeftInvertStickX + { + get => _leftInvertStickX; + set + { + _leftInvertStickX = value; + OnPropertyChanged(); + } + } + + private bool _leftInvertStickY; + public bool LeftInvertStickY + { + get => _leftInvertStickY; + set + { + _leftInvertStickY = value; + OnPropertyChanged(); + } + } + + private bool _leftRotate90; + public bool LeftRotate90 + { + get => _leftRotate90; + set + { + _leftRotate90 = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _leftStickButton; + public GamepadInputId LeftStickButton + { + get => _leftStickButton; + set + { + _leftStickButton = value; + OnPropertyChanged(); + } + } + + private StickInputId _rightJoystick; + public StickInputId RightJoystick + { + get => _rightJoystick; + set + { + _rightJoystick = value; + OnPropertyChanged(); + } + } + + private bool _rightInvertStickX; + public bool RightInvertStickX + { + get => _rightInvertStickX; + set + { + _rightInvertStickX = value; + OnPropertyChanged(); + } + } + + private bool _rightInvertStickY; + public bool RightInvertStickY + { + get => _rightInvertStickY; + set + { + _rightInvertStickY = value; + OnPropertyChanged(); + } + } + + private bool _rightRotate90; + public bool RightRotate90 + { + get => _rightRotate90; + set + { + _rightRotate90 = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _rightStickButton; + public GamepadInputId RightStickButton + { + get => _rightStickButton; + set + { + _rightStickButton = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadUp; + public GamepadInputId DpadUp + { + get => _dpadUp; + set + { + _dpadUp = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadDown; + public GamepadInputId DpadDown + { + get => _dpadDown; + set + { + _dpadDown = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadLeft; + public GamepadInputId DpadLeft + { + get => _dpadLeft; + set + { + _dpadLeft = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _dpadRight; + public GamepadInputId DpadRight + { + get => _dpadRight; + set + { + _dpadRight = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonL; + public GamepadInputId ButtonL + { + get => _buttonL; + set + { + _buttonL = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonMinus; + public GamepadInputId ButtonMinus + { + get => _buttonMinus; + set + { + _buttonMinus = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _leftButtonSl; + public GamepadInputId LeftButtonSl + { + get => _leftButtonSl; + set + { + _leftButtonSl = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _leftButtonSr; + public GamepadInputId LeftButtonSr + { + get => _leftButtonSr; + set + { + _leftButtonSr = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonZl; + public GamepadInputId ButtonZl + { + get => _buttonZl; + set + { + _buttonZl = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonA; + public GamepadInputId ButtonA + { + get => _buttonA; + set + { + _buttonA = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonB; + public GamepadInputId ButtonB + { + get => _buttonB; + set + { + _buttonB = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonX; + public GamepadInputId ButtonX + { + get => _buttonX; + set + { + _buttonX = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonY; + public GamepadInputId ButtonY + { + get => _buttonY; + set + { + _buttonY = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonR; + public GamepadInputId ButtonR + { + get => _buttonR; + set + { + _buttonR = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonPlus; + public GamepadInputId ButtonPlus + { + get => _buttonPlus; + set + { + _buttonPlus = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _rightButtonSl; + public GamepadInputId RightButtonSl + { + get => _rightButtonSl; + set + { + _rightButtonSl = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _rightButtonSr; + public GamepadInputId RightButtonSr + { + get => _rightButtonSr; + set + { + _rightButtonSr = value; + OnPropertyChanged(); + } + } + + private GamepadInputId _buttonZr; + public GamepadInputId ButtonZr + { + get => _buttonZr; + set + { + _buttonZr = value; + OnPropertyChanged(); + } + } + + private float _deadzoneLeft; + public float DeadzoneLeft + { + get => _deadzoneLeft; + set + { + _deadzoneLeft = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _deadzoneRight; + public float DeadzoneRight + { + get => _deadzoneRight; + set + { + _deadzoneRight = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _rangeLeft; + public float RangeLeft + { + get => _rangeLeft; + set + { + _rangeLeft = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _rangeRight; + public float RangeRight + { + get => _rangeRight; + set + { + _rangeRight = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private float _triggerThreshold; + public float TriggerThreshold + { + get => _triggerThreshold; + set + { + _triggerThreshold = MathF.Round(value, 3); + OnPropertyChanged(); + } + } + + private bool _enableMotion; + public bool EnableMotion + { + get => _enableMotion; + set + { + _enableMotion = value; + OnPropertyChanged(); + } + } + + private bool _enableRumble; + public bool EnableRumble + { + get => _enableRumble; + set + { + _enableRumble = value; + OnPropertyChanged(); + } + } + + public ControllerInputConfig(InputConfig config) + { + if (config != null) + { + Id = config.Id; + ControllerType = config.ControllerType; + PlayerIndex = config.PlayerIndex; + + if (config is not StandardControllerInputConfig controllerInput) + { + return; + } + + LeftJoystick = controllerInput.LeftJoyconStick.Joystick; + LeftInvertStickX = controllerInput.LeftJoyconStick.InvertStickX; + LeftInvertStickY = controllerInput.LeftJoyconStick.InvertStickY; + LeftRotate90 = controllerInput.LeftJoyconStick.Rotate90CW; + LeftStickButton = controllerInput.LeftJoyconStick.StickButton; + + RightJoystick = controllerInput.RightJoyconStick.Joystick; + RightInvertStickX = controllerInput.RightJoyconStick.InvertStickX; + RightInvertStickY = controllerInput.RightJoyconStick.InvertStickY; + RightRotate90 = controllerInput.RightJoyconStick.Rotate90CW; + RightStickButton = controllerInput.RightJoyconStick.StickButton; + + DpadUp = controllerInput.LeftJoycon.DpadUp; + DpadDown = controllerInput.LeftJoycon.DpadDown; + DpadLeft = controllerInput.LeftJoycon.DpadLeft; + DpadRight = controllerInput.LeftJoycon.DpadRight; + ButtonL = controllerInput.LeftJoycon.ButtonL; + ButtonMinus = controllerInput.LeftJoycon.ButtonMinus; + LeftButtonSl = controllerInput.LeftJoycon.ButtonSl; + LeftButtonSr = controllerInput.LeftJoycon.ButtonSr; + ButtonZl = controllerInput.LeftJoycon.ButtonZl; + + ButtonA = controllerInput.RightJoycon.ButtonA; + ButtonB = controllerInput.RightJoycon.ButtonB; + ButtonX = controllerInput.RightJoycon.ButtonX; + ButtonY = controllerInput.RightJoycon.ButtonY; + ButtonR = controllerInput.RightJoycon.ButtonR; + ButtonPlus = controllerInput.RightJoycon.ButtonPlus; + RightButtonSl = controllerInput.RightJoycon.ButtonSl; + RightButtonSr = controllerInput.RightJoycon.ButtonSr; + ButtonZr = controllerInput.RightJoycon.ButtonZr; + + DeadzoneLeft = controllerInput.DeadzoneLeft; + DeadzoneRight = controllerInput.DeadzoneRight; + RangeLeft = controllerInput.RangeLeft; + RangeRight = controllerInput.RangeRight; + TriggerThreshold = controllerInput.TriggerThreshold; + + if (controllerInput.Motion != null) + { + EnableMotion = controllerInput.Motion.EnableMotion; + GyroDeadzone = controllerInput.Motion.GyroDeadzone; + Sensitivity = controllerInput.Motion.Sensitivity; + + if (controllerInput.Motion is CemuHookMotionConfigController cemuHook) + { + EnableCemuHookMotion = true; + DsuServerHost = cemuHook.DsuServerHost; + DsuServerPort = cemuHook.DsuServerPort; + Slot = cemuHook.Slot; + AltSlot = cemuHook.AltSlot; + MirrorInput = cemuHook.MirrorInput; + } + } + + if (controllerInput.Rumble != null) + { + EnableRumble = controllerInput.Rumble.EnableRumble; + WeakRumble = controllerInput.Rumble.WeakRumble; + StrongRumble = controllerInput.Rumble.StrongRumble; + } + } + } + + public InputConfig GetConfig() + { + var config = new StandardControllerInputConfig + { + Id = Id, + Backend = InputBackendType.GamepadSDL2, + PlayerIndex = PlayerIndex, + ControllerType = ControllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = DpadUp, + DpadDown = DpadDown, + DpadLeft = DpadLeft, + DpadRight = DpadRight, + ButtonL = ButtonL, + ButtonMinus = ButtonMinus, + ButtonSl = LeftButtonSl, + ButtonSr = LeftButtonSr, + ButtonZl = ButtonZl + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = ButtonA, + ButtonB = ButtonB, + ButtonX = ButtonX, + ButtonY = ButtonY, + ButtonPlus = ButtonPlus, + ButtonSl = RightButtonSl, + ButtonSr = RightButtonSr, + ButtonR = ButtonR, + ButtonZr = ButtonZr + }, + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = LeftJoystick, + InvertStickX = LeftInvertStickX, + InvertStickY = LeftInvertStickY, + Rotate90CW = LeftRotate90, + StickButton = LeftStickButton + }, + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = RightJoystick, + InvertStickX = RightInvertStickX, + InvertStickY = RightInvertStickY, + Rotate90CW = RightRotate90, + StickButton = RightStickButton + }, + Rumble = new RumbleConfigController + { + EnableRumble = EnableRumble, + WeakRumble = WeakRumble, + StrongRumble = StrongRumble + }, + Version = InputConfig.CurrentVersion, + DeadzoneLeft = DeadzoneLeft, + DeadzoneRight = DeadzoneRight, + RangeLeft = RangeLeft, + RangeRight = RangeRight, + TriggerThreshold = TriggerThreshold + }; + + if (EnableCemuHookMotion) + { + config.Motion = new CemuHookMotionConfigController + { + EnableMotion = EnableMotion, + MotionBackend = MotionInputBackendType.CemuHook, + GyroDeadzone = GyroDeadzone, + Sensitivity = Sensitivity, + DsuServerHost = DsuServerHost, + DsuServerPort = DsuServerPort, + Slot = Slot, + AltSlot = AltSlot, + MirrorInput = MirrorInput + }; + } + else + { + config.Motion = new MotionConfigController + { + EnableMotion = EnableMotion, + MotionBackend = MotionInputBackendType.GamepadDriver, + GyroDeadzone = GyroDeadzone, + Sensitivity = Sensitivity + }; + } + + return config; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs b/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs new file mode 100644 index 000000000..029565210 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs @@ -0,0 +1,422 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Keyboard; + +namespace Ryujinx.Ava.UI.Models.Input +{ + public class KeyboardInputConfig : BaseModel + { + public string Id { get; set; } + public ControllerType ControllerType { get; set; } + public PlayerIndex PlayerIndex { get; set; } + + private Key _leftStickUp; + public Key LeftStickUp + { + get => _leftStickUp; + set + { + _leftStickUp = value; + OnPropertyChanged(); + } + } + + private Key _leftStickDown; + public Key LeftStickDown + { + get => _leftStickDown; + set + { + _leftStickDown = value; + OnPropertyChanged(); + } + } + + private Key _leftStickLeft; + public Key LeftStickLeft + { + get => _leftStickLeft; + set + { + _leftStickLeft = value; + OnPropertyChanged(); + } + } + + private Key _leftStickRight; + public Key LeftStickRight + { + get => _leftStickRight; + set + { + _leftStickRight = value; + OnPropertyChanged(); + } + } + + private Key _leftStickButton; + public Key LeftStickButton + { + get => _leftStickButton; + set + { + _leftStickButton = value; + OnPropertyChanged(); + } + } + + private Key _rightStickUp; + public Key RightStickUp + { + get => _rightStickUp; + set + { + _rightStickUp = value; + OnPropertyChanged(); + } + } + + private Key _rightStickDown; + public Key RightStickDown + { + get => _rightStickDown; + set + { + _rightStickDown = value; + OnPropertyChanged(); + } + } + + private Key _rightStickLeft; + public Key RightStickLeft + { + get => _rightStickLeft; + set + { + _rightStickLeft = value; + OnPropertyChanged(); + } + } + + private Key _rightStickRight; + public Key RightStickRight + { + get => _rightStickRight; + set + { + _rightStickRight = value; + OnPropertyChanged(); + } + } + + private Key _rightStickButton; + public Key RightStickButton + { + get => _rightStickButton; + set + { + _rightStickButton = value; + OnPropertyChanged(); + } + } + + private Key _dpadUp; + public Key DpadUp + { + get => _dpadUp; + set + { + _dpadUp = value; + OnPropertyChanged(); + } + } + + private Key _dpadDown; + public Key DpadDown + { + get => _dpadDown; + set + { + _dpadDown = value; + OnPropertyChanged(); + } + } + + private Key _dpadLeft; + public Key DpadLeft + { + get => _dpadLeft; + set + { + _dpadLeft = value; + OnPropertyChanged(); + } + } + + private Key _dpadRight; + public Key DpadRight + { + get => _dpadRight; + set + { + _dpadRight = value; + OnPropertyChanged(); + } + } + + private Key _buttonL; + public Key ButtonL + { + get => _buttonL; + set + { + _buttonL = value; + OnPropertyChanged(); + } + } + + private Key _buttonMinus; + public Key ButtonMinus + { + get => _buttonMinus; + set + { + _buttonMinus = value; + OnPropertyChanged(); + } + } + + private Key _leftButtonSl; + public Key LeftButtonSl + { + get => _leftButtonSl; + set + { + _leftButtonSl = value; + OnPropertyChanged(); + } + } + + private Key _leftButtonSr; + public Key LeftButtonSr + { + get => _leftButtonSr; + set + { + _leftButtonSr = value; + OnPropertyChanged(); + } + } + + private Key _buttonZl; + public Key ButtonZl + { + get => _buttonZl; + set + { + _buttonZl = value; + OnPropertyChanged(); + } + } + + private Key _buttonA; + public Key ButtonA + { + get => _buttonA; + set + { + _buttonA = value; + OnPropertyChanged(); + } + } + + private Key _buttonB; + public Key ButtonB + { + get => _buttonB; + set + { + _buttonB = value; + OnPropertyChanged(); + } + } + + private Key _buttonX; + public Key ButtonX + { + get => _buttonX; + set + { + _buttonX = value; + OnPropertyChanged(); + } + } + + private Key _buttonY; + public Key ButtonY + { + get => _buttonY; + set + { + _buttonY = value; + OnPropertyChanged(); + } + } + + private Key _buttonR; + public Key ButtonR + { + get => _buttonR; + set + { + _buttonR = value; + OnPropertyChanged(); + } + } + + private Key _buttonPlus; + public Key ButtonPlus + { + get => _buttonPlus; + set + { + _buttonPlus = value; + OnPropertyChanged(); + } + } + + private Key _rightButtonSl; + public Key RightButtonSl + { + get => _rightButtonSl; + set + { + _rightButtonSl = value; + OnPropertyChanged(); + } + } + + private Key _rightButtonSr; + public Key RightButtonSr + { + get => _rightButtonSr; + set + { + _rightButtonSr = value; + OnPropertyChanged(); + } + } + + private Key _buttonZr; + public Key ButtonZr + { + get => _buttonZr; + set + { + _buttonZr = value; + OnPropertyChanged(); + } + } + + public KeyboardInputConfig(InputConfig config) + { + if (config != null) + { + Id = config.Id; + ControllerType = config.ControllerType; + PlayerIndex = config.PlayerIndex; + + if (config is not StandardKeyboardInputConfig keyboardConfig) + { + return; + } + + LeftStickUp = keyboardConfig.LeftJoyconStick.StickUp; + LeftStickDown = keyboardConfig.LeftJoyconStick.StickDown; + LeftStickLeft = keyboardConfig.LeftJoyconStick.StickLeft; + LeftStickRight = keyboardConfig.LeftJoyconStick.StickRight; + LeftStickButton = keyboardConfig.LeftJoyconStick.StickButton; + + RightStickUp = keyboardConfig.RightJoyconStick.StickUp; + RightStickDown = keyboardConfig.RightJoyconStick.StickDown; + RightStickLeft = keyboardConfig.RightJoyconStick.StickLeft; + RightStickRight = keyboardConfig.RightJoyconStick.StickRight; + RightStickButton = keyboardConfig.RightJoyconStick.StickButton; + + DpadUp = keyboardConfig.LeftJoycon.DpadUp; + DpadDown = keyboardConfig.LeftJoycon.DpadDown; + DpadLeft = keyboardConfig.LeftJoycon.DpadLeft; + DpadRight = keyboardConfig.LeftJoycon.DpadRight; + ButtonL = keyboardConfig.LeftJoycon.ButtonL; + ButtonMinus = keyboardConfig.LeftJoycon.ButtonMinus; + LeftButtonSl = keyboardConfig.LeftJoycon.ButtonSl; + LeftButtonSr = keyboardConfig.LeftJoycon.ButtonSr; + ButtonZl = keyboardConfig.LeftJoycon.ButtonZl; + + ButtonA = keyboardConfig.RightJoycon.ButtonA; + ButtonB = keyboardConfig.RightJoycon.ButtonB; + ButtonX = keyboardConfig.RightJoycon.ButtonX; + ButtonY = keyboardConfig.RightJoycon.ButtonY; + ButtonR = keyboardConfig.RightJoycon.ButtonR; + ButtonPlus = keyboardConfig.RightJoycon.ButtonPlus; + RightButtonSl = keyboardConfig.RightJoycon.ButtonSl; + RightButtonSr = keyboardConfig.RightJoycon.ButtonSr; + ButtonZr = keyboardConfig.RightJoycon.ButtonZr; + } + } + + public InputConfig GetConfig() + { + var config = new StandardKeyboardInputConfig + { + Id = Id, + Backend = InputBackendType.WindowKeyboard, + PlayerIndex = PlayerIndex, + ControllerType = ControllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = DpadUp, + DpadDown = DpadDown, + DpadLeft = DpadLeft, + DpadRight = DpadRight, + ButtonL = ButtonL, + ButtonMinus = ButtonMinus, + ButtonZl = ButtonZl, + ButtonSl = LeftButtonSl, + ButtonSr = LeftButtonSr + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = ButtonA, + ButtonB = ButtonB, + ButtonX = ButtonX, + ButtonY = ButtonY, + ButtonPlus = ButtonPlus, + ButtonSl = RightButtonSl, + ButtonSr = RightButtonSr, + ButtonR = ButtonR, + ButtonZr = ButtonZr + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = LeftStickUp, + StickDown = LeftStickDown, + StickRight = LeftStickRight, + StickLeft = LeftStickLeft, + StickButton = LeftStickButton + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = RightStickUp, + StickDown = RightStickDown, + StickLeft = RightStickLeft, + StickRight = RightStickRight, + StickButton = RightStickButton + }, + Version = InputConfig.CurrentVersion + }; + + return config; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs b/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs deleted file mode 100644 index f1352c6d8..000000000 --- a/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs +++ /dev/null @@ -1,456 +0,0 @@ -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Configuration.Hid.Controller; -using Ryujinx.Common.Configuration.Hid.Controller.Motion; -using Ryujinx.Common.Configuration.Hid.Keyboard; -using System; - -namespace Ryujinx.Ava.UI.Models -{ - internal class InputConfiguration : BaseModel - { - private float _deadzoneRight; - private float _triggerThreshold; - private float _deadzoneLeft; - private double _gyroDeadzone; - private int _sensitivity; - private bool _enableMotion; - private float _weakRumble; - private float _strongRumble; - private float _rangeLeft; - private float _rangeRight; - - public InputBackendType Backend { get; set; } - - /// - /// Controller id - /// - public string Id { get; set; } - - /// - /// Controller's Type - /// - public ControllerType ControllerType { get; set; } - - /// - /// Player's Index for the controller - /// - public PlayerIndex PlayerIndex { get; set; } - - public TStick LeftJoystick { get; set; } - public bool LeftInvertStickX { get; set; } - public bool LeftInvertStickY { get; set; } - public bool RightRotate90 { get; set; } - public TKey LeftControllerStickButton { get; set; } - - public TStick RightJoystick { get; set; } - public bool RightInvertStickX { get; set; } - public bool RightInvertStickY { get; set; } - public bool LeftRotate90 { get; set; } - public TKey RightControllerStickButton { get; set; } - - public float DeadzoneLeft - { - get => _deadzoneLeft; - set - { - _deadzoneLeft = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float RangeLeft - { - get => _rangeLeft; - set - { - _rangeLeft = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float DeadzoneRight - { - get => _deadzoneRight; - set - { - _deadzoneRight = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float RangeRight - { - get => _rangeRight; - set - { - _rangeRight = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public float TriggerThreshold - { - get => _triggerThreshold; - set - { - _triggerThreshold = MathF.Round(value, 3); - - OnPropertyChanged(); - } - } - - public MotionInputBackendType MotionBackend { get; set; } - - public TKey ButtonMinus { get; set; } - public TKey ButtonL { get; set; } - public TKey ButtonZl { get; set; } - public TKey LeftButtonSl { get; set; } - public TKey LeftButtonSr { get; set; } - public TKey DpadUp { get; set; } - public TKey DpadDown { get; set; } - public TKey DpadLeft { get; set; } - public TKey DpadRight { get; set; } - - public TKey ButtonPlus { get; set; } - public TKey ButtonR { get; set; } - public TKey ButtonZr { get; set; } - public TKey RightButtonSl { get; set; } - public TKey RightButtonSr { get; set; } - public TKey ButtonX { get; set; } - public TKey ButtonB { get; set; } - public TKey ButtonY { get; set; } - public TKey ButtonA { get; set; } - - public TKey LeftStickUp { get; set; } - public TKey LeftStickDown { get; set; } - public TKey LeftStickLeft { get; set; } - public TKey LeftStickRight { get; set; } - public TKey LeftKeyboardStickButton { get; set; } - - public TKey RightStickUp { get; set; } - public TKey RightStickDown { get; set; } - public TKey RightStickLeft { get; set; } - public TKey RightStickRight { get; set; } - public TKey RightKeyboardStickButton { get; set; } - - public int Sensitivity - { - get => _sensitivity; - set - { - _sensitivity = value; - - OnPropertyChanged(); - } - } - - public double GyroDeadzone - { - get => _gyroDeadzone; - set - { - _gyroDeadzone = Math.Round(value, 3); - - OnPropertyChanged(); - } - } - - public bool EnableMotion - { - get => _enableMotion; set - { - _enableMotion = value; - - OnPropertyChanged(); - } - } - - public bool EnableCemuHookMotion { get; set; } - public int Slot { get; set; } - public int AltSlot { get; set; } - public bool MirrorInput { get; set; } - public string DsuServerHost { get; set; } - public int DsuServerPort { get; set; } - - public bool EnableRumble { get; set; } - public float WeakRumble - { - get => _weakRumble; set - { - _weakRumble = value; - - OnPropertyChanged(); - } - } - public float StrongRumble - { - get => _strongRumble; set - { - _strongRumble = value; - - OnPropertyChanged(); - } - } - - public InputConfiguration(InputConfig config) - { - if (config != null) - { - Backend = config.Backend; - Id = config.Id; - ControllerType = config.ControllerType; - PlayerIndex = config.PlayerIndex; - - if (config is StandardKeyboardInputConfig keyboardConfig) - { - LeftStickUp = (TKey)(object)keyboardConfig.LeftJoyconStick.StickUp; - LeftStickDown = (TKey)(object)keyboardConfig.LeftJoyconStick.StickDown; - LeftStickLeft = (TKey)(object)keyboardConfig.LeftJoyconStick.StickLeft; - LeftStickRight = (TKey)(object)keyboardConfig.LeftJoyconStick.StickRight; - LeftKeyboardStickButton = (TKey)(object)keyboardConfig.LeftJoyconStick.StickButton; - - RightStickUp = (TKey)(object)keyboardConfig.RightJoyconStick.StickUp; - RightStickDown = (TKey)(object)keyboardConfig.RightJoyconStick.StickDown; - RightStickLeft = (TKey)(object)keyboardConfig.RightJoyconStick.StickLeft; - RightStickRight = (TKey)(object)keyboardConfig.RightJoyconStick.StickRight; - RightKeyboardStickButton = (TKey)(object)keyboardConfig.RightJoyconStick.StickButton; - - ButtonA = (TKey)(object)keyboardConfig.RightJoycon.ButtonA; - ButtonB = (TKey)(object)keyboardConfig.RightJoycon.ButtonB; - ButtonX = (TKey)(object)keyboardConfig.RightJoycon.ButtonX; - ButtonY = (TKey)(object)keyboardConfig.RightJoycon.ButtonY; - ButtonR = (TKey)(object)keyboardConfig.RightJoycon.ButtonR; - RightButtonSl = (TKey)(object)keyboardConfig.RightJoycon.ButtonSl; - RightButtonSr = (TKey)(object)keyboardConfig.RightJoycon.ButtonSr; - ButtonZr = (TKey)(object)keyboardConfig.RightJoycon.ButtonZr; - ButtonPlus = (TKey)(object)keyboardConfig.RightJoycon.ButtonPlus; - - DpadUp = (TKey)(object)keyboardConfig.LeftJoycon.DpadUp; - DpadDown = (TKey)(object)keyboardConfig.LeftJoycon.DpadDown; - DpadLeft = (TKey)(object)keyboardConfig.LeftJoycon.DpadLeft; - DpadRight = (TKey)(object)keyboardConfig.LeftJoycon.DpadRight; - ButtonMinus = (TKey)(object)keyboardConfig.LeftJoycon.ButtonMinus; - LeftButtonSl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSl; - LeftButtonSr = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSr; - ButtonZl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonZl; - ButtonL = (TKey)(object)keyboardConfig.LeftJoycon.ButtonL; - } - else if (config is StandardControllerInputConfig controllerConfig) - { - LeftJoystick = (TStick)(object)controllerConfig.LeftJoyconStick.Joystick; - LeftInvertStickX = controllerConfig.LeftJoyconStick.InvertStickX; - LeftInvertStickY = controllerConfig.LeftJoyconStick.InvertStickY; - LeftRotate90 = controllerConfig.LeftJoyconStick.Rotate90CW; - LeftControllerStickButton = (TKey)(object)controllerConfig.LeftJoyconStick.StickButton; - - RightJoystick = (TStick)(object)controllerConfig.RightJoyconStick.Joystick; - RightInvertStickX = controllerConfig.RightJoyconStick.InvertStickX; - RightInvertStickY = controllerConfig.RightJoyconStick.InvertStickY; - RightRotate90 = controllerConfig.RightJoyconStick.Rotate90CW; - RightControllerStickButton = (TKey)(object)controllerConfig.RightJoyconStick.StickButton; - - ButtonA = (TKey)(object)controllerConfig.RightJoycon.ButtonA; - ButtonB = (TKey)(object)controllerConfig.RightJoycon.ButtonB; - ButtonX = (TKey)(object)controllerConfig.RightJoycon.ButtonX; - ButtonY = (TKey)(object)controllerConfig.RightJoycon.ButtonY; - ButtonR = (TKey)(object)controllerConfig.RightJoycon.ButtonR; - RightButtonSl = (TKey)(object)controllerConfig.RightJoycon.ButtonSl; - RightButtonSr = (TKey)(object)controllerConfig.RightJoycon.ButtonSr; - ButtonZr = (TKey)(object)controllerConfig.RightJoycon.ButtonZr; - ButtonPlus = (TKey)(object)controllerConfig.RightJoycon.ButtonPlus; - - DpadUp = (TKey)(object)controllerConfig.LeftJoycon.DpadUp; - DpadDown = (TKey)(object)controllerConfig.LeftJoycon.DpadDown; - DpadLeft = (TKey)(object)controllerConfig.LeftJoycon.DpadLeft; - DpadRight = (TKey)(object)controllerConfig.LeftJoycon.DpadRight; - ButtonMinus = (TKey)(object)controllerConfig.LeftJoycon.ButtonMinus; - LeftButtonSl = (TKey)(object)controllerConfig.LeftJoycon.ButtonSl; - LeftButtonSr = (TKey)(object)controllerConfig.LeftJoycon.ButtonSr; - ButtonZl = (TKey)(object)controllerConfig.LeftJoycon.ButtonZl; - ButtonL = (TKey)(object)controllerConfig.LeftJoycon.ButtonL; - - DeadzoneLeft = controllerConfig.DeadzoneLeft; - DeadzoneRight = controllerConfig.DeadzoneRight; - RangeLeft = controllerConfig.RangeLeft; - RangeRight = controllerConfig.RangeRight; - TriggerThreshold = controllerConfig.TriggerThreshold; - - if (controllerConfig.Motion != null) - { - EnableMotion = controllerConfig.Motion.EnableMotion; - MotionBackend = controllerConfig.Motion.MotionBackend; - GyroDeadzone = controllerConfig.Motion.GyroDeadzone; - Sensitivity = controllerConfig.Motion.Sensitivity; - - if (controllerConfig.Motion is CemuHookMotionConfigController cemuHook) - { - EnableCemuHookMotion = true; - DsuServerHost = cemuHook.DsuServerHost; - DsuServerPort = cemuHook.DsuServerPort; - Slot = cemuHook.Slot; - AltSlot = cemuHook.AltSlot; - MirrorInput = cemuHook.MirrorInput; - } - - if (controllerConfig.Rumble != null) - { - EnableRumble = controllerConfig.Rumble.EnableRumble; - WeakRumble = controllerConfig.Rumble.WeakRumble; - StrongRumble = controllerConfig.Rumble.StrongRumble; - } - } - } - } - } - - public InputConfiguration() - { - } - - public InputConfig GetConfig() - { - if (Backend == InputBackendType.WindowKeyboard) - { - return new StandardKeyboardInputConfig - { - Id = Id, - Backend = Backend, - PlayerIndex = PlayerIndex, - ControllerType = ControllerType, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = (Key)(object)DpadUp, - DpadDown = (Key)(object)DpadDown, - DpadLeft = (Key)(object)DpadLeft, - DpadRight = (Key)(object)DpadRight, - ButtonL = (Key)(object)ButtonL, - ButtonZl = (Key)(object)ButtonZl, - ButtonSl = (Key)(object)LeftButtonSl, - ButtonSr = (Key)(object)LeftButtonSr, - ButtonMinus = (Key)(object)ButtonMinus, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = (Key)(object)ButtonA, - ButtonB = (Key)(object)ButtonB, - ButtonX = (Key)(object)ButtonX, - ButtonY = (Key)(object)ButtonY, - ButtonPlus = (Key)(object)ButtonPlus, - ButtonSl = (Key)(object)RightButtonSl, - ButtonSr = (Key)(object)RightButtonSr, - ButtonR = (Key)(object)ButtonR, - ButtonZr = (Key)(object)ButtonZr, - }, - LeftJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = (Key)(object)LeftStickUp, - StickDown = (Key)(object)LeftStickDown, - StickRight = (Key)(object)LeftStickRight, - StickLeft = (Key)(object)LeftStickLeft, - StickButton = (Key)(object)LeftKeyboardStickButton, - }, - RightJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = (Key)(object)RightStickUp, - StickDown = (Key)(object)RightStickDown, - StickLeft = (Key)(object)RightStickLeft, - StickRight = (Key)(object)RightStickRight, - StickButton = (Key)(object)RightKeyboardStickButton, - }, - Version = InputConfig.CurrentVersion, - }; - - } - - if (Backend == InputBackendType.GamepadSDL2) - { - var config = new StandardControllerInputConfig - { - Id = Id, - Backend = Backend, - PlayerIndex = PlayerIndex, - ControllerType = ControllerType, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = (GamepadInputId)(object)DpadUp, - DpadDown = (GamepadInputId)(object)DpadDown, - DpadLeft = (GamepadInputId)(object)DpadLeft, - DpadRight = (GamepadInputId)(object)DpadRight, - ButtonL = (GamepadInputId)(object)ButtonL, - ButtonZl = (GamepadInputId)(object)ButtonZl, - ButtonSl = (GamepadInputId)(object)LeftButtonSl, - ButtonSr = (GamepadInputId)(object)LeftButtonSr, - ButtonMinus = (GamepadInputId)(object)ButtonMinus, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = (GamepadInputId)(object)ButtonA, - ButtonB = (GamepadInputId)(object)ButtonB, - ButtonX = (GamepadInputId)(object)ButtonX, - ButtonY = (GamepadInputId)(object)ButtonY, - ButtonPlus = (GamepadInputId)(object)ButtonPlus, - ButtonSl = (GamepadInputId)(object)RightButtonSl, - ButtonSr = (GamepadInputId)(object)RightButtonSr, - ButtonR = (GamepadInputId)(object)ButtonR, - ButtonZr = (GamepadInputId)(object)ButtonZr, - }, - LeftJoyconStick = new JoyconConfigControllerStick - { - Joystick = (StickInputId)(object)LeftJoystick, - InvertStickX = LeftInvertStickX, - InvertStickY = LeftInvertStickY, - Rotate90CW = LeftRotate90, - StickButton = (GamepadInputId)(object)LeftControllerStickButton, - }, - RightJoyconStick = new JoyconConfigControllerStick - { - Joystick = (StickInputId)(object)RightJoystick, - InvertStickX = RightInvertStickX, - InvertStickY = RightInvertStickY, - Rotate90CW = RightRotate90, - StickButton = (GamepadInputId)(object)RightControllerStickButton, - }, - Rumble = new RumbleConfigController - { - EnableRumble = EnableRumble, - WeakRumble = WeakRumble, - StrongRumble = StrongRumble, - }, - Version = InputConfig.CurrentVersion, - DeadzoneLeft = DeadzoneLeft, - DeadzoneRight = DeadzoneRight, - RangeLeft = RangeLeft, - RangeRight = RangeRight, - TriggerThreshold = TriggerThreshold, - Motion = EnableCemuHookMotion - ? new CemuHookMotionConfigController - { - DsuServerHost = DsuServerHost, - DsuServerPort = DsuServerPort, - Slot = Slot, - AltSlot = AltSlot, - MirrorInput = MirrorInput, - MotionBackend = MotionInputBackendType.CemuHook, - } - : new StandardMotionConfigController - { - MotionBackend = MotionInputBackendType.GamepadDriver, - }, - }; - - config.Motion.Sensitivity = Sensitivity; - config.Motion.EnableMotion = EnableMotion; - config.Motion.GyroDeadzone = GyroDeadzone; - - return config; - } - - return null; - } - } -} diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs new file mode 100644 index 000000000..0e23dfa76 --- /dev/null +++ b/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs @@ -0,0 +1,84 @@ +using Avalonia.Svg.Skia; +using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Ava.UI.Views.Input; + +namespace Ryujinx.Ava.UI.ViewModels.Input +{ + public class ControllerInputViewModel : BaseModel + { + private ControllerInputConfig _config; + public ControllerInputConfig Config + { + get => _config; + set + { + _config = value; + OnPropertyChanged(); + } + } + + private bool _isLeft; + public bool IsLeft + { + get => _isLeft; + set + { + _isLeft = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + private bool _isRight; + public bool IsRight + { + get => _isRight; + set + { + _isRight = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + public bool HasSides => IsLeft ^ IsRight; + + private SvgImage _image; + public SvgImage Image + { + get => _image; + set + { + _image = value; + OnPropertyChanged(); + } + } + + public InputViewModel parentModel; + + public ControllerInputViewModel(InputViewModel model, ControllerInputConfig config) + { + parentModel = model; + model.NotifyChangesEvent += OnParentModelChanged; + OnParentModelChanged(); + Config = config; + } + + public async void ShowMotionConfig() + { + await MotionInputView.Show(this); + } + + public async void ShowRumbleConfig() + { + await RumbleInputView.Show(this); + } + + public void OnParentModelChanged() + { + IsLeft = parentModel.IsLeft; + IsRight = parentModel.IsRight; + Image = parentModel.Image; + } + } +} diff --git a/src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs similarity index 92% rename from src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs index c0c625321..ef8ffd50d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs @@ -8,7 +8,7 @@ using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.Views.Input; +using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -30,9 +30,9 @@ using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.Gamepad using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; using Key = Ryujinx.Common.Configuration.Hid.Key; -namespace Ryujinx.Ava.UI.ViewModels +namespace Ryujinx.Ava.UI.ViewModels.Input { - public class ControllerInputViewModel : BaseModel, IDisposable + public class InputViewModel : BaseModel, IDisposable { private const string Disabled = "disabled"; private const string ProControllerResource = "Ryujinx.Ui.Common/Resources/Controller_ProCon.svg"; @@ -48,7 +48,7 @@ namespace Ryujinx.Ava.UI.ViewModels private int _controllerNumber; private string _controllerImage; private int _device; - private object _configuration; + private object _configViewModel; private string _profileName; private bool _isLoaded; @@ -71,13 +71,14 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsLeft { get; set; } public bool IsModified { get; set; } + public event Action NotifyChangesEvent; - public object Configuration + public object ConfigViewModel { - get => _configuration; + get => _configViewModel; set { - _configuration = value; + _configViewModel = value; OnPropertyChanged(); } @@ -232,7 +233,7 @@ namespace Ryujinx.Ava.UI.ViewModels public InputConfig Config { get; set; } - public ControllerInputViewModel(UserControl owner) : this() + public InputViewModel(UserControl owner) : this() { if (Program.PreviewerDetached) { @@ -244,7 +245,6 @@ namespace Ryujinx.Ava.UI.ViewModels _mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; - _mainWindow.ViewModel.AppHost?.NpadManager.BlockInputUpdates(); _isLoaded = false; @@ -255,7 +255,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public ControllerInputViewModel() + public InputViewModel() { PlayerIndexes = new ObservableCollection(); Controllers = new ObservableCollection(); @@ -282,12 +282,12 @@ namespace Ryujinx.Ava.UI.ViewModels if (Config is StandardKeyboardInputConfig keyboardInputConfig) { - Configuration = new InputConfiguration(keyboardInputConfig); + ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig)); } if (Config is StandardControllerInputConfig controllerInputConfig) { - Configuration = new InputConfiguration(controllerInputConfig); + ConfigViewModel = new ControllerInputViewModel(this, new ControllerInputConfig(controllerInputConfig)); } } @@ -323,16 +323,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public async void ShowMotionConfig() - { - await MotionInputView.Show(this); - } - - public async void ShowRumbleConfig() - { - await RumbleInputView.Show(this); - } - private void LoadInputDriver() { if (_device < 0) @@ -740,7 +730,7 @@ namespace Ryujinx.Ava.UI.ViewModels return; } - if (Configuration == null) + if (ConfigViewModel == null) { return; } @@ -751,35 +741,37 @@ namespace Ryujinx.Ava.UI.ViewModels return; } - - bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; - - if (validFileName) - { - string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); - - InputConfig config = null; - - if (IsKeyboard) - { - config = (Configuration as InputConfiguration).GetConfig(); - } - else if (IsController) - { - config = (Configuration as InputConfiguration).GetConfig(); - } - - config.ControllerType = Controllers[_controller].Type; - - string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig); - - await File.WriteAllTextAsync(path, jsonString); - - LoadProfiles(); - } else { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); + bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; + + if (validFileName) + { + string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); + + InputConfig config = null; + + if (IsKeyboard) + { + config = (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig(); + } + else if (IsController) + { + config = (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); + } + + config.ControllerType = Controllers[_controller].Type; + + string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig); + + await File.WriteAllTextAsync(path, jsonString); + + LoadProfiles(); + } + else + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); + } } } @@ -830,18 +822,18 @@ namespace Ryujinx.Ava.UI.ViewModels if (device.Type == DeviceType.Keyboard) { - var inputConfig = Configuration as InputConfiguration; + var inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config; inputConfig.Id = device.Id; } else { - var inputConfig = Configuration as InputConfiguration; + var inputConfig = (ConfigViewModel as ControllerInputViewModel).Config; inputConfig.Id = device.Id.Split(" ")[0]; } var config = !IsController - ? (Configuration as InputConfiguration).GetConfig() - : (Configuration as InputConfiguration).GetConfig(); + ? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig() + : (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); config.ControllerType = Controllers[_controller].Type; config.PlayerIndex = _playerId; @@ -872,12 +864,13 @@ namespace Ryujinx.Ava.UI.ViewModels public void NotifyChanges() { - OnPropertyChanged(nameof(Configuration)); + OnPropertyChanged(nameof(ConfigViewModel)); OnPropertyChanged(nameof(IsController)); OnPropertyChanged(nameof(ShowSettings)); OnPropertyChanged(nameof(IsKeyboard)); OnPropertyChanged(nameof(IsRight)); OnPropertyChanged(nameof(IsLeft)); + NotifyChangesEvent?.Invoke(); } public void Dispose() diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs new file mode 100644 index 000000000..a93873063 --- /dev/null +++ b/src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs @@ -0,0 +1,73 @@ +using Avalonia.Svg.Skia; +using Ryujinx.Ava.UI.Models.Input; + +namespace Ryujinx.Ava.UI.ViewModels.Input +{ + public class KeyboardInputViewModel : BaseModel + { + private KeyboardInputConfig _config; + public KeyboardInputConfig Config + { + get => _config; + set + { + _config = value; + OnPropertyChanged(); + } + } + + private bool _isLeft; + public bool IsLeft + { + get => _isLeft; + set + { + _isLeft = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + private bool _isRight; + public bool IsRight + { + get => _isRight; + set + { + _isRight = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasSides)); + } + } + + public bool HasSides => IsLeft ^ IsRight; + + private SvgImage _image; + public SvgImage Image + { + get => _image; + set + { + _image = value; + OnPropertyChanged(); + } + } + + public InputViewModel parentModel; + + public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config) + { + parentModel = model; + model.NotifyChangesEvent += OnParentModelChanged; + OnParentModelChanged(); + Config = config; + } + + public void OnParentModelChanged() + { + IsLeft = parentModel.IsLeft; + IsRight = parentModel.IsRight; + Image = parentModel.Image; + } + } +} diff --git a/src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs similarity index 97% rename from src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs index 0b12a51f6..c9ed8f2d4 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ava.UI.ViewModels +namespace Ryujinx.Ava.UI.ViewModels.Input { public class MotionInputViewModel : BaseModel { diff --git a/src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs similarity index 92% rename from src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs index 49de19937..8ad33cf4c 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ava.UI.ViewModels +namespace Ryujinx.Ava.UI.ViewModels.Input { public class RumbleInputViewModel : BaseModel { diff --git a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml index d636873a3..08bdf90f4 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml @@ -1,13 +1,11 @@ @@ -34,191 +33,10 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Orientation="Vertical"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + MinHeight="450"> @@ -257,9 +75,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerZL}" TextAlignment="Center" /> - + @@ -273,9 +91,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerL}" TextAlignment="Center" /> - + @@ -289,9 +107,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsButtonMinus}" TextAlignment="Center" /> - + @@ -311,100 +129,8 @@ Margin="0,0,0,10" HorizontalAlignment="Center" Text="{locale:Locale ControllerSettingsLStick}" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -415,9 +141,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsStickButton}" TextAlignment="Center" /> - + @@ -432,22 +158,22 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsStickStick}" TextAlignment="Center" /> - + - + - + - + + Value="{Binding Config.DeadzoneLeft, Mode=TwoWay}" /> + Text="{Binding Config.DeadzoneLeft, StringFormat=\{0:0.00\}}" /> + Value="{Binding Config.RangeLeft, Mode=TwoWay}" /> + Text="{Binding Config.RangeLeft, StringFormat=\{0:0.00\}}" /> @@ -525,9 +251,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadUp}" TextAlignment="Center" /> - + @@ -542,9 +268,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadDown}" TextAlignment="Center" /> - + @@ -559,9 +285,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadLeft}" TextAlignment="Center" /> - + @@ -576,9 +302,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadRight}" TextAlignment="Center" /> - + @@ -591,6 +317,13 @@ Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> + + + Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" /> + Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" /> - + - + IsVisible="{Binding IsLeft}" + Orientation="Horizontal"> - - - - + + + + - + IsVisible="{Binding IsLeft}" + Orientation="Horizontal"> - - - - + + + + - + IsVisible="{Binding IsRight}" + Orientation="Horizontal"> - - - - + + + + - + IsVisible="{Binding IsRight}" + Orientation="Horizontal"> - + + + + - - + HorizontalAlignment="Stretch"> @@ -720,7 +449,7 @@ Margin="10" MinWidth="0" Grid.Column="0" - IsChecked="{ReflectionBinding Configuration.EnableMotion, Mode=TwoWay}"> + IsChecked="{Binding Config.EnableMotion, Mode=TwoWay}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs new file mode 100644 index 000000000..356381a8a --- /dev/null +++ b/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs @@ -0,0 +1,61 @@ +using Avalonia.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels.Input; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class InputView : UserControl + { + private bool _dialogOpen; + private InputViewModel ViewModel { get; set; } + + public InputView() + { + DataContext = ViewModel = new InputViewModel(this); + + InitializeComponent(); + } + + public void SaveCurrentProfile() + { + ViewModel.Save(); + } + + private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ViewModel.IsModified && !_dialogOpen) + { + _dialogOpen = true; + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage], + LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage], + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); + + if (result == UserResult.Yes) + { + ViewModel.Save(); + } + + _dialogOpen = false; + + ViewModel.IsModified = false; + + if (e.AddedItems.Count > 0) + { + var player = (PlayerModel)e.AddedItems[0]; + ViewModel.PlayerId = player.Id; + } + } + } + + public void Dispose() + { + ViewModel.Dispose(); + } + } +} diff --git a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml new file mode 100644 index 000000000..e4566f463 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml @@ -0,0 +1,675 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs new file mode 100644 index 000000000..f7024c5d1 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs @@ -0,0 +1,210 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; + +namespace Ryujinx.Ava.UI.Views.Input +{ + public partial class KeyboardInputView : UserControl + { + private ButtonKeyAssigner _currentAssigner; + + public KeyboardInputView() + { + InitializeComponent(); + + foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) + { + if (visual is ToggleButton button and not CheckBox) + { + button.IsCheckedChanged += Button_IsCheckedChanged; + } + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver) + { + _currentAssigner.Cancel(); + } + } + + private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton button) + { + if ((bool)button.IsChecked) + { + if (_currentAssigner != null && button == _currentAssigner.ToggledButton) + { + return; + } + + bool isStick = button.Tag != null && button.Tag.ToString() == "stick"; + + if (_currentAssigner == null && (bool)button.IsChecked) + { + _currentAssigner = new ButtonKeyAssigner(button); + + this.Focus(NavigationMethod.Pointer); + + PointerPressed += MouseClick; + + IKeyboard keyboard = (IKeyboard)(DataContext as KeyboardInputViewModel).parentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. + IButtonAssigner assigner = CreateButtonAssigner(isStick); + + _currentAssigner.ButtonAssigned += (sender, e) => + { + if (e.ButtonValue.HasValue) + { + var viewModel = (DataContext as KeyboardInputViewModel); + var buttonValue = e.ButtonValue.Value; + viewModel.parentModel.IsModified = true; + + switch (button.Name) + { + case "ButtonZl": + viewModel.Config.ButtonZl = buttonValue.AsKey(); + break; + case "ButtonL": + viewModel.Config.ButtonL = buttonValue.AsKey(); + break; + case "ButtonMinus": + viewModel.Config.ButtonMinus = buttonValue.AsKey(); + break; + case "LeftStickButton": + viewModel.Config.LeftStickButton = buttonValue.AsKey(); + break; + case "LeftStickUp": + viewModel.Config.LeftStickUp = buttonValue.AsKey(); + break; + case "LeftStickDown": + viewModel.Config.LeftStickDown = buttonValue.AsKey(); + break; + case "LeftStickRight": + viewModel.Config.LeftStickRight = buttonValue.AsKey(); + break; + case "LeftStickLeft": + viewModel.Config.LeftStickLeft = buttonValue.AsKey(); + break; + case "DpadUp": + viewModel.Config.DpadUp = buttonValue.AsKey(); + break; + case "DpadDown": + viewModel.Config.DpadDown = buttonValue.AsKey(); + break; + case "DpadLeft": + viewModel.Config.DpadLeft = buttonValue.AsKey(); + break; + case "DpadRight": + viewModel.Config.DpadRight = buttonValue.AsKey(); + break; + case "LeftButtonSr": + viewModel.Config.LeftButtonSr = buttonValue.AsKey(); + break; + case "LeftButtonSl": + viewModel.Config.LeftButtonSl = buttonValue.AsKey(); + break; + case "RightButtonSr": + viewModel.Config.RightButtonSr = buttonValue.AsKey(); + break; + case "RightButtonSl": + viewModel.Config.RightButtonSl = buttonValue.AsKey(); + break; + case "ButtonZr": + viewModel.Config.ButtonZr = buttonValue.AsKey(); + break; + case "ButtonR": + viewModel.Config.ButtonR = buttonValue.AsKey(); + break; + case "ButtonPlus": + viewModel.Config.ButtonPlus = buttonValue.AsKey(); + break; + case "ButtonA": + viewModel.Config.ButtonA = buttonValue.AsKey(); + break; + case "ButtonB": + viewModel.Config.ButtonB = buttonValue.AsKey(); + break; + case "ButtonX": + viewModel.Config.ButtonX = buttonValue.AsKey(); + break; + case "ButtonY": + viewModel.Config.ButtonY = buttonValue.AsKey(); + break; + case "RightStickButton": + viewModel.Config.RightStickButton = buttonValue.AsKey(); + break; + case "RightStickUp": + viewModel.Config.RightStickUp = buttonValue.AsKey(); + break; + case "RightStickDown": + viewModel.Config.RightStickDown = buttonValue.AsKey(); + break; + case "RightStickRight": + viewModel.Config.RightStickRight = buttonValue.AsKey(); + break; + case "RightStickLeft": + viewModel.Config.RightStickLeft = buttonValue.AsKey(); + break; + } + } + }; + + _currentAssigner.GetInputAndAssign(assigner, keyboard); + } + else + { + if (_currentAssigner != null) + { + ToggleButton oldButton = _currentAssigner.ToggledButton; + + _currentAssigner.Cancel(); + _currentAssigner = null; + button.IsChecked = false; + } + } + } + else + { + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } + } + + private void MouseClick(object sender, PointerPressedEventArgs e) + { + bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed; + + _currentAssigner?.Cancel(shouldUnbind); + + PointerPressed -= MouseClick; + } + + private IButtonAssigner CreateButtonAssigner(bool forStick) + { + IButtonAssigner assigner; + + assigner = new KeyboardKeyAssigner((IKeyboard)(DataContext as KeyboardInputViewModel).parentModel.SelectedGamepad); + + return assigner; + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _currentAssigner?.Cancel(); + _currentAssigner = null; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml index a6b587f67..0d018e297 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml @@ -6,7 +6,7 @@ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.MotionInputView" x:DataType="viewModels:MotionInputViewModel" diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs index 1b340752b..2304364b6 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs @@ -1,9 +1,7 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Ava.UI.ViewModels.Input; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input @@ -19,7 +17,7 @@ namespace Ryujinx.Ava.UI.Views.Input public MotionInputView(ControllerInputViewModel viewModel) { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; _viewModel = new MotionInputViewModel { @@ -51,7 +49,7 @@ namespace Ryujinx.Ava.UI.Views.Input }; contentDialog.PrimaryButtonClick += (sender, args) => { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; config.Slot = content._viewModel.Slot; config.Sensitivity = content._viewModel.Sensitivity; config.GyroDeadzone = content._viewModel.GyroDeadzone; diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml index 5b7087a47..1beb1f06e 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.RumbleInputView" x:DataType="viewModels:RumbleInputViewModel" diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs index 9307f872c..58a4b416b 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs @@ -1,9 +1,7 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Ava.UI.ViewModels.Input; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input @@ -19,7 +17,7 @@ namespace Ryujinx.Ava.UI.Views.Input public RumbleInputView(ControllerInputViewModel viewModel) { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; _viewModel = new RumbleInputViewModel { @@ -47,7 +45,7 @@ namespace Ryujinx.Ava.UI.Views.Input contentDialog.PrimaryButtonClick += (sender, args) => { - var config = viewModel.Configuration as InputConfiguration; + var config = viewModel.Config; config.StrongRumble = content._viewModel.StrongRumble; config.WeakRumble = content._viewModel.WeakRumble; }; diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml index 81f4b68b7..55c2ed6e3 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml @@ -27,9 +27,9 @@ - + Name="InputView" /> diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs index 8a0cb8ab9..85ccffccd 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Ava.UI.Views.Settings public void Dispose() { - ControllerSettings.Dispose(); + InputView.Dispose(); } } } diff --git a/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs index d7bb0b883..314501c52 100644 --- a/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs @@ -37,7 +37,7 @@ namespace Ryujinx.Ava.UI.Windows public void SaveSettings() { - InputPage.ControllerSettings?.SaveCurrentProfile(); + InputPage.InputView?.SaveCurrentProfile(); if (Owner is MainWindow window && ViewModel.DirectoryChanged) { diff --git a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs index 388ebcc07..bf8319a6a 100644 --- a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs @@ -59,16 +59,16 @@ namespace Ryujinx.Input.Assigner return _gamepad == null || !_gamepad.IsConnected; } - public string GetPressedButton() + public ButtonValue? GetPressedButton() { IEnumerable pressedButtons = _detector.GetPressedButtons(); if (pressedButtons.Any()) { - return !_forStick ? pressedButtons.First().ToString() : ((StickInputId)pressedButtons.First()).ToString(); + return !_forStick ? new(pressedButtons.First()) : new(((StickInputId)pressedButtons.First())); } - return ""; + return null; } private void CollectButtonStats() diff --git a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs index 76a9fece4..653717133 100644 --- a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs @@ -31,6 +31,6 @@ namespace Ryujinx.Input.Assigner /// Get the pressed button that was read in by the button assigner. /// /// The pressed button that was read - string GetPressedButton(); + ButtonValue? GetPressedButton(); } } diff --git a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs index e52ef4a2c..c66812ba0 100644 --- a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs +++ b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Input.Assigner public bool HasAnyButtonPressed() { - return GetPressedButton().Length != 0; + return GetPressedButton() is not null; } public bool ShouldCancel() @@ -31,20 +31,20 @@ namespace Ryujinx.Input.Assigner return _keyboardState.IsPressed(Key.Escape); } - public string GetPressedButton() + public ButtonValue? GetPressedButton() { - string keyPressed = ""; + ButtonValue? keyPressed = null; for (Key key = Key.Unknown; key < Key.Count; key++) { if (_keyboardState.IsPressed(key)) { - keyPressed = key.ToString(); + keyPressed = new(key); break; } } - return !ShouldCancel() ? keyPressed : ""; + return !ShouldCancel() ? keyPressed : null; } } } diff --git a/src/Ryujinx.Input/ButtonValue.cs b/src/Ryujinx.Input/ButtonValue.cs new file mode 100644 index 000000000..f037e6b60 --- /dev/null +++ b/src/Ryujinx.Input/ButtonValue.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; + +namespace Ryujinx.Input +{ + public enum ButtonValueType { Key, GamepadButtonInputId, StickId } + + public readonly struct ButtonValue + { + private readonly ButtonValueType _type; + private readonly uint _rawValue; + + public ButtonValue(Key key) + { + _type = ButtonValueType.Key; + _rawValue = (uint)key; + } + + public ButtonValue(GamepadButtonInputId gamepad) + { + _type = ButtonValueType.GamepadButtonInputId; + _rawValue = (uint)gamepad; + } + + public ButtonValue(StickInputId stick) + { + _type = ButtonValueType.StickId; + _rawValue = (uint)stick; + } + + public Common.Configuration.Hid.Key AsKey() + { + Debug.Assert(_type == ButtonValueType.Key); + return (Common.Configuration.Hid.Key)_rawValue; + } + + public Common.Configuration.Hid.Controller.GamepadInputId AsGamepadButtonInputId() + { + Debug.Assert(_type == ButtonValueType.GamepadButtonInputId); + return (Common.Configuration.Hid.Controller.GamepadInputId)_rawValue; + } + + public Common.Configuration.Hid.Controller.StickInputId AsGamepadStickId() + { + Debug.Assert(_type == ButtonValueType.StickId); + return (Common.Configuration.Hid.Controller.StickInputId)_rawValue; + } + } +} diff --git a/src/Ryujinx/Ui/Windows/ControllerWindow.cs b/src/Ryujinx/Ui/Windows/ControllerWindow.cs index ebf22ab60..52cad5c85 100644 --- a/src/Ryujinx/Ui/Windows/ControllerWindow.cs +++ b/src/Ryujinx/Ui/Windows/ControllerWindow.cs @@ -893,7 +893,7 @@ namespace Ryujinx.Ui.Windows } } - string pressedButton = assigner.GetPressedButton(); + string pressedButton = assigner.GetPressedButton().ToString(); Application.Invoke(delegate { From 638be5f296bf52943da4366699d33f1e8656e00c Mon Sep 17 00:00:00 2001 From: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Date: Sat, 21 Oct 2023 15:19:21 +0200 Subject: [PATCH 07/40] Revert "Ava UI: Input Menu Refactor (#4998)" This reverts commit 49b37550cae6b3c69f59a9c7a44b17e3c12a813b. This currently breaks the GTK GUI. --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 99 --- src/Ryujinx.Ava/Assets/Styles/Styles.xaml | 5 +- .../UI/Helpers/ButtonKeyAssigner.cs | 28 +- .../UI/Helpers/KeyValueConverter.cs | 167 +---- .../UI/Models/Input/ControllerInputConfig.cs | 580 --------------- .../UI/Models/Input/KeyboardInputConfig.cs | 422 ----------- .../UI/Models/InputConfiguration.cs | 456 ++++++++++++ ...ewModel.cs => ControllerInputViewModel.cs} | 103 +-- .../Input/ControllerInputViewModel.cs | 84 --- .../Input/KeyboardInputViewModel.cs | 73 -- .../{Input => }/MotionInputViewModel.cs | 2 +- .../{Input => }/RumbleInputViewModel.cs | 2 +- .../UI/Views/Input/ControllerInputView.axaml | 616 ++++++++++++---- .../Views/Input/ControllerInputView.axaml.cs | 164 ++--- .../UI/Views/Input/InputView.axaml | 225 ------ .../UI/Views/Input/InputView.axaml.cs | 61 -- .../UI/Views/Input/KeyboardInputView.axaml | 675 ------------------ .../UI/Views/Input/KeyboardInputView.axaml.cs | 210 ------ .../UI/Views/Input/MotionInputView.axaml | 2 +- .../UI/Views/Input/MotionInputView.axaml.cs | 8 +- .../UI/Views/Input/RumbleInputView.axaml | 2 +- .../UI/Views/Input/RumbleInputView.axaml.cs | 8 +- .../UI/Views/Settings/SettingsInputView.axaml | 4 +- .../Views/Settings/SettingsInputView.axaml.cs | 2 +- .../UI/Windows/SettingsWindow.axaml.cs | 2 +- .../Assigner/GamepadButtonAssigner.cs | 6 +- src/Ryujinx.Input/Assigner/IButtonAssigner.cs | 2 +- .../Assigner/KeyboardKeyAssigner.cs | 10 +- src/Ryujinx.Input/ButtonValue.cs | 48 -- src/Ryujinx/Ui/Windows/ControllerWindow.cs | 2 +- 30 files changed, 1152 insertions(+), 2916 deletions(-) delete mode 100644 src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs delete mode 100644 src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs create mode 100644 src/Ryujinx.Ava/UI/Models/InputConfiguration.cs rename src/Ryujinx.Ava/UI/ViewModels/{Input/InputViewModel.cs => ControllerInputViewModel.cs} (92%) delete mode 100644 src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs delete mode 100644 src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs rename src/Ryujinx.Ava/UI/ViewModels/{Input => }/MotionInputViewModel.cs (97%) rename src/Ryujinx.Ava/UI/ViewModels/{Input => }/RumbleInputViewModel.cs (92%) delete mode 100644 src/Ryujinx.Ava/UI/Views/Input/InputView.axaml delete mode 100644 src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs delete mode 100644 src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml delete mode 100644 src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs delete mode 100644 src/Ryujinx.Input/ButtonValue.cs diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index fc65fe4a0..a67b796bd 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -263,105 +263,6 @@ "ControllerSettingsMotionGyroDeadzone": "Gyro Deadzone:", "ControllerSettingsSave": "Save", "ControllerSettingsClose": "Close", - "KeyUnknown": "Unknown", - "KeyShiftLeft": "Shift Left", - "KeyShiftRight": "Shift Right", - "KeyControlLeft": "Control Left", - "KeyControlRight": "Control Right", - "KeyAltLeft": "Alt Left", - "KeyAltRight": "Alt Right", - "KeyOptLeft": "⌥ Left", - "KeyOptRight": "⌥ Right", - "KeyWinLeft": "⊞ Left", - "KeyWinRight": "⊞ Right", - "KeyCmdLeft": "⌘ Left", - "KeyCmdRight": "⌘ Right", - "KeyMenu": "Menu", - "KeyUp": "Up", - "KeyDown": "Down", - "KeyLeft": "Left", - "KeyRight": "Right", - "KeyEnter": "Enter", - "KeyEscape": "Escape", - "KeySpace": "Space", - "KeyTab": "Tab", - "KeyBackSpace": "Backspace", - "KeyInsert": "Insert", - "KeyDelete": "Delete", - "KeyPageUp": "Page Up", - "KeyPageDown": "Page Down", - "KeyHome": "Home", - "KeyEnd": "End", - "KeyCapsLock": "Caps Lock", - "KeyScrollLock": "Scroll Lock", - "KeyPrintScreen": "Print Screen", - "KeyPause": "Pause", - "KeyNumLock": "Num Lock", - "KeyClear": "Clear", - "KeyKeypad0": "Keypad 0", - "KeyKeypad1": "Keypad 1", - "KeyKeypad2": "Keypad 2", - "KeyKeypad3": "Keypad 3", - "KeyKeypad4": "Keypad 4", - "KeyKeypad5": "Keypad 5", - "KeyKeypad6": "Keypad 6", - "KeyKeypad7": "Keypad 7", - "KeyKeypad8": "Keypad 8", - "KeyKeypad9": "Keypad 9", - "KeyKeypadDivide": "Keypad Divide", - "KeyKeypadMultiply": "Keypad Multiply", - "KeyKeypadSubtract": "Keypad Subtract", - "KeyKeypadAdd": "Keypad Add", - "KeyKeypadDecimal": "Keypad Decimal", - "KeyKeypadEnter": "Keypad Enter", - "KeyNumber0": "0", - "KeyNumber1": "1", - "KeyNumber2": "2", - "KeyNumber3": "3", - "KeyNumber4": "4", - "KeyNumber5": "5", - "KeyNumber6": "6", - "KeyNumber7": "7", - "KeyNumber8": "8", - "KeyNumber9": "9", - "KeyTilde": "~", - "KeyGrave": "`", - "KeyMinus": "-", - "KeyPlus": "+", - "KeyBracketLeft": "[", - "KeyBracketRight": "]", - "KeySemicolon": ";", - "KeyQuote": "\"", - "KeyComma": ",", - "KeyPeriod": ".", - "KeySlash": "/", - "KeyBackSlash": "\\", - "KeyUnbound": "Unbound", - "GamepadLeftStick": "Left Stick Button", - "GamepadRightStick": "Right Stick Button", - "GamepadLeftShoulder": "Left Shoulder", - "GamepadRightShoulder": "Right Shoulder", - "GamepadLeftTrigger": "Left Trigger", - "GamepadRightTrigger": "Right Trigger", - "GamepadDpadUp": "Up", - "GamepadDpadDown": "Down", - "GamepadDpadLeft": "Left", - "GamepadDpadRight": "Right", - "GamepadMinus": "-", - "GamepadPlus": "+", - "GamepadGuide": "Guide", - "GamepadMisc1": "Misc", - "GamepadPaddle1": "Paddle 1", - "GamepadPaddle2": "Paddle 2", - "GamepadPaddle3": "Paddle 3", - "GamepadPaddle4": "Paddle 4", - "GamepadTouchpad": "Touchpad", - "GamepadSingleLeftTrigger0": "Left Trigger 0", - "GamepadSingleRightTrigger0": "Right Trigger 0", - "GamepadSingleLeftTrigger1": "Left Trigger 1", - "GamepadSingleRightTrigger1": "Right Trigger 1", - "StickLeft": "Left Stick", - "StickRight": "Right Stick", "UserProfilesSelectedUserProfile": "Selected User Profile:", "UserProfilesSaveProfileName": "Save Profile Name", "UserProfilesChangeProfileImage": "Change Profile Image", diff --git a/src/Ryujinx.Ava/Assets/Styles/Styles.xaml b/src/Ryujinx.Ava/Assets/Styles/Styles.xaml index b3a6f59c8..f7f64be22 100644 --- a/src/Ryujinx.Ava/Assets/Styles/Styles.xaml +++ b/src/Ryujinx.Ava/Assets/Styles/Styles.xaml @@ -15,7 +15,8 @@ - + @@ -392,4 +393,4 @@ 600 756 - + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs index 54e0918a5..7e8ba7342 100644 --- a/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs +++ b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs @@ -1,8 +1,11 @@ +using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.LogicalTree; using Avalonia.Threading; using Ryujinx.Input; using Ryujinx.Input.Assigner; using System; +using System.Linq; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Helpers @@ -12,12 +15,12 @@ namespace Ryujinx.Ava.UI.Helpers internal class ButtonAssignedEventArgs : EventArgs { public ToggleButton Button { get; } - public ButtonValue? ButtonValue { get; } + public bool IsAssigned { get; } - public ButtonAssignedEventArgs(ToggleButton button, ButtonValue? buttonValue) + public ButtonAssignedEventArgs(ToggleButton button, bool isAssigned) { Button = button; - ButtonValue = buttonValue; + IsAssigned = isAssigned; } } @@ -75,11 +78,15 @@ namespace Ryujinx.Ava.UI.Helpers await Dispatcher.UIThread.InvokeAsync(() => { - ButtonValue? pressedButton = assigner.GetPressedButton(); + string pressedButton = assigner.GetPressedButton(); if (_shouldUnbind) { - pressedButton = null; + SetButtonText(ToggledButton, "Unbound"); + } + else if (pressedButton != "") + { + SetButtonText(ToggledButton, pressedButton); } _shouldUnbind = false; @@ -87,8 +94,17 @@ namespace Ryujinx.Ava.UI.Helpers ToggledButton.IsChecked = false; - ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton)); + ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton != null)); + static void SetButtonText(ToggleButton button, string text) + { + ILogical textBlock = button.GetLogicalDescendants().First(x => x is TextBlock); + + if (textBlock != null && textBlock is TextBlock block) + { + block.Text = text; + } + } }); } diff --git a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs index 1c4aa7b21..028ed6bf4 100644 --- a/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs +++ b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs @@ -1,9 +1,7 @@ using Avalonia.Data.Converters; -using Ryujinx.Ava.Common.Locale; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using System; -using System.Collections.Generic; using System.Globalization; namespace Ryujinx.Ava.UI.Helpers @@ -12,158 +10,37 @@ namespace Ryujinx.Ava.UI.Helpers { public static KeyValueConverter Instance = new(); - private static readonly Dictionary _keysMap = new() - { - { Key.Unknown, LocaleKeys.KeyUnknown }, - { Key.ShiftLeft, LocaleKeys.KeyShiftLeft }, - { Key.ShiftRight, LocaleKeys.KeyShiftRight }, - { Key.ControlLeft, LocaleKeys.KeyControlLeft }, - { Key.ControlRight, LocaleKeys.KeyControlRight }, - { Key.AltLeft, OperatingSystem.IsMacOS() ? LocaleKeys.KeyOptLeft : LocaleKeys.KeyAltLeft }, - { Key.AltRight, OperatingSystem.IsMacOS() ? LocaleKeys.KeyOptRight : LocaleKeys.KeyAltRight }, - { Key.WinLeft, OperatingSystem.IsMacOS() ? LocaleKeys.KeyCmdLeft : LocaleKeys.KeyWinLeft }, - { Key.WinRight, OperatingSystem.IsMacOS() ? LocaleKeys.KeyCmdRight : LocaleKeys.KeyWinRight }, - { Key.Up, LocaleKeys.KeyUp }, - { Key.Down, LocaleKeys.KeyDown }, - { Key.Left, LocaleKeys.KeyLeft }, - { Key.Right, LocaleKeys.KeyRight }, - { Key.Enter, LocaleKeys.KeyEnter }, - { Key.Escape, LocaleKeys.KeyEscape }, - { Key.Space, LocaleKeys.KeySpace }, - { Key.Tab, LocaleKeys.KeyTab }, - { Key.BackSpace, LocaleKeys.KeyBackSpace }, - { Key.Insert, LocaleKeys.KeyInsert }, - { Key.Delete, LocaleKeys.KeyDelete }, - { Key.PageUp, LocaleKeys.KeyPageUp }, - { Key.PageDown, LocaleKeys.KeyPageDown }, - { Key.Home, LocaleKeys.KeyHome }, - { Key.End, LocaleKeys.KeyEnd }, - { Key.CapsLock, LocaleKeys.KeyCapsLock }, - { Key.ScrollLock, LocaleKeys.KeyScrollLock }, - { Key.PrintScreen, LocaleKeys.KeyPrintScreen }, - { Key.Pause, LocaleKeys.KeyPause }, - { Key.NumLock, LocaleKeys.KeyNumLock }, - { Key.Clear, LocaleKeys.KeyClear }, - { Key.Keypad0, LocaleKeys.KeyKeypad0 }, - { Key.Keypad1, LocaleKeys.KeyKeypad1 }, - { Key.Keypad2, LocaleKeys.KeyKeypad2 }, - { Key.Keypad3, LocaleKeys.KeyKeypad3 }, - { Key.Keypad4, LocaleKeys.KeyKeypad4 }, - { Key.Keypad5, LocaleKeys.KeyKeypad5 }, - { Key.Keypad6, LocaleKeys.KeyKeypad6 }, - { Key.Keypad7, LocaleKeys.KeyKeypad7 }, - { Key.Keypad8, LocaleKeys.KeyKeypad8 }, - { Key.Keypad9, LocaleKeys.KeyKeypad9 }, - { Key.KeypadDivide, LocaleKeys.KeyKeypadDivide }, - { Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply }, - { Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract }, - { Key.KeypadAdd, LocaleKeys.KeyKeypadAdd }, - { Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal }, - { Key.KeypadEnter, LocaleKeys.KeyKeypadEnter }, - { Key.Number0, LocaleKeys.KeyNumber0 }, - { Key.Number1, LocaleKeys.KeyNumber1 }, - { Key.Number2, LocaleKeys.KeyNumber2 }, - { Key.Number3, LocaleKeys.KeyNumber3 }, - { Key.Number4, LocaleKeys.KeyNumber4 }, - { Key.Number5, LocaleKeys.KeyNumber5 }, - { Key.Number6, LocaleKeys.KeyNumber6 }, - { Key.Number7, LocaleKeys.KeyNumber7 }, - { Key.Number8, LocaleKeys.KeyNumber8 }, - { Key.Number9, LocaleKeys.KeyNumber9 }, - { Key.Tilde, LocaleKeys.KeyTilde }, - { Key.Grave, LocaleKeys.KeyGrave }, - { Key.Minus, LocaleKeys.KeyMinus }, - { Key.Plus, LocaleKeys.KeyPlus }, - { Key.BracketLeft, LocaleKeys.KeyBracketLeft }, - { Key.BracketRight, LocaleKeys.KeyBracketRight }, - { Key.Semicolon, LocaleKeys.KeySemicolon }, - { Key.Quote, LocaleKeys.KeyQuote }, - { Key.Comma, LocaleKeys.KeyComma }, - { Key.Period, LocaleKeys.KeyPeriod }, - { Key.Slash, LocaleKeys.KeySlash }, - { Key.BackSlash, LocaleKeys.KeyBackSlash }, - { Key.Unbound, LocaleKeys.KeyUnbound }, - }; - - private static readonly Dictionary _gamepadInputIdMap = new() - { - { GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick }, - { GamepadInputId.RightStick, LocaleKeys.GamepadRightStick }, - { GamepadInputId.LeftShoulder, LocaleKeys.GamepadLeftShoulder }, - { GamepadInputId.RightShoulder, LocaleKeys.GamepadRightShoulder }, - { GamepadInputId.LeftTrigger, LocaleKeys.GamepadLeftTrigger }, - { GamepadInputId.RightTrigger, LocaleKeys.GamepadRightTrigger }, - { GamepadInputId.DpadUp, LocaleKeys.GamepadDpadUp}, - { GamepadInputId.DpadDown, LocaleKeys.GamepadDpadDown}, - { GamepadInputId.DpadLeft, LocaleKeys.GamepadDpadLeft}, - { GamepadInputId.DpadRight, LocaleKeys.GamepadDpadRight}, - { GamepadInputId.Minus, LocaleKeys.GamepadMinus}, - { GamepadInputId.Plus, LocaleKeys.GamepadPlus}, - { GamepadInputId.Guide, LocaleKeys.GamepadGuide}, - { GamepadInputId.Misc1, LocaleKeys.GamepadMisc1}, - { GamepadInputId.Paddle1, LocaleKeys.GamepadPaddle1}, - { GamepadInputId.Paddle2, LocaleKeys.GamepadPaddle2}, - { GamepadInputId.Paddle3, LocaleKeys.GamepadPaddle3}, - { GamepadInputId.Paddle4, LocaleKeys.GamepadPaddle4}, - { GamepadInputId.Touchpad, LocaleKeys.GamepadTouchpad}, - { GamepadInputId.SingleLeftTrigger0, LocaleKeys.GamepadSingleLeftTrigger0}, - { GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0}, - { GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1}, - { GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1}, - { GamepadInputId.Unbound, LocaleKeys.KeyUnbound}, - }; - - private static readonly Dictionary _stickInputIdMap = new() - { - { StickInputId.Left, LocaleKeys.StickLeft}, - { StickInputId.Right, LocaleKeys.StickRight}, - { StickInputId.Unbound, LocaleKeys.KeyUnbound}, - }; - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - string keyString = ""; - - if (value is Key key) + if (value == null) { - if (_keysMap.TryGetValue(key, out LocaleKeys localeKey)) - { - keyString = LocaleManager.Instance[localeKey]; - } - else - { - keyString = key.ToString(); - } - } - else if (value is GamepadInputId gamepadInputId) - { - if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out LocaleKeys localeKey)) - { - keyString = LocaleManager.Instance[localeKey]; - } - else - { - keyString = gamepadInputId.ToString(); - } - } - else if (value is StickInputId stickInputId) - { - if (_stickInputIdMap.TryGetValue(stickInputId, out LocaleKeys localeKey)) - { - keyString = LocaleManager.Instance[localeKey]; - } - else - { - keyString = stickInputId.ToString(); - } + return null; } - return keyString; + return value.ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - throw new NotSupportedException(); + object key = null; + + if (value != null) + { + if (targetType == typeof(Key)) + { + key = Enum.Parse(value.ToString()); + } + else if (targetType == typeof(GamepadInputId)) + { + key = Enum.Parse(value.ToString()); + } + else if (targetType == typeof(StickInputId)) + { + key = Enum.Parse(value.ToString()); + } + } + + return key; } } } diff --git a/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs b/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs deleted file mode 100644 index 4929e582e..000000000 --- a/src/Ryujinx.Ava/UI/Models/Input/ControllerInputConfig.cs +++ /dev/null @@ -1,580 +0,0 @@ -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Configuration.Hid.Controller; -using Ryujinx.Common.Configuration.Hid.Controller.Motion; -using System; - -namespace Ryujinx.Ava.UI.Models.Input -{ - public class ControllerInputConfig : BaseModel - { - public bool EnableCemuHookMotion { get; set; } - public string DsuServerHost { get; set; } - public int DsuServerPort { get; set; } - public int Slot { get; set; } - public int AltSlot { get; set; } - public bool MirrorInput { get; set; } - public int Sensitivity { get; set; } - public double GyroDeadzone { get; set; } - - public float WeakRumble { get; set; } - public float StrongRumble { get; set; } - - public string Id { get; set; } - public ControllerType ControllerType { get; set; } - public PlayerIndex PlayerIndex { get; set; } - - private StickInputId _leftJoystick; - public StickInputId LeftJoystick - { - get => _leftJoystick; - set - { - _leftJoystick = value; - OnPropertyChanged(); - } - } - - private bool _leftInvertStickX; - public bool LeftInvertStickX - { - get => _leftInvertStickX; - set - { - _leftInvertStickX = value; - OnPropertyChanged(); - } - } - - private bool _leftInvertStickY; - public bool LeftInvertStickY - { - get => _leftInvertStickY; - set - { - _leftInvertStickY = value; - OnPropertyChanged(); - } - } - - private bool _leftRotate90; - public bool LeftRotate90 - { - get => _leftRotate90; - set - { - _leftRotate90 = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _leftStickButton; - public GamepadInputId LeftStickButton - { - get => _leftStickButton; - set - { - _leftStickButton = value; - OnPropertyChanged(); - } - } - - private StickInputId _rightJoystick; - public StickInputId RightJoystick - { - get => _rightJoystick; - set - { - _rightJoystick = value; - OnPropertyChanged(); - } - } - - private bool _rightInvertStickX; - public bool RightInvertStickX - { - get => _rightInvertStickX; - set - { - _rightInvertStickX = value; - OnPropertyChanged(); - } - } - - private bool _rightInvertStickY; - public bool RightInvertStickY - { - get => _rightInvertStickY; - set - { - _rightInvertStickY = value; - OnPropertyChanged(); - } - } - - private bool _rightRotate90; - public bool RightRotate90 - { - get => _rightRotate90; - set - { - _rightRotate90 = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _rightStickButton; - public GamepadInputId RightStickButton - { - get => _rightStickButton; - set - { - _rightStickButton = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _dpadUp; - public GamepadInputId DpadUp - { - get => _dpadUp; - set - { - _dpadUp = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _dpadDown; - public GamepadInputId DpadDown - { - get => _dpadDown; - set - { - _dpadDown = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _dpadLeft; - public GamepadInputId DpadLeft - { - get => _dpadLeft; - set - { - _dpadLeft = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _dpadRight; - public GamepadInputId DpadRight - { - get => _dpadRight; - set - { - _dpadRight = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _buttonL; - public GamepadInputId ButtonL - { - get => _buttonL; - set - { - _buttonL = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _buttonMinus; - public GamepadInputId ButtonMinus - { - get => _buttonMinus; - set - { - _buttonMinus = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _leftButtonSl; - public GamepadInputId LeftButtonSl - { - get => _leftButtonSl; - set - { - _leftButtonSl = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _leftButtonSr; - public GamepadInputId LeftButtonSr - { - get => _leftButtonSr; - set - { - _leftButtonSr = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _buttonZl; - public GamepadInputId ButtonZl - { - get => _buttonZl; - set - { - _buttonZl = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _buttonA; - public GamepadInputId ButtonA - { - get => _buttonA; - set - { - _buttonA = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _buttonB; - public GamepadInputId ButtonB - { - get => _buttonB; - set - { - _buttonB = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _buttonX; - public GamepadInputId ButtonX - { - get => _buttonX; - set - { - _buttonX = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _buttonY; - public GamepadInputId ButtonY - { - get => _buttonY; - set - { - _buttonY = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _buttonR; - public GamepadInputId ButtonR - { - get => _buttonR; - set - { - _buttonR = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _buttonPlus; - public GamepadInputId ButtonPlus - { - get => _buttonPlus; - set - { - _buttonPlus = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _rightButtonSl; - public GamepadInputId RightButtonSl - { - get => _rightButtonSl; - set - { - _rightButtonSl = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _rightButtonSr; - public GamepadInputId RightButtonSr - { - get => _rightButtonSr; - set - { - _rightButtonSr = value; - OnPropertyChanged(); - } - } - - private GamepadInputId _buttonZr; - public GamepadInputId ButtonZr - { - get => _buttonZr; - set - { - _buttonZr = value; - OnPropertyChanged(); - } - } - - private float _deadzoneLeft; - public float DeadzoneLeft - { - get => _deadzoneLeft; - set - { - _deadzoneLeft = MathF.Round(value, 3); - OnPropertyChanged(); - } - } - - private float _deadzoneRight; - public float DeadzoneRight - { - get => _deadzoneRight; - set - { - _deadzoneRight = MathF.Round(value, 3); - OnPropertyChanged(); - } - } - - private float _rangeLeft; - public float RangeLeft - { - get => _rangeLeft; - set - { - _rangeLeft = MathF.Round(value, 3); - OnPropertyChanged(); - } - } - - private float _rangeRight; - public float RangeRight - { - get => _rangeRight; - set - { - _rangeRight = MathF.Round(value, 3); - OnPropertyChanged(); - } - } - - private float _triggerThreshold; - public float TriggerThreshold - { - get => _triggerThreshold; - set - { - _triggerThreshold = MathF.Round(value, 3); - OnPropertyChanged(); - } - } - - private bool _enableMotion; - public bool EnableMotion - { - get => _enableMotion; - set - { - _enableMotion = value; - OnPropertyChanged(); - } - } - - private bool _enableRumble; - public bool EnableRumble - { - get => _enableRumble; - set - { - _enableRumble = value; - OnPropertyChanged(); - } - } - - public ControllerInputConfig(InputConfig config) - { - if (config != null) - { - Id = config.Id; - ControllerType = config.ControllerType; - PlayerIndex = config.PlayerIndex; - - if (config is not StandardControllerInputConfig controllerInput) - { - return; - } - - LeftJoystick = controllerInput.LeftJoyconStick.Joystick; - LeftInvertStickX = controllerInput.LeftJoyconStick.InvertStickX; - LeftInvertStickY = controllerInput.LeftJoyconStick.InvertStickY; - LeftRotate90 = controllerInput.LeftJoyconStick.Rotate90CW; - LeftStickButton = controllerInput.LeftJoyconStick.StickButton; - - RightJoystick = controllerInput.RightJoyconStick.Joystick; - RightInvertStickX = controllerInput.RightJoyconStick.InvertStickX; - RightInvertStickY = controllerInput.RightJoyconStick.InvertStickY; - RightRotate90 = controllerInput.RightJoyconStick.Rotate90CW; - RightStickButton = controllerInput.RightJoyconStick.StickButton; - - DpadUp = controllerInput.LeftJoycon.DpadUp; - DpadDown = controllerInput.LeftJoycon.DpadDown; - DpadLeft = controllerInput.LeftJoycon.DpadLeft; - DpadRight = controllerInput.LeftJoycon.DpadRight; - ButtonL = controllerInput.LeftJoycon.ButtonL; - ButtonMinus = controllerInput.LeftJoycon.ButtonMinus; - LeftButtonSl = controllerInput.LeftJoycon.ButtonSl; - LeftButtonSr = controllerInput.LeftJoycon.ButtonSr; - ButtonZl = controllerInput.LeftJoycon.ButtonZl; - - ButtonA = controllerInput.RightJoycon.ButtonA; - ButtonB = controllerInput.RightJoycon.ButtonB; - ButtonX = controllerInput.RightJoycon.ButtonX; - ButtonY = controllerInput.RightJoycon.ButtonY; - ButtonR = controllerInput.RightJoycon.ButtonR; - ButtonPlus = controllerInput.RightJoycon.ButtonPlus; - RightButtonSl = controllerInput.RightJoycon.ButtonSl; - RightButtonSr = controllerInput.RightJoycon.ButtonSr; - ButtonZr = controllerInput.RightJoycon.ButtonZr; - - DeadzoneLeft = controllerInput.DeadzoneLeft; - DeadzoneRight = controllerInput.DeadzoneRight; - RangeLeft = controllerInput.RangeLeft; - RangeRight = controllerInput.RangeRight; - TriggerThreshold = controllerInput.TriggerThreshold; - - if (controllerInput.Motion != null) - { - EnableMotion = controllerInput.Motion.EnableMotion; - GyroDeadzone = controllerInput.Motion.GyroDeadzone; - Sensitivity = controllerInput.Motion.Sensitivity; - - if (controllerInput.Motion is CemuHookMotionConfigController cemuHook) - { - EnableCemuHookMotion = true; - DsuServerHost = cemuHook.DsuServerHost; - DsuServerPort = cemuHook.DsuServerPort; - Slot = cemuHook.Slot; - AltSlot = cemuHook.AltSlot; - MirrorInput = cemuHook.MirrorInput; - } - } - - if (controllerInput.Rumble != null) - { - EnableRumble = controllerInput.Rumble.EnableRumble; - WeakRumble = controllerInput.Rumble.WeakRumble; - StrongRumble = controllerInput.Rumble.StrongRumble; - } - } - } - - public InputConfig GetConfig() - { - var config = new StandardControllerInputConfig - { - Id = Id, - Backend = InputBackendType.GamepadSDL2, - PlayerIndex = PlayerIndex, - ControllerType = ControllerType, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = DpadUp, - DpadDown = DpadDown, - DpadLeft = DpadLeft, - DpadRight = DpadRight, - ButtonL = ButtonL, - ButtonMinus = ButtonMinus, - ButtonSl = LeftButtonSl, - ButtonSr = LeftButtonSr, - ButtonZl = ButtonZl - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = ButtonA, - ButtonB = ButtonB, - ButtonX = ButtonX, - ButtonY = ButtonY, - ButtonPlus = ButtonPlus, - ButtonSl = RightButtonSl, - ButtonSr = RightButtonSr, - ButtonR = ButtonR, - ButtonZr = ButtonZr - }, - LeftJoyconStick = new JoyconConfigControllerStick - { - Joystick = LeftJoystick, - InvertStickX = LeftInvertStickX, - InvertStickY = LeftInvertStickY, - Rotate90CW = LeftRotate90, - StickButton = LeftStickButton - }, - RightJoyconStick = new JoyconConfigControllerStick - { - Joystick = RightJoystick, - InvertStickX = RightInvertStickX, - InvertStickY = RightInvertStickY, - Rotate90CW = RightRotate90, - StickButton = RightStickButton - }, - Rumble = new RumbleConfigController - { - EnableRumble = EnableRumble, - WeakRumble = WeakRumble, - StrongRumble = StrongRumble - }, - Version = InputConfig.CurrentVersion, - DeadzoneLeft = DeadzoneLeft, - DeadzoneRight = DeadzoneRight, - RangeLeft = RangeLeft, - RangeRight = RangeRight, - TriggerThreshold = TriggerThreshold - }; - - if (EnableCemuHookMotion) - { - config.Motion = new CemuHookMotionConfigController - { - EnableMotion = EnableMotion, - MotionBackend = MotionInputBackendType.CemuHook, - GyroDeadzone = GyroDeadzone, - Sensitivity = Sensitivity, - DsuServerHost = DsuServerHost, - DsuServerPort = DsuServerPort, - Slot = Slot, - AltSlot = AltSlot, - MirrorInput = MirrorInput - }; - } - else - { - config.Motion = new MotionConfigController - { - EnableMotion = EnableMotion, - MotionBackend = MotionInputBackendType.GamepadDriver, - GyroDeadzone = GyroDeadzone, - Sensitivity = Sensitivity - }; - } - - return config; - } - } -} diff --git a/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs b/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs deleted file mode 100644 index 029565210..000000000 --- a/src/Ryujinx.Ava/UI/Models/Input/KeyboardInputConfig.cs +++ /dev/null @@ -1,422 +0,0 @@ -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Configuration.Hid.Keyboard; - -namespace Ryujinx.Ava.UI.Models.Input -{ - public class KeyboardInputConfig : BaseModel - { - public string Id { get; set; } - public ControllerType ControllerType { get; set; } - public PlayerIndex PlayerIndex { get; set; } - - private Key _leftStickUp; - public Key LeftStickUp - { - get => _leftStickUp; - set - { - _leftStickUp = value; - OnPropertyChanged(); - } - } - - private Key _leftStickDown; - public Key LeftStickDown - { - get => _leftStickDown; - set - { - _leftStickDown = value; - OnPropertyChanged(); - } - } - - private Key _leftStickLeft; - public Key LeftStickLeft - { - get => _leftStickLeft; - set - { - _leftStickLeft = value; - OnPropertyChanged(); - } - } - - private Key _leftStickRight; - public Key LeftStickRight - { - get => _leftStickRight; - set - { - _leftStickRight = value; - OnPropertyChanged(); - } - } - - private Key _leftStickButton; - public Key LeftStickButton - { - get => _leftStickButton; - set - { - _leftStickButton = value; - OnPropertyChanged(); - } - } - - private Key _rightStickUp; - public Key RightStickUp - { - get => _rightStickUp; - set - { - _rightStickUp = value; - OnPropertyChanged(); - } - } - - private Key _rightStickDown; - public Key RightStickDown - { - get => _rightStickDown; - set - { - _rightStickDown = value; - OnPropertyChanged(); - } - } - - private Key _rightStickLeft; - public Key RightStickLeft - { - get => _rightStickLeft; - set - { - _rightStickLeft = value; - OnPropertyChanged(); - } - } - - private Key _rightStickRight; - public Key RightStickRight - { - get => _rightStickRight; - set - { - _rightStickRight = value; - OnPropertyChanged(); - } - } - - private Key _rightStickButton; - public Key RightStickButton - { - get => _rightStickButton; - set - { - _rightStickButton = value; - OnPropertyChanged(); - } - } - - private Key _dpadUp; - public Key DpadUp - { - get => _dpadUp; - set - { - _dpadUp = value; - OnPropertyChanged(); - } - } - - private Key _dpadDown; - public Key DpadDown - { - get => _dpadDown; - set - { - _dpadDown = value; - OnPropertyChanged(); - } - } - - private Key _dpadLeft; - public Key DpadLeft - { - get => _dpadLeft; - set - { - _dpadLeft = value; - OnPropertyChanged(); - } - } - - private Key _dpadRight; - public Key DpadRight - { - get => _dpadRight; - set - { - _dpadRight = value; - OnPropertyChanged(); - } - } - - private Key _buttonL; - public Key ButtonL - { - get => _buttonL; - set - { - _buttonL = value; - OnPropertyChanged(); - } - } - - private Key _buttonMinus; - public Key ButtonMinus - { - get => _buttonMinus; - set - { - _buttonMinus = value; - OnPropertyChanged(); - } - } - - private Key _leftButtonSl; - public Key LeftButtonSl - { - get => _leftButtonSl; - set - { - _leftButtonSl = value; - OnPropertyChanged(); - } - } - - private Key _leftButtonSr; - public Key LeftButtonSr - { - get => _leftButtonSr; - set - { - _leftButtonSr = value; - OnPropertyChanged(); - } - } - - private Key _buttonZl; - public Key ButtonZl - { - get => _buttonZl; - set - { - _buttonZl = value; - OnPropertyChanged(); - } - } - - private Key _buttonA; - public Key ButtonA - { - get => _buttonA; - set - { - _buttonA = value; - OnPropertyChanged(); - } - } - - private Key _buttonB; - public Key ButtonB - { - get => _buttonB; - set - { - _buttonB = value; - OnPropertyChanged(); - } - } - - private Key _buttonX; - public Key ButtonX - { - get => _buttonX; - set - { - _buttonX = value; - OnPropertyChanged(); - } - } - - private Key _buttonY; - public Key ButtonY - { - get => _buttonY; - set - { - _buttonY = value; - OnPropertyChanged(); - } - } - - private Key _buttonR; - public Key ButtonR - { - get => _buttonR; - set - { - _buttonR = value; - OnPropertyChanged(); - } - } - - private Key _buttonPlus; - public Key ButtonPlus - { - get => _buttonPlus; - set - { - _buttonPlus = value; - OnPropertyChanged(); - } - } - - private Key _rightButtonSl; - public Key RightButtonSl - { - get => _rightButtonSl; - set - { - _rightButtonSl = value; - OnPropertyChanged(); - } - } - - private Key _rightButtonSr; - public Key RightButtonSr - { - get => _rightButtonSr; - set - { - _rightButtonSr = value; - OnPropertyChanged(); - } - } - - private Key _buttonZr; - public Key ButtonZr - { - get => _buttonZr; - set - { - _buttonZr = value; - OnPropertyChanged(); - } - } - - public KeyboardInputConfig(InputConfig config) - { - if (config != null) - { - Id = config.Id; - ControllerType = config.ControllerType; - PlayerIndex = config.PlayerIndex; - - if (config is not StandardKeyboardInputConfig keyboardConfig) - { - return; - } - - LeftStickUp = keyboardConfig.LeftJoyconStick.StickUp; - LeftStickDown = keyboardConfig.LeftJoyconStick.StickDown; - LeftStickLeft = keyboardConfig.LeftJoyconStick.StickLeft; - LeftStickRight = keyboardConfig.LeftJoyconStick.StickRight; - LeftStickButton = keyboardConfig.LeftJoyconStick.StickButton; - - RightStickUp = keyboardConfig.RightJoyconStick.StickUp; - RightStickDown = keyboardConfig.RightJoyconStick.StickDown; - RightStickLeft = keyboardConfig.RightJoyconStick.StickLeft; - RightStickRight = keyboardConfig.RightJoyconStick.StickRight; - RightStickButton = keyboardConfig.RightJoyconStick.StickButton; - - DpadUp = keyboardConfig.LeftJoycon.DpadUp; - DpadDown = keyboardConfig.LeftJoycon.DpadDown; - DpadLeft = keyboardConfig.LeftJoycon.DpadLeft; - DpadRight = keyboardConfig.LeftJoycon.DpadRight; - ButtonL = keyboardConfig.LeftJoycon.ButtonL; - ButtonMinus = keyboardConfig.LeftJoycon.ButtonMinus; - LeftButtonSl = keyboardConfig.LeftJoycon.ButtonSl; - LeftButtonSr = keyboardConfig.LeftJoycon.ButtonSr; - ButtonZl = keyboardConfig.LeftJoycon.ButtonZl; - - ButtonA = keyboardConfig.RightJoycon.ButtonA; - ButtonB = keyboardConfig.RightJoycon.ButtonB; - ButtonX = keyboardConfig.RightJoycon.ButtonX; - ButtonY = keyboardConfig.RightJoycon.ButtonY; - ButtonR = keyboardConfig.RightJoycon.ButtonR; - ButtonPlus = keyboardConfig.RightJoycon.ButtonPlus; - RightButtonSl = keyboardConfig.RightJoycon.ButtonSl; - RightButtonSr = keyboardConfig.RightJoycon.ButtonSr; - ButtonZr = keyboardConfig.RightJoycon.ButtonZr; - } - } - - public InputConfig GetConfig() - { - var config = new StandardKeyboardInputConfig - { - Id = Id, - Backend = InputBackendType.WindowKeyboard, - PlayerIndex = PlayerIndex, - ControllerType = ControllerType, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = DpadUp, - DpadDown = DpadDown, - DpadLeft = DpadLeft, - DpadRight = DpadRight, - ButtonL = ButtonL, - ButtonMinus = ButtonMinus, - ButtonZl = ButtonZl, - ButtonSl = LeftButtonSl, - ButtonSr = LeftButtonSr - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = ButtonA, - ButtonB = ButtonB, - ButtonX = ButtonX, - ButtonY = ButtonY, - ButtonPlus = ButtonPlus, - ButtonSl = RightButtonSl, - ButtonSr = RightButtonSr, - ButtonR = ButtonR, - ButtonZr = ButtonZr - }, - LeftJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = LeftStickUp, - StickDown = LeftStickDown, - StickRight = LeftStickRight, - StickLeft = LeftStickLeft, - StickButton = LeftStickButton - }, - RightJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = RightStickUp, - StickDown = RightStickDown, - StickLeft = RightStickLeft, - StickRight = RightStickRight, - StickButton = RightStickButton - }, - Version = InputConfig.CurrentVersion - }; - - return config; - } - } -} diff --git a/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs b/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs new file mode 100644 index 000000000..f1352c6d8 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Models/InputConfiguration.cs @@ -0,0 +1,456 @@ +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using System; + +namespace Ryujinx.Ava.UI.Models +{ + internal class InputConfiguration : BaseModel + { + private float _deadzoneRight; + private float _triggerThreshold; + private float _deadzoneLeft; + private double _gyroDeadzone; + private int _sensitivity; + private bool _enableMotion; + private float _weakRumble; + private float _strongRumble; + private float _rangeLeft; + private float _rangeRight; + + public InputBackendType Backend { get; set; } + + /// + /// Controller id + /// + public string Id { get; set; } + + /// + /// Controller's Type + /// + public ControllerType ControllerType { get; set; } + + /// + /// Player's Index for the controller + /// + public PlayerIndex PlayerIndex { get; set; } + + public TStick LeftJoystick { get; set; } + public bool LeftInvertStickX { get; set; } + public bool LeftInvertStickY { get; set; } + public bool RightRotate90 { get; set; } + public TKey LeftControllerStickButton { get; set; } + + public TStick RightJoystick { get; set; } + public bool RightInvertStickX { get; set; } + public bool RightInvertStickY { get; set; } + public bool LeftRotate90 { get; set; } + public TKey RightControllerStickButton { get; set; } + + public float DeadzoneLeft + { + get => _deadzoneLeft; + set + { + _deadzoneLeft = MathF.Round(value, 3); + + OnPropertyChanged(); + } + } + + public float RangeLeft + { + get => _rangeLeft; + set + { + _rangeLeft = MathF.Round(value, 3); + + OnPropertyChanged(); + } + } + + public float DeadzoneRight + { + get => _deadzoneRight; + set + { + _deadzoneRight = MathF.Round(value, 3); + + OnPropertyChanged(); + } + } + + public float RangeRight + { + get => _rangeRight; + set + { + _rangeRight = MathF.Round(value, 3); + + OnPropertyChanged(); + } + } + + public float TriggerThreshold + { + get => _triggerThreshold; + set + { + _triggerThreshold = MathF.Round(value, 3); + + OnPropertyChanged(); + } + } + + public MotionInputBackendType MotionBackend { get; set; } + + public TKey ButtonMinus { get; set; } + public TKey ButtonL { get; set; } + public TKey ButtonZl { get; set; } + public TKey LeftButtonSl { get; set; } + public TKey LeftButtonSr { get; set; } + public TKey DpadUp { get; set; } + public TKey DpadDown { get; set; } + public TKey DpadLeft { get; set; } + public TKey DpadRight { get; set; } + + public TKey ButtonPlus { get; set; } + public TKey ButtonR { get; set; } + public TKey ButtonZr { get; set; } + public TKey RightButtonSl { get; set; } + public TKey RightButtonSr { get; set; } + public TKey ButtonX { get; set; } + public TKey ButtonB { get; set; } + public TKey ButtonY { get; set; } + public TKey ButtonA { get; set; } + + public TKey LeftStickUp { get; set; } + public TKey LeftStickDown { get; set; } + public TKey LeftStickLeft { get; set; } + public TKey LeftStickRight { get; set; } + public TKey LeftKeyboardStickButton { get; set; } + + public TKey RightStickUp { get; set; } + public TKey RightStickDown { get; set; } + public TKey RightStickLeft { get; set; } + public TKey RightStickRight { get; set; } + public TKey RightKeyboardStickButton { get; set; } + + public int Sensitivity + { + get => _sensitivity; + set + { + _sensitivity = value; + + OnPropertyChanged(); + } + } + + public double GyroDeadzone + { + get => _gyroDeadzone; + set + { + _gyroDeadzone = Math.Round(value, 3); + + OnPropertyChanged(); + } + } + + public bool EnableMotion + { + get => _enableMotion; set + { + _enableMotion = value; + + OnPropertyChanged(); + } + } + + public bool EnableCemuHookMotion { get; set; } + public int Slot { get; set; } + public int AltSlot { get; set; } + public bool MirrorInput { get; set; } + public string DsuServerHost { get; set; } + public int DsuServerPort { get; set; } + + public bool EnableRumble { get; set; } + public float WeakRumble + { + get => _weakRumble; set + { + _weakRumble = value; + + OnPropertyChanged(); + } + } + public float StrongRumble + { + get => _strongRumble; set + { + _strongRumble = value; + + OnPropertyChanged(); + } + } + + public InputConfiguration(InputConfig config) + { + if (config != null) + { + Backend = config.Backend; + Id = config.Id; + ControllerType = config.ControllerType; + PlayerIndex = config.PlayerIndex; + + if (config is StandardKeyboardInputConfig keyboardConfig) + { + LeftStickUp = (TKey)(object)keyboardConfig.LeftJoyconStick.StickUp; + LeftStickDown = (TKey)(object)keyboardConfig.LeftJoyconStick.StickDown; + LeftStickLeft = (TKey)(object)keyboardConfig.LeftJoyconStick.StickLeft; + LeftStickRight = (TKey)(object)keyboardConfig.LeftJoyconStick.StickRight; + LeftKeyboardStickButton = (TKey)(object)keyboardConfig.LeftJoyconStick.StickButton; + + RightStickUp = (TKey)(object)keyboardConfig.RightJoyconStick.StickUp; + RightStickDown = (TKey)(object)keyboardConfig.RightJoyconStick.StickDown; + RightStickLeft = (TKey)(object)keyboardConfig.RightJoyconStick.StickLeft; + RightStickRight = (TKey)(object)keyboardConfig.RightJoyconStick.StickRight; + RightKeyboardStickButton = (TKey)(object)keyboardConfig.RightJoyconStick.StickButton; + + ButtonA = (TKey)(object)keyboardConfig.RightJoycon.ButtonA; + ButtonB = (TKey)(object)keyboardConfig.RightJoycon.ButtonB; + ButtonX = (TKey)(object)keyboardConfig.RightJoycon.ButtonX; + ButtonY = (TKey)(object)keyboardConfig.RightJoycon.ButtonY; + ButtonR = (TKey)(object)keyboardConfig.RightJoycon.ButtonR; + RightButtonSl = (TKey)(object)keyboardConfig.RightJoycon.ButtonSl; + RightButtonSr = (TKey)(object)keyboardConfig.RightJoycon.ButtonSr; + ButtonZr = (TKey)(object)keyboardConfig.RightJoycon.ButtonZr; + ButtonPlus = (TKey)(object)keyboardConfig.RightJoycon.ButtonPlus; + + DpadUp = (TKey)(object)keyboardConfig.LeftJoycon.DpadUp; + DpadDown = (TKey)(object)keyboardConfig.LeftJoycon.DpadDown; + DpadLeft = (TKey)(object)keyboardConfig.LeftJoycon.DpadLeft; + DpadRight = (TKey)(object)keyboardConfig.LeftJoycon.DpadRight; + ButtonMinus = (TKey)(object)keyboardConfig.LeftJoycon.ButtonMinus; + LeftButtonSl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSl; + LeftButtonSr = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSr; + ButtonZl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonZl; + ButtonL = (TKey)(object)keyboardConfig.LeftJoycon.ButtonL; + } + else if (config is StandardControllerInputConfig controllerConfig) + { + LeftJoystick = (TStick)(object)controllerConfig.LeftJoyconStick.Joystick; + LeftInvertStickX = controllerConfig.LeftJoyconStick.InvertStickX; + LeftInvertStickY = controllerConfig.LeftJoyconStick.InvertStickY; + LeftRotate90 = controllerConfig.LeftJoyconStick.Rotate90CW; + LeftControllerStickButton = (TKey)(object)controllerConfig.LeftJoyconStick.StickButton; + + RightJoystick = (TStick)(object)controllerConfig.RightJoyconStick.Joystick; + RightInvertStickX = controllerConfig.RightJoyconStick.InvertStickX; + RightInvertStickY = controllerConfig.RightJoyconStick.InvertStickY; + RightRotate90 = controllerConfig.RightJoyconStick.Rotate90CW; + RightControllerStickButton = (TKey)(object)controllerConfig.RightJoyconStick.StickButton; + + ButtonA = (TKey)(object)controllerConfig.RightJoycon.ButtonA; + ButtonB = (TKey)(object)controllerConfig.RightJoycon.ButtonB; + ButtonX = (TKey)(object)controllerConfig.RightJoycon.ButtonX; + ButtonY = (TKey)(object)controllerConfig.RightJoycon.ButtonY; + ButtonR = (TKey)(object)controllerConfig.RightJoycon.ButtonR; + RightButtonSl = (TKey)(object)controllerConfig.RightJoycon.ButtonSl; + RightButtonSr = (TKey)(object)controllerConfig.RightJoycon.ButtonSr; + ButtonZr = (TKey)(object)controllerConfig.RightJoycon.ButtonZr; + ButtonPlus = (TKey)(object)controllerConfig.RightJoycon.ButtonPlus; + + DpadUp = (TKey)(object)controllerConfig.LeftJoycon.DpadUp; + DpadDown = (TKey)(object)controllerConfig.LeftJoycon.DpadDown; + DpadLeft = (TKey)(object)controllerConfig.LeftJoycon.DpadLeft; + DpadRight = (TKey)(object)controllerConfig.LeftJoycon.DpadRight; + ButtonMinus = (TKey)(object)controllerConfig.LeftJoycon.ButtonMinus; + LeftButtonSl = (TKey)(object)controllerConfig.LeftJoycon.ButtonSl; + LeftButtonSr = (TKey)(object)controllerConfig.LeftJoycon.ButtonSr; + ButtonZl = (TKey)(object)controllerConfig.LeftJoycon.ButtonZl; + ButtonL = (TKey)(object)controllerConfig.LeftJoycon.ButtonL; + + DeadzoneLeft = controllerConfig.DeadzoneLeft; + DeadzoneRight = controllerConfig.DeadzoneRight; + RangeLeft = controllerConfig.RangeLeft; + RangeRight = controllerConfig.RangeRight; + TriggerThreshold = controllerConfig.TriggerThreshold; + + if (controllerConfig.Motion != null) + { + EnableMotion = controllerConfig.Motion.EnableMotion; + MotionBackend = controllerConfig.Motion.MotionBackend; + GyroDeadzone = controllerConfig.Motion.GyroDeadzone; + Sensitivity = controllerConfig.Motion.Sensitivity; + + if (controllerConfig.Motion is CemuHookMotionConfigController cemuHook) + { + EnableCemuHookMotion = true; + DsuServerHost = cemuHook.DsuServerHost; + DsuServerPort = cemuHook.DsuServerPort; + Slot = cemuHook.Slot; + AltSlot = cemuHook.AltSlot; + MirrorInput = cemuHook.MirrorInput; + } + + if (controllerConfig.Rumble != null) + { + EnableRumble = controllerConfig.Rumble.EnableRumble; + WeakRumble = controllerConfig.Rumble.WeakRumble; + StrongRumble = controllerConfig.Rumble.StrongRumble; + } + } + } + } + } + + public InputConfiguration() + { + } + + public InputConfig GetConfig() + { + if (Backend == InputBackendType.WindowKeyboard) + { + return new StandardKeyboardInputConfig + { + Id = Id, + Backend = Backend, + PlayerIndex = PlayerIndex, + ControllerType = ControllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = (Key)(object)DpadUp, + DpadDown = (Key)(object)DpadDown, + DpadLeft = (Key)(object)DpadLeft, + DpadRight = (Key)(object)DpadRight, + ButtonL = (Key)(object)ButtonL, + ButtonZl = (Key)(object)ButtonZl, + ButtonSl = (Key)(object)LeftButtonSl, + ButtonSr = (Key)(object)LeftButtonSr, + ButtonMinus = (Key)(object)ButtonMinus, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = (Key)(object)ButtonA, + ButtonB = (Key)(object)ButtonB, + ButtonX = (Key)(object)ButtonX, + ButtonY = (Key)(object)ButtonY, + ButtonPlus = (Key)(object)ButtonPlus, + ButtonSl = (Key)(object)RightButtonSl, + ButtonSr = (Key)(object)RightButtonSr, + ButtonR = (Key)(object)ButtonR, + ButtonZr = (Key)(object)ButtonZr, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = (Key)(object)LeftStickUp, + StickDown = (Key)(object)LeftStickDown, + StickRight = (Key)(object)LeftStickRight, + StickLeft = (Key)(object)LeftStickLeft, + StickButton = (Key)(object)LeftKeyboardStickButton, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = (Key)(object)RightStickUp, + StickDown = (Key)(object)RightStickDown, + StickLeft = (Key)(object)RightStickLeft, + StickRight = (Key)(object)RightStickRight, + StickButton = (Key)(object)RightKeyboardStickButton, + }, + Version = InputConfig.CurrentVersion, + }; + + } + + if (Backend == InputBackendType.GamepadSDL2) + { + var config = new StandardControllerInputConfig + { + Id = Id, + Backend = Backend, + PlayerIndex = PlayerIndex, + ControllerType = ControllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = (GamepadInputId)(object)DpadUp, + DpadDown = (GamepadInputId)(object)DpadDown, + DpadLeft = (GamepadInputId)(object)DpadLeft, + DpadRight = (GamepadInputId)(object)DpadRight, + ButtonL = (GamepadInputId)(object)ButtonL, + ButtonZl = (GamepadInputId)(object)ButtonZl, + ButtonSl = (GamepadInputId)(object)LeftButtonSl, + ButtonSr = (GamepadInputId)(object)LeftButtonSr, + ButtonMinus = (GamepadInputId)(object)ButtonMinus, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = (GamepadInputId)(object)ButtonA, + ButtonB = (GamepadInputId)(object)ButtonB, + ButtonX = (GamepadInputId)(object)ButtonX, + ButtonY = (GamepadInputId)(object)ButtonY, + ButtonPlus = (GamepadInputId)(object)ButtonPlus, + ButtonSl = (GamepadInputId)(object)RightButtonSl, + ButtonSr = (GamepadInputId)(object)RightButtonSr, + ButtonR = (GamepadInputId)(object)ButtonR, + ButtonZr = (GamepadInputId)(object)ButtonZr, + }, + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = (StickInputId)(object)LeftJoystick, + InvertStickX = LeftInvertStickX, + InvertStickY = LeftInvertStickY, + Rotate90CW = LeftRotate90, + StickButton = (GamepadInputId)(object)LeftControllerStickButton, + }, + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = (StickInputId)(object)RightJoystick, + InvertStickX = RightInvertStickX, + InvertStickY = RightInvertStickY, + Rotate90CW = RightRotate90, + StickButton = (GamepadInputId)(object)RightControllerStickButton, + }, + Rumble = new RumbleConfigController + { + EnableRumble = EnableRumble, + WeakRumble = WeakRumble, + StrongRumble = StrongRumble, + }, + Version = InputConfig.CurrentVersion, + DeadzoneLeft = DeadzoneLeft, + DeadzoneRight = DeadzoneRight, + RangeLeft = RangeLeft, + RangeRight = RangeRight, + TriggerThreshold = TriggerThreshold, + Motion = EnableCemuHookMotion + ? new CemuHookMotionConfigController + { + DsuServerHost = DsuServerHost, + DsuServerPort = DsuServerPort, + Slot = Slot, + AltSlot = AltSlot, + MirrorInput = MirrorInput, + MotionBackend = MotionInputBackendType.CemuHook, + } + : new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + }, + }; + + config.Motion.Sensitivity = Sensitivity; + config.Motion.EnableMotion = EnableMotion; + config.Motion.GyroDeadzone = GyroDeadzone; + + return config; + } + + return null; + } + } +} diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs similarity index 92% rename from src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs index ef8ffd50d..c0c625321 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/ControllerInputViewModel.cs @@ -8,7 +8,7 @@ using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Ava.UI.Views.Input; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -30,9 +30,9 @@ using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.Gamepad using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; using Key = Ryujinx.Common.Configuration.Hid.Key; -namespace Ryujinx.Ava.UI.ViewModels.Input +namespace Ryujinx.Ava.UI.ViewModels { - public class InputViewModel : BaseModel, IDisposable + public class ControllerInputViewModel : BaseModel, IDisposable { private const string Disabled = "disabled"; private const string ProControllerResource = "Ryujinx.Ui.Common/Resources/Controller_ProCon.svg"; @@ -48,7 +48,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input private int _controllerNumber; private string _controllerImage; private int _device; - private object _configViewModel; + private object _configuration; private string _profileName; private bool _isLoaded; @@ -71,14 +71,13 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public bool IsLeft { get; set; } public bool IsModified { get; set; } - public event Action NotifyChangesEvent; - public object ConfigViewModel + public object Configuration { - get => _configViewModel; + get => _configuration; set { - _configViewModel = value; + _configuration = value; OnPropertyChanged(); } @@ -233,7 +232,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public InputConfig Config { get; set; } - public InputViewModel(UserControl owner) : this() + public ControllerInputViewModel(UserControl owner) : this() { if (Program.PreviewerDetached) { @@ -245,6 +244,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input _mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; + _mainWindow.ViewModel.AppHost?.NpadManager.BlockInputUpdates(); _isLoaded = false; @@ -255,7 +255,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } - public InputViewModel() + public ControllerInputViewModel() { PlayerIndexes = new ObservableCollection(); Controllers = new ObservableCollection(); @@ -282,12 +282,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (Config is StandardKeyboardInputConfig keyboardInputConfig) { - ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig)); + Configuration = new InputConfiguration(keyboardInputConfig); } if (Config is StandardControllerInputConfig controllerInputConfig) { - ConfigViewModel = new ControllerInputViewModel(this, new ControllerInputConfig(controllerInputConfig)); + Configuration = new InputConfiguration(controllerInputConfig); } } @@ -323,6 +323,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } + public async void ShowMotionConfig() + { + await MotionInputView.Show(this); + } + + public async void ShowRumbleConfig() + { + await RumbleInputView.Show(this); + } + private void LoadInputDriver() { if (_device < 0) @@ -730,7 +740,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input return; } - if (ConfigViewModel == null) + if (Configuration == null) { return; } @@ -741,37 +751,35 @@ namespace Ryujinx.Ava.UI.ViewModels.Input return; } + + bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; + + if (validFileName) + { + string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); + + InputConfig config = null; + + if (IsKeyboard) + { + config = (Configuration as InputConfiguration).GetConfig(); + } + else if (IsController) + { + config = (Configuration as InputConfiguration).GetConfig(); + } + + config.ControllerType = Controllers[_controller].Type; + + string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig); + + await File.WriteAllTextAsync(path, jsonString); + + LoadProfiles(); + } else { - bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; - - if (validFileName) - { - string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); - - InputConfig config = null; - - if (IsKeyboard) - { - config = (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig(); - } - else if (IsController) - { - config = (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); - } - - config.ControllerType = Controllers[_controller].Type; - - string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig); - - await File.WriteAllTextAsync(path, jsonString); - - LoadProfiles(); - } - else - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); - } + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); } } @@ -822,18 +830,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (device.Type == DeviceType.Keyboard) { - var inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config; + var inputConfig = Configuration as InputConfiguration; inputConfig.Id = device.Id; } else { - var inputConfig = (ConfigViewModel as ControllerInputViewModel).Config; + var inputConfig = Configuration as InputConfiguration; inputConfig.Id = device.Id.Split(" ")[0]; } var config = !IsController - ? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig() - : (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); + ? (Configuration as InputConfiguration).GetConfig() + : (Configuration as InputConfiguration).GetConfig(); config.ControllerType = Controllers[_controller].Type; config.PlayerIndex = _playerId; @@ -864,13 +872,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public void NotifyChanges() { - OnPropertyChanged(nameof(ConfigViewModel)); + OnPropertyChanged(nameof(Configuration)); OnPropertyChanged(nameof(IsController)); OnPropertyChanged(nameof(ShowSettings)); OnPropertyChanged(nameof(IsKeyboard)); OnPropertyChanged(nameof(IsRight)); OnPropertyChanged(nameof(IsLeft)); - NotifyChangesEvent?.Invoke(); } public void Dispose() diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs deleted file mode 100644 index 0e23dfa76..000000000 --- a/src/Ryujinx.Ava/UI/ViewModels/Input/ControllerInputViewModel.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Avalonia.Svg.Skia; -using Ryujinx.Ava.UI.Models.Input; -using Ryujinx.Ava.UI.Views.Input; - -namespace Ryujinx.Ava.UI.ViewModels.Input -{ - public class ControllerInputViewModel : BaseModel - { - private ControllerInputConfig _config; - public ControllerInputConfig Config - { - get => _config; - set - { - _config = value; - OnPropertyChanged(); - } - } - - private bool _isLeft; - public bool IsLeft - { - get => _isLeft; - set - { - _isLeft = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(HasSides)); - } - } - - private bool _isRight; - public bool IsRight - { - get => _isRight; - set - { - _isRight = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(HasSides)); - } - } - - public bool HasSides => IsLeft ^ IsRight; - - private SvgImage _image; - public SvgImage Image - { - get => _image; - set - { - _image = value; - OnPropertyChanged(); - } - } - - public InputViewModel parentModel; - - public ControllerInputViewModel(InputViewModel model, ControllerInputConfig config) - { - parentModel = model; - model.NotifyChangesEvent += OnParentModelChanged; - OnParentModelChanged(); - Config = config; - } - - public async void ShowMotionConfig() - { - await MotionInputView.Show(this); - } - - public async void ShowRumbleConfig() - { - await RumbleInputView.Show(this); - } - - public void OnParentModelChanged() - { - IsLeft = parentModel.IsLeft; - IsRight = parentModel.IsRight; - Image = parentModel.Image; - } - } -} diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs deleted file mode 100644 index a93873063..000000000 --- a/src/Ryujinx.Ava/UI/ViewModels/Input/KeyboardInputViewModel.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Avalonia.Svg.Skia; -using Ryujinx.Ava.UI.Models.Input; - -namespace Ryujinx.Ava.UI.ViewModels.Input -{ - public class KeyboardInputViewModel : BaseModel - { - private KeyboardInputConfig _config; - public KeyboardInputConfig Config - { - get => _config; - set - { - _config = value; - OnPropertyChanged(); - } - } - - private bool _isLeft; - public bool IsLeft - { - get => _isLeft; - set - { - _isLeft = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(HasSides)); - } - } - - private bool _isRight; - public bool IsRight - { - get => _isRight; - set - { - _isRight = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(HasSides)); - } - } - - public bool HasSides => IsLeft ^ IsRight; - - private SvgImage _image; - public SvgImage Image - { - get => _image; - set - { - _image = value; - OnPropertyChanged(); - } - } - - public InputViewModel parentModel; - - public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config) - { - parentModel = model; - model.NotifyChangesEvent += OnParentModelChanged; - OnParentModelChanged(); - Config = config; - } - - public void OnParentModelChanged() - { - IsLeft = parentModel.IsLeft; - IsRight = parentModel.IsRight; - Image = parentModel.Image; - } - } -} diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs similarity index 97% rename from src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs index c9ed8f2d4..0b12a51f6 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/Input/MotionInputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MotionInputViewModel.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ava.UI.ViewModels.Input +namespace Ryujinx.Ava.UI.ViewModels { public class MotionInputViewModel : BaseModel { diff --git a/src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs similarity index 92% rename from src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs rename to src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs index 8ad33cf4c..49de19937 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/Input/RumbleInputViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/RumbleInputViewModel.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.Ava.UI.ViewModels.Input +namespace Ryujinx.Ava.UI.ViewModels { public class RumbleInputViewModel : BaseModel { diff --git a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml index 08bdf90f4..d636873a3 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/ControllerInputView.axaml @@ -1,11 +1,13 @@ @@ -33,10 +34,191 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Orientation="Vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MinHeight="450" + IsVisible="{Binding ShowSettings}"> @@ -75,9 +257,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerZL}" TextAlignment="Center" /> - + @@ -91,9 +273,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsTriggerL}" TextAlignment="Center" /> - + @@ -107,9 +289,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsButtonMinus}" TextAlignment="Center" /> - + @@ -129,8 +311,100 @@ Margin="0,0,0,10" HorizontalAlignment="Center" Text="{locale:Locale ControllerSettingsLStick}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -141,9 +415,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsStickButton}" TextAlignment="Center" /> - + @@ -158,22 +432,22 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsStickStick}" TextAlignment="Center" /> - + - + - + - + + Value="{ReflectionBinding Configuration.DeadzoneLeft, Mode=TwoWay}" /> + Text="{ReflectionBinding Configuration.DeadzoneLeft, StringFormat=\{0:0.00\}}" /> + Value="{ReflectionBinding Configuration.RangeLeft, Mode=TwoWay}" /> + Text="{ReflectionBinding Configuration.RangeLeft, StringFormat=\{0:0.00\}}" /> @@ -251,9 +525,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadUp}" TextAlignment="Center" /> - + @@ -268,9 +542,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadDown}" TextAlignment="Center" /> - + @@ -285,9 +559,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadLeft}" TextAlignment="Center" /> - + @@ -302,9 +576,9 @@ VerticalAlignment="Center" Text="{locale:Locale ControllerSettingsDPadRight}" TextAlignment="Center" /> - + @@ -317,13 +591,6 @@ Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - - + Value="{ReflectionBinding Configuration.TriggerThreshold, Mode=TwoWay}" /> + Text="{ReflectionBinding Configuration.TriggerThreshold, StringFormat=\{0:0.00\}}" /> - + + Text="{locale:Locale ControllerSettingsLeftSR}" + TextAlignment="Center" /> + - - - - - + + + + Text="{locale:Locale ControllerSettingsLeftSL}" + TextAlignment="Center" /> + - - - - - + + + + Text="{locale:Locale ControllerSettingsRightSR}" + TextAlignment="Center" /> + - - - - - + + + + Text="{locale:Locale ControllerSettingsRightSL}" + TextAlignment="Center" /> + - - - - + + + + HorizontalAlignment="Stretch" + IsVisible="{Binding IsController}"> @@ -449,7 +720,7 @@ Margin="10" MinWidth="0" Grid.Column="0" - IsChecked="{Binding Config.EnableMotion, Mode=TwoWay}"> + IsChecked="{ReflectionBinding Configuration.EnableMotion, Mode=TwoWay}"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs deleted file mode 100644 index 356381a8a..000000000 --- a/src/Ryujinx.Ava/UI/Views/Input/InputView.axaml.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Avalonia.Controls; -using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.ViewModels.Input; - -namespace Ryujinx.Ava.UI.Views.Input -{ - public partial class InputView : UserControl - { - private bool _dialogOpen; - private InputViewModel ViewModel { get; set; } - - public InputView() - { - DataContext = ViewModel = new InputViewModel(this); - - InitializeComponent(); - } - - public void SaveCurrentProfile() - { - ViewModel.Save(); - } - - private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (ViewModel.IsModified && !_dialogOpen) - { - _dialogOpen = true; - - var result = await ContentDialogHelper.CreateConfirmationDialog( - LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage], - LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage], - LocaleManager.Instance[LocaleKeys.InputDialogYes], - LocaleManager.Instance[LocaleKeys.InputDialogNo], - LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); - - if (result == UserResult.Yes) - { - ViewModel.Save(); - } - - _dialogOpen = false; - - ViewModel.IsModified = false; - - if (e.AddedItems.Count > 0) - { - var player = (PlayerModel)e.AddedItems[0]; - ViewModel.PlayerId = player.Id; - } - } - } - - public void Dispose() - { - ViewModel.Dispose(); - } - } -} diff --git a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml deleted file mode 100644 index e4566f463..000000000 --- a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml +++ /dev/null @@ -1,675 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs deleted file mode 100644 index f7024c5d1..000000000 --- a/src/Ryujinx.Ava/UI/Views/Input/KeyboardInputView.axaml.cs +++ /dev/null @@ -1,210 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.LogicalTree; -using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.ViewModels.Input; -using Ryujinx.Input; -using Ryujinx.Input.Assigner; - -namespace Ryujinx.Ava.UI.Views.Input -{ - public partial class KeyboardInputView : UserControl - { - private ButtonKeyAssigner _currentAssigner; - - public KeyboardInputView() - { - InitializeComponent(); - - foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) - { - if (visual is ToggleButton button and not CheckBox) - { - button.IsCheckedChanged += Button_IsCheckedChanged; - } - } - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - base.OnPointerReleased(e); - - if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver) - { - _currentAssigner.Cancel(); - } - } - - private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) - { - if (sender is ToggleButton button) - { - if ((bool)button.IsChecked) - { - if (_currentAssigner != null && button == _currentAssigner.ToggledButton) - { - return; - } - - bool isStick = button.Tag != null && button.Tag.ToString() == "stick"; - - if (_currentAssigner == null && (bool)button.IsChecked) - { - _currentAssigner = new ButtonKeyAssigner(button); - - this.Focus(NavigationMethod.Pointer); - - PointerPressed += MouseClick; - - IKeyboard keyboard = (IKeyboard)(DataContext as KeyboardInputViewModel).parentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. - IButtonAssigner assigner = CreateButtonAssigner(isStick); - - _currentAssigner.ButtonAssigned += (sender, e) => - { - if (e.ButtonValue.HasValue) - { - var viewModel = (DataContext as KeyboardInputViewModel); - var buttonValue = e.ButtonValue.Value; - viewModel.parentModel.IsModified = true; - - switch (button.Name) - { - case "ButtonZl": - viewModel.Config.ButtonZl = buttonValue.AsKey(); - break; - case "ButtonL": - viewModel.Config.ButtonL = buttonValue.AsKey(); - break; - case "ButtonMinus": - viewModel.Config.ButtonMinus = buttonValue.AsKey(); - break; - case "LeftStickButton": - viewModel.Config.LeftStickButton = buttonValue.AsKey(); - break; - case "LeftStickUp": - viewModel.Config.LeftStickUp = buttonValue.AsKey(); - break; - case "LeftStickDown": - viewModel.Config.LeftStickDown = buttonValue.AsKey(); - break; - case "LeftStickRight": - viewModel.Config.LeftStickRight = buttonValue.AsKey(); - break; - case "LeftStickLeft": - viewModel.Config.LeftStickLeft = buttonValue.AsKey(); - break; - case "DpadUp": - viewModel.Config.DpadUp = buttonValue.AsKey(); - break; - case "DpadDown": - viewModel.Config.DpadDown = buttonValue.AsKey(); - break; - case "DpadLeft": - viewModel.Config.DpadLeft = buttonValue.AsKey(); - break; - case "DpadRight": - viewModel.Config.DpadRight = buttonValue.AsKey(); - break; - case "LeftButtonSr": - viewModel.Config.LeftButtonSr = buttonValue.AsKey(); - break; - case "LeftButtonSl": - viewModel.Config.LeftButtonSl = buttonValue.AsKey(); - break; - case "RightButtonSr": - viewModel.Config.RightButtonSr = buttonValue.AsKey(); - break; - case "RightButtonSl": - viewModel.Config.RightButtonSl = buttonValue.AsKey(); - break; - case "ButtonZr": - viewModel.Config.ButtonZr = buttonValue.AsKey(); - break; - case "ButtonR": - viewModel.Config.ButtonR = buttonValue.AsKey(); - break; - case "ButtonPlus": - viewModel.Config.ButtonPlus = buttonValue.AsKey(); - break; - case "ButtonA": - viewModel.Config.ButtonA = buttonValue.AsKey(); - break; - case "ButtonB": - viewModel.Config.ButtonB = buttonValue.AsKey(); - break; - case "ButtonX": - viewModel.Config.ButtonX = buttonValue.AsKey(); - break; - case "ButtonY": - viewModel.Config.ButtonY = buttonValue.AsKey(); - break; - case "RightStickButton": - viewModel.Config.RightStickButton = buttonValue.AsKey(); - break; - case "RightStickUp": - viewModel.Config.RightStickUp = buttonValue.AsKey(); - break; - case "RightStickDown": - viewModel.Config.RightStickDown = buttonValue.AsKey(); - break; - case "RightStickRight": - viewModel.Config.RightStickRight = buttonValue.AsKey(); - break; - case "RightStickLeft": - viewModel.Config.RightStickLeft = buttonValue.AsKey(); - break; - } - } - }; - - _currentAssigner.GetInputAndAssign(assigner, keyboard); - } - else - { - if (_currentAssigner != null) - { - ToggleButton oldButton = _currentAssigner.ToggledButton; - - _currentAssigner.Cancel(); - _currentAssigner = null; - button.IsChecked = false; - } - } - } - else - { - _currentAssigner?.Cancel(); - _currentAssigner = null; - } - } - } - - private void MouseClick(object sender, PointerPressedEventArgs e) - { - bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed; - - _currentAssigner?.Cancel(shouldUnbind); - - PointerPressed -= MouseClick; - } - - private IButtonAssigner CreateButtonAssigner(bool forStick) - { - IButtonAssigner assigner; - - assigner = new KeyboardKeyAssigner((IKeyboard)(DataContext as KeyboardInputViewModel).parentModel.SelectedGamepad); - - return assigner; - } - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - _currentAssigner?.Cancel(); - _currentAssigner = null; - } - } -} diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml index 0d018e297..a6b587f67 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml @@ -6,7 +6,7 @@ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.MotionInputView" x:DataType="viewModels:MotionInputViewModel" diff --git a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs index 2304364b6..1b340752b 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Input/MotionInputView.axaml.cs @@ -1,7 +1,9 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid.Controller; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input @@ -17,7 +19,7 @@ namespace Ryujinx.Ava.UI.Views.Input public MotionInputView(ControllerInputViewModel viewModel) { - var config = viewModel.Config; + var config = viewModel.Configuration as InputConfiguration; _viewModel = new MotionInputViewModel { @@ -49,7 +51,7 @@ namespace Ryujinx.Ava.UI.Views.Input }; contentDialog.PrimaryButtonClick += (sender, args) => { - var config = viewModel.Config; + var config = viewModel.Configuration as InputConfiguration; config.Slot = content._viewModel.Slot; config.Sensitivity = content._viewModel.Sensitivity; config.GyroDeadzone = content._viewModel.GyroDeadzone; diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml index 1beb1f06e..5b7087a47 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d" x:Class="Ryujinx.Ava.UI.Views.Input.RumbleInputView" x:DataType="viewModels:RumbleInputViewModel" diff --git a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs index 58a4b416b..9307f872c 100644 --- a/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Input/RumbleInputView.axaml.cs @@ -1,7 +1,9 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Common.Configuration.Hid.Controller; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input @@ -17,7 +19,7 @@ namespace Ryujinx.Ava.UI.Views.Input public RumbleInputView(ControllerInputViewModel viewModel) { - var config = viewModel.Config; + var config = viewModel.Configuration as InputConfiguration; _viewModel = new RumbleInputViewModel { @@ -45,7 +47,7 @@ namespace Ryujinx.Ava.UI.Views.Input contentDialog.PrimaryButtonClick += (sender, args) => { - var config = viewModel.Config; + var config = viewModel.Configuration as InputConfiguration; config.StrongRumble = content._viewModel.StrongRumble; config.WeakRumble = content._viewModel.WeakRumble; }; diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml index 55c2ed6e3..81f4b68b7 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml @@ -27,9 +27,9 @@ - + Name="ControllerSettings" /> diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs index 85ccffccd..8a0cb8ab9 100644 --- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs @@ -11,7 +11,7 @@ namespace Ryujinx.Ava.UI.Views.Settings public void Dispose() { - InputView.Dispose(); + ControllerSettings.Dispose(); } } } diff --git a/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs index 314501c52..d7bb0b883 100644 --- a/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs @@ -37,7 +37,7 @@ namespace Ryujinx.Ava.UI.Windows public void SaveSettings() { - InputPage.InputView?.SaveCurrentProfile(); + InputPage.ControllerSettings?.SaveCurrentProfile(); if (Owner is MainWindow window && ViewModel.DirectoryChanged) { diff --git a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs index bf8319a6a..388ebcc07 100644 --- a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs @@ -59,16 +59,16 @@ namespace Ryujinx.Input.Assigner return _gamepad == null || !_gamepad.IsConnected; } - public ButtonValue? GetPressedButton() + public string GetPressedButton() { IEnumerable pressedButtons = _detector.GetPressedButtons(); if (pressedButtons.Any()) { - return !_forStick ? new(pressedButtons.First()) : new(((StickInputId)pressedButtons.First())); + return !_forStick ? pressedButtons.First().ToString() : ((StickInputId)pressedButtons.First()).ToString(); } - return null; + return ""; } private void CollectButtonStats() diff --git a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs index 653717133..76a9fece4 100644 --- a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs +++ b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs @@ -31,6 +31,6 @@ namespace Ryujinx.Input.Assigner /// Get the pressed button that was read in by the button assigner. /// /// The pressed button that was read - ButtonValue? GetPressedButton(); + string GetPressedButton(); } } diff --git a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs index c66812ba0..e52ef4a2c 100644 --- a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs +++ b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Input.Assigner public bool HasAnyButtonPressed() { - return GetPressedButton() is not null; + return GetPressedButton().Length != 0; } public bool ShouldCancel() @@ -31,20 +31,20 @@ namespace Ryujinx.Input.Assigner return _keyboardState.IsPressed(Key.Escape); } - public ButtonValue? GetPressedButton() + public string GetPressedButton() { - ButtonValue? keyPressed = null; + string keyPressed = ""; for (Key key = Key.Unknown; key < Key.Count; key++) { if (_keyboardState.IsPressed(key)) { - keyPressed = new(key); + keyPressed = key.ToString(); break; } } - return !ShouldCancel() ? keyPressed : null; + return !ShouldCancel() ? keyPressed : ""; } } } diff --git a/src/Ryujinx.Input/ButtonValue.cs b/src/Ryujinx.Input/ButtonValue.cs deleted file mode 100644 index f037e6b60..000000000 --- a/src/Ryujinx.Input/ButtonValue.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Diagnostics; - -namespace Ryujinx.Input -{ - public enum ButtonValueType { Key, GamepadButtonInputId, StickId } - - public readonly struct ButtonValue - { - private readonly ButtonValueType _type; - private readonly uint _rawValue; - - public ButtonValue(Key key) - { - _type = ButtonValueType.Key; - _rawValue = (uint)key; - } - - public ButtonValue(GamepadButtonInputId gamepad) - { - _type = ButtonValueType.GamepadButtonInputId; - _rawValue = (uint)gamepad; - } - - public ButtonValue(StickInputId stick) - { - _type = ButtonValueType.StickId; - _rawValue = (uint)stick; - } - - public Common.Configuration.Hid.Key AsKey() - { - Debug.Assert(_type == ButtonValueType.Key); - return (Common.Configuration.Hid.Key)_rawValue; - } - - public Common.Configuration.Hid.Controller.GamepadInputId AsGamepadButtonInputId() - { - Debug.Assert(_type == ButtonValueType.GamepadButtonInputId); - return (Common.Configuration.Hid.Controller.GamepadInputId)_rawValue; - } - - public Common.Configuration.Hid.Controller.StickInputId AsGamepadStickId() - { - Debug.Assert(_type == ButtonValueType.StickId); - return (Common.Configuration.Hid.Controller.StickInputId)_rawValue; - } - } -} diff --git a/src/Ryujinx/Ui/Windows/ControllerWindow.cs b/src/Ryujinx/Ui/Windows/ControllerWindow.cs index 52cad5c85..ebf22ab60 100644 --- a/src/Ryujinx/Ui/Windows/ControllerWindow.cs +++ b/src/Ryujinx/Ui/Windows/ControllerWindow.cs @@ -893,7 +893,7 @@ namespace Ryujinx.Ui.Windows } } - string pressedButton = assigner.GetPressedButton().ToString(); + string pressedButton = assigner.GetPressedButton(); Application.Invoke(delegate { From 33ba1703158564c2c3564fa329fd2e630f8a8e95 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Sun, 22 Oct 2023 15:31:36 -0300 Subject: [PATCH 08/40] Fix NRE on gather operations with depth compare on macOS (#5832) --- src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs index 5a231079a..55f7d5778 100644 --- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs +++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs @@ -766,7 +766,10 @@ namespace Ryujinx.Graphics.Shader.Instructions flags |= offset == TexOffset.Ptp ? TextureFlags.Offsets : TextureFlags.Offset; } - sourcesList.Add(Const((int)component)); + if (!hasDepthCompare) + { + sourcesList.Add(Const((int)component)); + } Operand[] sources = sourcesList.ToArray(); Operand[] dests = new Operand[BitOperations.PopCount((uint)componentMask)]; From d773d5152e685a164a6eb9f419873ef1908364f7 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sun, 22 Oct 2023 16:30:46 -0700 Subject: [PATCH 09/40] Update to LibHac 0.19.0 (#5831) * Update to LibHac v0.19.0 - PartitionFileSystem classes now fully match Nintendo's implementation. Current code creating a PartitionFileSystem now need to use the Initialize method. - Implementing nn::gcsrv and nn::sdmmcsrv now means the FS server now uses that abstraction instead of the old one where we passed in an IDeviceOperator. * Add GetFileSystemAttribute --- Directory.Packages.props | 2 +- src/Ryujinx.Ava/Common/ApplicationHelper.cs | 6 ++++-- .../DownloadableContentManagerViewModel.cs | 6 ++++-- .../UI/ViewModels/TitleUpdateViewModel.cs | 4 +++- src/Ryujinx.HLE/FileSystem/ContentManager.cs | 11 ++++++----- .../FileSystem/VirtualFileSystem.cs | 9 +++++---- src/Ryujinx.HLE/HOS/ModLoader.cs | 4 +++- .../FileSystemProxy/FileSystemProxyHelper.cs | 7 +++++-- .../Fs/FileSystemProxy/IFileSystem.cs | 11 +++++++++++ .../HOS/Services/Fs/IFileSystemProxy.cs | 5 ++++- .../PartitionFileSystemExtensions.cs | 9 +++++++-- .../Loaders/Processes/ProcessLoader.cs | 3 ++- .../Loaders/Processes/ProcessLoaderHelper.cs | 4 ++-- src/Ryujinx.Ui.Common/App/ApplicationData.cs | 6 ++++-- .../App/ApplicationLibrary.cs | 19 ++++++++++++------- .../Ui/Widgets/GameTableContextMenu.cs | 6 ++++-- src/Ryujinx/Ui/Windows/DlcWindow.cs | 6 ++++-- src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs | 3 ++- 18 files changed, 83 insertions(+), 38 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6fdaafddc..4fd079af6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + diff --git a/src/Ryujinx.Ava/Common/ApplicationHelper.cs b/src/Ryujinx.Ava/Common/ApplicationHelper.cs index b8cd06f3d..91ca8f4d5 100644 --- a/src/Ryujinx.Ava/Common/ApplicationHelper.cs +++ b/src/Ryujinx.Ava/Common/ApplicationHelper.cs @@ -173,7 +173,7 @@ namespace Ryujinx.Ava.Common string extension = Path.GetExtension(titleFilePath).ToLower(); if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") { - PartitionFileSystem pfs; + IFileSystem pfs; if (extension == ".xci") { @@ -181,7 +181,9 @@ namespace Ryujinx.Ava.Common } else { - pfs = new PartitionFileSystem(file.AsStorage()); + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; } foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index b88bd3d9c..cdecae77d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -126,7 +126,8 @@ namespace Ryujinx.Ava.UI.ViewModels { using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); - PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -232,7 +233,8 @@ namespace Ryujinx.Ava.UI.ViewModels using FileStream containerFile = File.OpenRead(path); - PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); bool containsDownloadableContent = false; _virtualFileSystem.ImportTickets(partitionFileSystem); diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs index dd0b92a51..5090a8c70 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs @@ -170,7 +170,9 @@ namespace Ryujinx.Ava.UI.ViewModels try { - (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, new PartitionFileSystem(file.AsStorage()), TitleId.ToString("x16"), 0); + var pfs = new PartitionFileSystem(); + pfs.Initialize(file.AsStorage()).ThrowIfFailure(); + (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0); if (controlNca != null && patchNca != null) { diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index 646808e78..8ade34a8b 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -238,7 +238,8 @@ namespace Ryujinx.HLE.FileSystem if (!mergedToContainer) { using FileStream fileStream = File.OpenRead(containerPath); - using PartitionFileSystem partitionFileSystem = new(fileStream.AsStorage()); + using PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(fileStream.AsStorage()).ThrowIfFailure(); _virtualFileSystem.ImportTickets(partitionFileSystem); } @@ -259,16 +260,16 @@ namespace Ryujinx.HLE.FileSystem { var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read); using var ncaFile = new UniqueRef(); - PartitionFileSystem pfs; switch (Path.GetExtension(aoc.ContainerPath)) { case ".xci": - pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); - pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); + var xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); break; case ".nsp": - pfs = new PartitionFileSystem(file.AsStorage()); + var pfs = new PartitionFileSystem(); + pfs.Initialize(file.AsStorage()); pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); break; default: diff --git a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs index 807020c60..eaf481dd7 100644 --- a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs +++ b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs @@ -7,6 +7,7 @@ using LibHac.Fs.Shim; using LibHac.FsSrv; using LibHac.FsSystem; using LibHac.Ncm; +using LibHac.Sdmmc; using LibHac.Spl; using LibHac.Tools.Es; using LibHac.Tools.Fs; @@ -32,7 +33,7 @@ namespace Ryujinx.HLE.FileSystem public KeySet KeySet { get; private set; } public EmulatedGameCard GameCard { get; private set; } - public EmulatedSdCard SdCard { get; private set; } + public SdmmcApi SdCard { get; private set; } public ModLoader ModLoader { get; private set; } private readonly ConcurrentDictionary _romFsByPid; @@ -198,15 +199,15 @@ namespace Ryujinx.HLE.FileSystem fsServerObjects.FsCreators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator(); GameCard = fsServerObjects.GameCard; - SdCard = fsServerObjects.SdCard; + SdCard = fsServerObjects.Sdmmc; - SdCard.SetSdCardInsertionStatus(true); + SdCard.SetSdCardInserted(true); var fsServerConfig = new FileSystemServerConfig { - DeviceOperator = fsServerObjects.DeviceOperator, ExternalKeySet = KeySet.ExternalKeySet, FsCreators = fsServerObjects.FsCreators, + StorageDeviceManagerFactory = fsServerObjects.StorageDeviceManagerFactory, RandomGenerator = randomGenerator, }; diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index 6706006c3..834bc0595 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -533,7 +533,9 @@ namespace Ryujinx.HLE.HOS Logger.Info?.Print(LogClass.ModLoader, "Using replacement ExeFS partition"); - exefs = new PartitionFileSystem(mods.ExefsContainers[0].Path.OpenRead().AsStorage()); + var pfs = new PartitionFileSystem(); + pfs.Initialize(mods.ExefsContainers[0].Path.OpenRead().AsStorage()).ThrowIfFailure(); + exefs = pfs; return true; } diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs index 599025e3b..1ef52a00d 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs @@ -26,7 +26,9 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy try { LocalStorage storage = new(pfsPath, FileAccess.Read, FileMode.Open); - using SharedRef nsp = new(new PartitionFileSystem(storage)); + var pfs = new PartitionFileSystem(); + using SharedRef nsp = new(pfs); + pfs.Initialize(storage).ThrowIfFailure(); ImportTitleKeysFromNsp(nsp.Get, context.Device.System.KeySet); @@ -90,7 +92,8 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy try { - PartitionFileSystem nsp = new(pfsFile.AsStorage()); + PartitionFileSystem nsp = new(); + nsp.Initialize(pfsFile.AsStorage()).ThrowIfFailure(); ImportTitleKeysFromNsp(nsp, context.Device.System.KeySet); diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs index 4c5c56240..66020d57b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs @@ -1,6 +1,7 @@ using LibHac; using LibHac.Common; using LibHac.Fs; +using LibHac.Fs.Fsa; using Path = LibHac.FsSrv.Sf.Path; namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy @@ -202,6 +203,16 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy return (ResultCode)result.Value; } + [CommandCmif(16)] + public ResultCode GetFileSystemAttribute(ServiceCtx context) + { + Result result = _fileSystem.Get.GetFileSystemAttribute(out FileSystemAttribute attribute); + + context.ResponseData.Write(SpanHelpers.AsReadOnlyByteSpan(in attribute)); + + return (ResultCode)result.Value; + } + protected override void Dispose(bool isDisposing) { if (isDisposing) diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs b/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs index 644e1a17a..24dd1e9be 100644 --- a/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs +++ b/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs @@ -1380,7 +1380,10 @@ namespace Ryujinx.HLE.HOS.Services.Fs [CommandCmif(1016)] public ResultCode FlushAccessLogOnSdCard(ServiceCtx context) { - return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value; + // Logging the access log to the SD card isn't implemented, meaning this function will be a no-op since + // there's nothing to flush. Return success until it's implemented. + // return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value; + return ResultCode.Success; } [CommandCmif(1017)] diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs index 6de99131e..50f7d5853 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs @@ -20,7 +20,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - internal static (bool, ProcessResult) TryLoad(this PartitionFileSystem partitionFileSystem, Switch device, string path, out string errorMessage) + internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, out string errorMessage) + where TMetaData : PartitionFileSystemMetaCore, new() + where TFormat : IPartitionFileSystemFormat + where THeader : unmanaged, IPartitionFileSystemHeader + where TEntry : unmanaged, IPartitionFileSystemEntry { errorMessage = null; @@ -91,7 +95,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; if (File.Exists(updatePath)) { - PartitionFileSystem updatePartitionFileSystem = new(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()); + PartitionFileSystem updatePartitionFileSystem = new(); + updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure(); device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem); diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 51cbb6f99..220b868db 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -69,7 +69,8 @@ namespace Ryujinx.HLE.Loaders.Processes public bool LoadNsp(string path) { FileStream file = new(path, FileMode.Open, FileAccess.Read); - PartitionFileSystem partitionFileSystem = new(file.AsStorage()); + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure(); (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage); diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index 292a5c122..c229b1742 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -1,8 +1,8 @@ using LibHac.Account; using LibHac.Common; using LibHac.Fs; +using LibHac.Fs.Fsa; using LibHac.Fs.Shim; -using LibHac.FsSystem; using LibHac.Loader; using LibHac.Ncm; using LibHac.Ns; @@ -33,7 +33,7 @@ namespace Ryujinx.HLE.Loaders.Processes // TODO: Remove this workaround when ASLR is implemented. private const ulong CodeStartOffset = 0x500000UL; - public static LibHac.Result RegisterProgramMapInfo(Switch device, PartitionFileSystem partitionFileSystem) + public static LibHac.Result RegisterProgramMapInfo(Switch device, IFileSystem partitionFileSystem) { ulong applicationId = 0; int programCount = 0; diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index e6130bdac..1be883ee1 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -65,7 +65,7 @@ namespace Ryujinx.Ui.App.Common if (extension is ".nsp" or ".xci") { - PartitionFileSystem pfs; + IFileSystem pfs; if (extension == ".xci") { @@ -75,7 +75,9 @@ namespace Ryujinx.Ui.App.Common } else { - pfs = new PartitionFileSystem(file.AsStorage()); + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; } foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 36b2b727d..2f688126a 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -174,7 +174,7 @@ namespace Ryujinx.Ui.App.Common { try { - PartitionFileSystem pfs; + IFileSystem pfs; bool isExeFs = false; @@ -186,7 +186,9 @@ namespace Ryujinx.Ui.App.Common } else { - pfs = new PartitionFileSystem(file.AsStorage()); + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. bool hasMainNca = false; @@ -500,7 +502,7 @@ namespace Ryujinx.Ui.App.Common ApplicationCountUpdated?.Invoke(null, e); } - private void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId) + private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId) { (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0); @@ -563,7 +565,7 @@ namespace Ryujinx.Ui.App.Common { try { - PartitionFileSystem pfs; + IFileSystem pfs; bool isExeFs = false; @@ -575,7 +577,9 @@ namespace Ryujinx.Ui.App.Common } else { - pfs = new PartitionFileSystem(file.AsStorage()); + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) { @@ -827,7 +831,7 @@ namespace Ryujinx.Ui.App.Common return false; } - public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex) + public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex) { Nca mainNca = null; Nca patchNca = null; @@ -931,7 +935,8 @@ namespace Ryujinx.Ui.App.Common if (File.Exists(updatePath)) { FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new(file.AsStorage()); + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); } diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index ea60421f8..5af181b08 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -211,7 +211,7 @@ namespace Ryujinx.Ui.Widgets (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) { - PartitionFileSystem pfs; + IFileSystem pfs; if (System.IO.Path.GetExtension(_titleFilePath) == ".xci") { @@ -221,7 +221,9 @@ namespace Ryujinx.Ui.Widgets } else { - pfs = new PartitionFileSystem(file.AsStorage()); + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; } foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.cs b/src/Ryujinx/Ui/Windows/DlcWindow.cs index 74aef00f4..9f7179467 100644 --- a/src/Ryujinx/Ui/Windows/DlcWindow.cs +++ b/src/Ryujinx/Ui/Windows/DlcWindow.cs @@ -88,7 +88,8 @@ namespace Ryujinx.Ui.Windows using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); - PartitionFileSystem pfs = new(containerFile.AsStorage()); + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); _virtualFileSystem.ImportTickets(pfs); @@ -153,7 +154,8 @@ namespace Ryujinx.Ui.Windows using FileStream containerFile = File.OpenRead(containerPath); - PartitionFileSystem pfs = new(containerFile.AsStorage()); + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); bool containsDlc = false; _virtualFileSystem.ImportTickets(pfs); diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs index 044f7e95a..51918eeab 100644 --- a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs +++ b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs @@ -90,7 +90,8 @@ namespace Ryujinx.Ui.Windows { using FileStream file = new(path, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new(file.AsStorage()); + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); try { From b1f8f868f6fdec87bd3342ac379594bd695cbbfd Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Mon, 23 Oct 2023 10:34:31 -0700 Subject: [PATCH 10/40] Fix the AOC manager using incorrect paths (#5840) * Fix the content manager using incorrect path for some AOC NCAs * Check Results in a few more places in the content manager --- src/Ryujinx.HLE/FileSystem/ContentManager.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index 8ade34a8b..724cb675c 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -198,7 +198,7 @@ namespace Ryujinx.HLE.FileSystem { using var ncaFile = new UniqueRef(); - fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read); + fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); if (nca.Header.ContentType != NcaContentType.Meta) { @@ -210,7 +210,7 @@ namespace Ryujinx.HLE.FileSystem using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel); using var cnmtFile = new UniqueRef(); - pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read); + pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); var cnmt = new Cnmt(cnmtFile.Get.AsStream()); if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId) @@ -220,7 +220,7 @@ namespace Ryujinx.HLE.FileSystem string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower(); - AddAocItem(cnmt.TitleId, containerPath, $"{ncaId}.nca", true); + AddAocItem(cnmt.TitleId, containerPath, $"/{ncaId}.nca", true); } } @@ -265,12 +265,12 @@ namespace Ryujinx.HLE.FileSystem { case ".xci": var xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); - xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); + xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); break; case ".nsp": var pfs = new PartitionFileSystem(); pfs.Initialize(file.AsStorage()); - pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); + pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); break; default: return false; // Print error? @@ -607,11 +607,11 @@ namespace Ryujinx.HLE.FileSystem if (filesystem.FileExists($"{path}/00")) { - filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode); + filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode).ThrowIfFailure(); } else { - filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode); + filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode).ThrowIfFailure(); } return file.Release(); From 56fe2ff535560a5b7e6461f5925479848b9a9994 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Tue, 24 Oct 2023 09:26:25 -0700 Subject: [PATCH 11/40] Fix loading tickets from a Sha256PartitionFileSystem (#5844) --- src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs index eaf481dd7..43bd27761 100644 --- a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs +++ b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs @@ -264,7 +264,16 @@ namespace Ryujinx.HLE.FileSystem if (result.IsSuccess()) { - Ticket ticket = new(ticketFile.Get.AsStream()); + // When reading a file from a Sha256PartitionFileSystem, you can't start a read in the middle + // of the hashed portion (usually the first 0x200 bytes) of the file and end the read after + // the end of the hashed portion, so we read the ticket file using a single read. + byte[] ticketData = new byte[0x2C0]; + result = ticketFile.Get.Read(out long bytesRead, 0, ticketData); + + if (result.IsFailure() || bytesRead != ticketData.Length) + continue; + + Ticket ticket = new(new MemoryStream(ticketData)); var titleKey = ticket.GetTitleKey(KeySet); if (titleKey != null) From 171b46ef49f479a5eb30b9769eab3af452092641 Mon Sep 17 00:00:00 2001 From: jcm Date: Tue, 24 Oct 2023 17:37:13 -0500 Subject: [PATCH 12/40] macOS: Use user-friendly macOS version string (#5838) * use user-friendly macOS version string rather than kernel version * add build identifier string --------- Co-authored-by: jcm --- src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs b/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs index 98a0d8abf..a968ad17b 100644 --- a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs +++ b/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs @@ -12,6 +12,13 @@ namespace Ryujinx.Common.SystemInfo { internal MacOSSystemInfo() { + if (SysctlByName("kern.osversion", out string buildRevision) != 0) + { + buildRevision = "Unknown Build"; + } + + OsDescription = $"macOS {Environment.OSVersion.Version} ({buildRevision}) ({RuntimeInformation.OSArchitecture})"; + string cpuName = GetCpuidCpuName(); if (cpuName == null && SysctlByName("machdep.cpu.brand_string", out cpuName) != 0) From c14ce4d2a5c9b373fb454906a6dc142c028d7be2 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:32:13 +0200 Subject: [PATCH 13/40] Add ldn_mitm as a network client for LDN (#5656) * Add relevant files from private repo Hopefully I didn't miss anything. JsonHelper.cs is a debug only change I only added line 810-812 in IUserLocalCommunicationService.cs for the new Spacemeowx2Ldn case. * Add a small README.md just for fun * Add note about NetCoreServer update to 5.1.0 * Fix a few issues Fix usage of wrong broadcast address Log warning if empty userstring was received and don't add them to outNetworkInfo * Add warning about incompatibility with public LDN version * Add missing changes from old_master * Adjust ldn_mitm for Ryujinx/Ryujinx#3805 * ldn: Adapt to changes from #4582 * ldn_mitm: First cleanup iteration * ldn_mitm: Second cleanup iteration * Credit spacemeowx2 in README.md * Address first review comments by AcK Adhere to Ryujinx coding style Remove leftover log calls Change category of a few log calls Remove leftover debug notes * Replace return type with void for methods always returning true * Address first review comments by riperiperi Purely stylistic changes: - Adhere to naming style for internal fields - Improve code formatting * Throw InvalidOperationException when calling wrong ldn proxy methods * Add missing newlines in LanDiscovery.Scan() * Fix Linux not receiving broadcast packets * Remove ILdnUdpSocket It's very unlikely that we will ever need a udp client. Thus we should simplify LanDiscovery initialization and remove the parameter of InitUdp(). * ldn_mitm: Improve formatting * fixup! Fix Linux not receiving broadcast packets By opening the udp server on 'LocalBroadcastAddr' Linux refused to answer packets going to LocalAddr. So in order to fix this problem, Linux now opens two LdnProxyUdpServers. * ldn_mitm: Fix assigning incorrect NodeIds This just made connecting a lot more reliable! Thanks @riperiperi * Fix node ids when leaving/joining * Change NodeId behaviour to work like RyuLdn * Change timing for accept and network info being reported. * Wait for connection before sending anything. * Remove ConnectAsync() from ILdnTcpSocket * Only broadcast scan responses if we're hosting a network. * Fix some filters, scan network duplication. * Fix silly mistake * Don't die on duplicates, just replace. * Lock around node updates These can happen from multiple threads. * ldn_mitm: Fix namespaces for Types Improve formatting Add warning if compression failed * Add quicker scan, forgetting networks that disappear. * Always force a network sync when updating AdvertiseData * Fix TCP frame size being too large for compressed frames * Allow ldn_mitm to pass -1 id for room localcommunicationids. * ldn_mitm: Match server socket options * ldn_mitm: Use correct socket options * ldn_mitm: Remove TCP broadcast socket options * config: Rename Spacemeowx2Ldn to LdnMitm * ldn_mitm: Generate random fake SSID * ldn_mitm: Adjust logging statements/levels * ldn_mitm: Add missing Stop() call for udp2 * ldn_mitm: Adjust formatting * ldn_mitm: Add stub comments and adjust existing ones * ldn: Add LdnConst class & set tx/rx buffer sizes correctly * Move LdnConst out of UserServiceCreator Replace a few values with LdnConsts * ldn: Adjust namespaces and client names * ldn_mitm: Adjust formatting * ldn: Rename RyuLdn to LdnRyu * Replace LanProtocol.Read() refs with scoped refs * Add MIT license for ldn_mitm * Clarify that network interface is also used for LDN Although it's currently only used by ldn_mitm, it would probably be more confusing to exclude RyuLdn there. * Fix giving a station node id 0 * Update Nuget packages * Remove LdnHelper * Add update functions for EnableInternetAccess setting * ldn: Log MultiplayerMode and DisableP2P * ldn: Adjust namespaces * Apply formatting * Conform to Ryujinx code style * Remove ldn_mitm from THIRDPARTY.md It shouldn't have been there in the first place. * Improve formatting --------- Co-authored-by: riperiperi Co-authored-by: Ac_K --- Directory.Packages.props | 1 + README.md | 1 + distribution/legal/THIRDPARTY.md | 2 +- src/Ryujinx.Ava/AppHost.cs | 6 + src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 +- .../Multiplayer/MultiplayerMode.cs | 1 + .../Utilities/NetworkHelpers.cs | 5 + src/Ryujinx.HLE/HLEConfiguration.cs | 2 +- src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs | 12 + .../HOS/Services/Ldn/Types/LdnNetworkInfo.cs | 2 +- .../HOS/Services/Ldn/Types/NodeInfo.cs | 2 +- .../Services/Ldn/Types/NodeLatestUpdate.cs | 4 +- .../HOS/Services/Ldn/Types/SecurityConfig.cs | 2 +- .../HOS/Services/Ldn/Types/Ssid.cs | 2 +- .../HOS/Services/Ldn/Types/UserConfig.cs | 2 +- .../Ldn/UserServiceCreator/AccessPoint.cs | 5 +- .../{RyuLdn => }/INetworkClient.cs | 7 +- .../IUserLocalCommunicationService.cs | 28 +- ...abledLdnClient.cs => LdnDisabledClient.cs} | 9 +- .../LdnMitm/LanDiscovery.cs | 611 ++++++++++++++++++ .../UserServiceCreator/LdnMitm/LanProtocol.cs | 314 +++++++++ .../LdnMitm/LdnMitmClient.cs | 104 +++ .../LdnMitm/Proxy/ILdnSocket.cs | 12 + .../LdnMitm/Proxy/ILdnTcpSocket.cs | 8 + .../LdnMitm/Proxy/LdnProxyTcpClient.cs | 99 +++ .../LdnMitm/Proxy/LdnProxyTcpServer.cs | 54 ++ .../LdnMitm/Proxy/LdnProxyTcpSession.cs | 83 +++ .../LdnMitm/Proxy/LdnProxyUdpServer.cs | 157 +++++ .../LdnMitm/Types/LanPacketHeader.cs | 16 + .../LdnMitm/Types/LanPacketType.cs | 10 + .../{RyuLdn => }/NetworkChangeEventArgs.cs | 2 +- .../Ldn/UserServiceCreator/Station.cs | 5 +- .../Types/ConnectPrivateRequest.cs | 2 +- .../{Network => }/Types/ConnectRequest.cs | 2 +- .../Types/CreateAccessPointPrivateRequest.cs | 2 +- .../Types/CreateAccessPointRequest.cs | 2 +- .../{RyuLdn => }/Types/NetworkError.cs | 2 +- .../{RyuLdn => }/Types/NetworkErrorMessage.cs | 2 +- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 1 + .../Configuration/ConfigurationState.cs | 1 + src/Ryujinx/Ui/MainWindow.cs | 8 + src/Ryujinx/Ui/Windows/SettingsWindow.cs | 2 + src/Ryujinx/Ui/Windows/SettingsWindow.glade | 5 +- 43 files changed, 1556 insertions(+), 43 deletions(-) create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/INetworkClient.cs (81%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn/DisabledLdnClient.cs => LdnDisabledClient.cs} (87%) create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/NetworkChangeEventArgs.cs (91%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/ConnectPrivateRequest.cs (86%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{Network => }/Types/ConnectRequest.cs (84%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/CreateAccessPointPrivateRequest.cs (88%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{Network => }/Types/CreateAccessPointRequest.cs (86%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/NetworkError.cs (80%) rename src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/{RyuLdn => }/Types/NetworkErrorMessage.cs (71%) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4fd079af6..009430f92 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,6 +24,7 @@ + diff --git a/README.md b/README.md index 56333278f..b2a6646f5 100644 --- a/README.md +++ b/README.md @@ -141,4 +141,5 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY - [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system. - [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation. +- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes. - [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation. diff --git a/distribution/legal/THIRDPARTY.md b/distribution/legal/THIRDPARTY.md index b0bd5a690..5caa03771 100644 --- a/distribution/legal/THIRDPARTY.md +++ b/distribution/legal/THIRDPARTY.md @@ -710,4 +710,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` - \ No newline at end of file + diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index c473cf562..cd066efba 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -190,6 +190,7 @@ namespace Ryujinx.Ava ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; + ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState; @@ -408,6 +409,11 @@ namespace Ryujinx.Ava }); } + private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs e) + { + Device.Configuration.EnableInternetAccess = e.NewValue; + } + private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs e) { Device.Configuration.MultiplayerLanInterfaceId = e.NewValue; diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index a67b796bd..62aac1227 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -650,7 +650,7 @@ "UserEditorTitle": "Edit User", "UserEditorTitleCreate": "Create User", "SettingsTabNetworkInterface": "Network Interface:", - "NetworkInterfaceTooltip": "The network interface used for LAN features", + "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features", "NetworkInterfaceDefault": "Default", "PackagingShaders": "Packaging Shaders", "AboutChangelogButton": "View Changelog on GitHub", diff --git a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs index 167429433..05108716d 100644 --- a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs +++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs @@ -3,5 +3,6 @@ public enum MultiplayerMode { Disabled, + LdnMitm, } } diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs index 78fb342b1..3b64a28f5 100644 --- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs +++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs @@ -74,5 +74,10 @@ namespace Ryujinx.Common.Utilities { return ConvertIpv4Address(IPAddress.Parse(ipAddress)); } + + public static IPAddress ConvertUint(uint ipAddress) + { + return new IPAddress(new byte[] { (byte)((ipAddress >> 24) & 0xFF), (byte)((ipAddress >> 16) & 0xFF), (byte)((ipAddress >> 8) & 0xFF), (byte)(ipAddress & 0xFF) }); + } } } diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index b1ba11b59..d52f1815a 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -101,7 +101,7 @@ namespace Ryujinx.HLE /// /// Control if the guest application should be told that there is a Internet connection available. /// - internal readonly bool EnableInternetAccess; + public bool EnableInternetAccess { internal get; set; } /// /// Control LibHac's integrity check level. diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs new file mode 100644 index 000000000..80ea2c9d7 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn +{ + static class LdnConst + { + public const int SsidLengthMax = 0x20; + public const int AdvertiseDataSizeMax = 0x180; + public const int UserNameBytesMax = 0x20; + public const int NodeCountMax = 8; + public const int StationCountMax = NodeCountMax - 1; + public const int PassphraseLengthMax = 0x40; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs index 4b7241c43..5fb2aca05 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs index c57a7dc45..9d5477931 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs index f33ceaebe..0461e783e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types @@ -48,7 +48,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Types { result[i].Reserved = new Array7(); - if (i < 8) + if (i < LdnConst.NodeCountMax) { result[i].State = array[i].State; array[i].State = NodeLatestUpdateFlags.None; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs index 85a19a875..5939a1394 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs index 72db4d41a..764862508 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs index 1401f5214..3820f936e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Memory; +using Ryujinx.Common.Memory; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs index 07bbbeda3..78ebcac82 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs @@ -1,7 +1,6 @@ using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator @@ -30,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _parent.NetworkClient.NetworkChange -= NetworkChanged; } - private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e) + private void NetworkChanged(object sender, NetworkChangeEventArgs e) { LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs similarity index 81% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs index ff342d27c..81825e977 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs @@ -1,12 +1,13 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { interface INetworkClient : IDisposable { + bool NeedsRealId { get; } + event EventHandler NetworkChange; void DisconnectNetwork(); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 29cc0e1b9..8c6ea66f7 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -8,7 +8,7 @@ using Ryujinx.Cpu; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; @@ -395,7 +395,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { - if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1) + if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId) { // TODO: Call nn::arp::GetApplicationControlProperty here when implemented. ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; @@ -546,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment? NetworkConfig networkConfig = context.RequestData.ReadStruct(); - if (networkConfig.IntentId.LocalCommunicationId == -1) + if (networkConfig.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId) { // TODO: Call nn::arp::GetApplicationControlProperty here when implemented. ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; @@ -555,7 +555,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId); - if (!isLocalCommunicationIdValid) + if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { return ResultCode.InvalidObject; } @@ -568,13 +568,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel); securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode); - if (networkConfig.NodeCountMax <= 8) + if (networkConfig.NodeCountMax <= LdnConst.NodeCountMax) { if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0) { if (securityConfig.SecurityMode <= SecurityMode.Retail) { - if (securityConfig.Passphrase.Length <= 0x40) + if (securityConfig.Passphrase.Length <= LdnConst.PassphraseLengthMax) { if (_state == NetworkState.AccessPoint) { @@ -678,7 +678,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator return _nifmResultCode; } - if (bufferSize == 0 || bufferSize > 0x180) + if (bufferSize == 0 || bufferSize > LdnConst.AdvertiseDataSizeMax) { return ResultCode.InvalidArgument; } @@ -848,10 +848,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator context.Memory.Read(bufferPosition, networkInfoBytes); - networkInfo = MemoryMarshal.Cast(networkInfoBytes)[0]; + networkInfo = MemoryMarshal.Read(networkInfoBytes); } - if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1) + if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId) { // TODO: Call nn::arp::GetApplicationControlProperty here when implemented. ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; @@ -860,7 +860,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId); - if (!isLocalCommunicationIdValid) + if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { return ResultCode.InvalidObject; } @@ -1061,10 +1061,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) { MultiplayerMode mode = context.Device.Configuration.MultiplayerMode; + + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initializing with multiplayer mode: {mode}"); + switch (mode) { + case MultiplayerMode.LdnMitm: + NetworkClient = new LdnMitmClient(context.Device.Configuration); + break; case MultiplayerMode.Disabled: - NetworkClient = new DisabledLdnClient(); + NetworkClient = new LdnDisabledClient(); break; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs similarity index 87% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs index 75a1e35ff..e5340b4e9 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs @@ -1,12 +1,13 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { - class DisabledLdnClient : INetworkClient + class LdnDisabledClient : INetworkClient { + public bool NeedsRealId => true; + public event EventHandler NetworkChange; public NetworkError Connect(ConnectRequest request) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs new file mode 100644 index 000000000..8cfd77acb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs @@ -0,0 +1,611 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm +{ + internal class LanDiscovery : IDisposable + { + private const int DefaultPort = 11452; + private const ushort CommonChannel = 6; + private const byte CommonLinkLevel = 3; + private const byte CommonNetworkType = 2; + + private const int FailureTimeout = 4000; + + private readonly LdnMitmClient _parent; + private readonly LanProtocol _protocol; + private bool _initialized; + private readonly Ssid _fakeSsid; + private ILdnTcpSocket _tcp; + private LdnProxyUdpServer _udp, _udp2; + private readonly List _stations = new(); + private readonly object _lock = new(); + + private readonly AutoResetEvent _apConnected = new(false); + + internal readonly IPAddress LocalAddr; + internal readonly IPAddress LocalBroadcastAddr; + internal NetworkInfo NetworkInfo; + + public bool IsHost => _tcp is LdnProxyTcpServer; + + private readonly Random _random = new(); + + // NOTE: Credit to https://stackoverflow.com/a/39338188 + private static IPAddress GetBroadcastAddress(IPAddress address, IPAddress mask) + { + uint ipAddress = BitConverter.ToUInt32(address.GetAddressBytes(), 0); + uint ipMaskV4 = BitConverter.ToUInt32(mask.GetAddressBytes(), 0); + uint broadCastIpAddress = ipAddress | ~ipMaskV4; + + return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); + } + + private static NetworkInfo GetEmptyNetworkInfo() + { + NetworkInfo networkInfo = new() + { + NetworkId = new NetworkId + { + SessionId = new Array16(), + }, + Common = new CommonNetworkInfo + { + MacAddress = new Array6(), + Ssid = new Ssid + { + Name = new Array33(), + }, + }, + Ldn = new LdnNetworkInfo + { + NodeCountMax = LdnConst.NodeCountMax, + SecurityParameter = new Array16(), + Nodes = new Array8(), + AdvertiseData = new Array384(), + Reserved4 = new Array140(), + }, + }; + + for (int i = 0; i < LdnConst.NodeCountMax; i++) + { + networkInfo.Ldn.Nodes[i] = new NodeInfo + { + MacAddress = new Array6(), + UserName = new Array33(), + Reserved2 = new Array16(), + }; + } + + return networkInfo; + } + + public LanDiscovery(LdnMitmClient parent, IPAddress ipAddress, IPAddress ipv4Mask) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initialize LanDiscovery using IP: {ipAddress}"); + + _parent = parent; + LocalAddr = ipAddress; + LocalBroadcastAddr = GetBroadcastAddress(ipAddress, ipv4Mask); + + _fakeSsid = new Ssid + { + Length = LdnConst.SsidLengthMax, + }; + _random.NextBytes(_fakeSsid.Name.AsSpan()[..32]); + + _protocol = new LanProtocol(this); + _protocol.Accept += OnConnect; + _protocol.SyncNetwork += OnSyncNetwork; + _protocol.DisconnectStation += DisconnectStation; + + NetworkInfo = GetEmptyNetworkInfo(); + + ResetStations(); + + if (!InitUdp()) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Initialize: InitUdp failed."); + + return; + } + + _initialized = true; + } + + protected void OnSyncNetwork(NetworkInfo info) + { + bool updated = false; + + lock (_lock) + { + if (!NetworkInfo.Equals(info)) + { + NetworkInfo = info; + updated = true; + + Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"Host IP: {NetworkHelpers.ConvertUint(info.Ldn.Nodes[0].Ipv4Address)}"); + } + } + + if (updated) + { + _parent.InvokeNetworkChange(info, true); + } + + _apConnected.Set(); + } + + protected void OnConnect(LdnProxyTcpSession station) + { + lock (_lock) + { + station.NodeId = LocateEmptyNode(); + + if (_stations.Count > LdnConst.StationCountMax || station.NodeId == -1) + { + station.Disconnect(); + station.Dispose(); + + return; + } + + _stations.Add(station); + + UpdateNodes(); + } + } + + public void DisconnectStation(LdnProxyTcpSession station) + { + if (!station.IsDisposed) + { + if (station.IsConnected) + { + station.Disconnect(); + } + + station.Dispose(); + } + + lock (_lock) + { + if (_stations.Remove(station)) + { + NetworkInfo.Ldn.Nodes[station.NodeId] = new NodeInfo() + { + MacAddress = new Array6(), + UserName = new Array33(), + Reserved2 = new Array16(), + }; + + UpdateNodes(); + } + } + } + + public bool SetAdvertiseData(byte[] data) + { + if (data.Length > LdnConst.AdvertiseDataSizeMax) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "AdvertiseData exceeds size limit."); + + return false; + } + + data.CopyTo(NetworkInfo.Ldn.AdvertiseData.AsSpan()); + NetworkInfo.Ldn.AdvertiseDataSize = (ushort)data.Length; + + // NOTE: Otherwise this results in SessionKeepFailed or MasterDisconnected + lock (_lock) + { + if (NetworkInfo.Ldn.Nodes[0].IsConnected == 1) + { + UpdateNodes(true); + } + } + + return true; + } + + public void InitNetworkInfo() + { + lock (_lock) + { + NetworkInfo.Common.MacAddress = GetFakeMac(); + NetworkInfo.Common.Channel = CommonChannel; + NetworkInfo.Common.LinkLevel = CommonLinkLevel; + NetworkInfo.Common.NetworkType = CommonNetworkType; + NetworkInfo.Common.Ssid = _fakeSsid; + + NetworkInfo.Ldn.Nodes = new Array8(); + + for (int i = 0; i < LdnConst.NodeCountMax; i++) + { + NetworkInfo.Ldn.Nodes[i].NodeId = (byte)i; + NetworkInfo.Ldn.Nodes[i].IsConnected = 0; + } + } + } + + protected Array6 GetFakeMac(IPAddress address = null) + { + address ??= LocalAddr; + + byte[] ip = address.GetAddressBytes(); + + var macAddress = new Array6(); + new byte[] { 0x02, 0x00, ip[0], ip[1], ip[2], ip[3] }.CopyTo(macAddress.AsSpan()); + + return macAddress; + } + + public bool InitTcp(bool listening, IPAddress address = null, int port = DefaultPort) + { + Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"LanDiscovery InitTcp: IP: {address}, listening: {listening}"); + + if (_tcp != null) + { + _tcp.DisconnectAndStop(); + _tcp.Dispose(); + _tcp = null; + } + + ILdnTcpSocket tcpSocket; + + if (listening) + { + try + { + address ??= LocalAddr; + + tcpSocket = new LdnProxyTcpServer(_protocol, address, port); + } + catch (Exception ex) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpServer: {ex}"); + + return false; + } + + if (!tcpSocket.Start()) + { + return false; + } + } + else + { + if (address == null) + { + return false; + } + + try + { + tcpSocket = new LdnProxyTcpClient(_protocol, address, port); + } + catch (Exception ex) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpClient: {ex}"); + + return false; + } + } + + _tcp = tcpSocket; + + return true; + } + + public bool InitUdp() + { + _udp?.Stop(); + _udp2?.Stop(); + + try + { + // NOTE: Linux won't receive any broadcast packets if the socket is not bound to the broadcast address. + // Windows only works if bound to localhost or the local address. + // See this discussion: https://stackoverflow.com/questions/13666789/receiving-udp-broadcast-packets-on-linux + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + _udp2 = new LdnProxyUdpServer(_protocol, LocalBroadcastAddr, DefaultPort); + } + + _udp = new LdnProxyUdpServer(_protocol, LocalAddr, DefaultPort); + } + catch (Exception ex) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyUdpServer: {ex}"); + + return false; + } + + return true; + } + + public NetworkInfo[] Scan(ushort channel, ScanFilter filter) + { + _udp.ClearScanResults(); + + if (_protocol.SendBroadcast(_udp, LanPacketType.Scan, DefaultPort) < 0) + { + return Array.Empty(); + } + + List outNetworkInfo = new(); + + foreach (KeyValuePair item in _udp.GetScanResults()) + { + bool copy = true; + + if (filter.Flag.HasFlag(ScanFilterFlag.LocalCommunicationId)) + { + copy &= filter.NetworkId.IntentId.LocalCommunicationId == item.Value.NetworkId.IntentId.LocalCommunicationId; + } + + if (filter.Flag.HasFlag(ScanFilterFlag.SessionId)) + { + copy &= filter.NetworkId.SessionId.AsSpan().SequenceEqual(item.Value.NetworkId.SessionId.AsSpan()); + } + + if (filter.Flag.HasFlag(ScanFilterFlag.NetworkType)) + { + copy &= filter.NetworkType == (NetworkType)item.Value.Common.NetworkType; + } + + if (filter.Flag.HasFlag(ScanFilterFlag.Ssid)) + { + Span gameSsid = item.Value.Common.Ssid.Name.AsSpan()[item.Value.Common.Ssid.Length..]; + Span scanSsid = filter.Ssid.Name.AsSpan()[filter.Ssid.Length..]; + copy &= gameSsid.SequenceEqual(scanSsid); + } + + if (filter.Flag.HasFlag(ScanFilterFlag.SceneId)) + { + copy &= filter.NetworkId.IntentId.SceneId == item.Value.NetworkId.IntentId.SceneId; + } + + if (copy) + { + if (item.Value.Ldn.Nodes[0].UserName[0] != 0) + { + outNetworkInfo.Add(item.Value); + } + else + { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Scan: Got empty Username. There might be a timing issue somewhere..."); + } + } + } + + return outNetworkInfo.ToArray(); + } + + protected void ResetStations() + { + lock (_lock) + { + foreach (LdnProxyTcpSession station in _stations) + { + station.Disconnect(); + station.Dispose(); + } + + _stations.Clear(); + } + } + + private int LocateEmptyNode() + { + Array8 nodes = NetworkInfo.Ldn.Nodes; + + for (int i = 1; i < nodes.Length; i++) + { + if (nodes[i].IsConnected == 0) + { + return i; + } + } + + return -1; + } + + protected void UpdateNodes(bool forceUpdate = false) + { + int countConnected = 1; + + foreach (LdnProxyTcpSession station in _stations.Where(station => station.IsConnected)) + { + countConnected++; + + station.OverrideInfo(); + + // NOTE: This is not part of the original implementation. + NetworkInfo.Ldn.Nodes[station.NodeId] = station.NodeInfo; + } + + byte nodeCount = (byte)countConnected; + + bool networkInfoChanged = forceUpdate || NetworkInfo.Ldn.NodeCount != nodeCount; + + NetworkInfo.Ldn.NodeCount = nodeCount; + + foreach (LdnProxyTcpSession station in _stations) + { + if (station.IsConnected) + { + if (_protocol.SendPacket(station, LanPacketType.SyncNetwork, SpanHelpers.AsSpan(ref NetworkInfo).ToArray()) < 0) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to send {LanPacketType.SyncNetwork} to station {station.NodeId}"); + } + } + } + + if (networkInfoChanged) + { + _parent.InvokeNetworkChange(NetworkInfo, true); + } + } + + protected NodeInfo GetNodeInfo(NodeInfo node, UserConfig userConfig, ushort localCommunicationVersion) + { + uint ipAddress = NetworkHelpers.ConvertIpv4Address(LocalAddr); + + node.MacAddress = GetFakeMac(); + node.IsConnected = 1; + node.UserName = userConfig.UserName; + node.LocalCommunicationVersion = localCommunicationVersion; + node.Ipv4Address = ipAddress; + + return node; + } + + public bool CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig) + { + if (!InitTcp(true)) + { + return false; + } + + InitNetworkInfo(); + + NetworkInfo.Ldn.NodeCountMax = networkConfig.NodeCountMax; + NetworkInfo.Ldn.SecurityMode = (ushort)securityConfig.SecurityMode; + + NetworkInfo.Common.Channel = networkConfig.Channel == 0 ? (ushort)6 : networkConfig.Channel; + + NetworkInfo.NetworkId.SessionId = new Array16(); + _random.NextBytes(NetworkInfo.NetworkId.SessionId.AsSpan()); + NetworkInfo.NetworkId.IntentId = networkConfig.IntentId; + + NetworkInfo.Ldn.Nodes[0] = GetNodeInfo(NetworkInfo.Ldn.Nodes[0], userConfig, networkConfig.LocalCommunicationVersion); + NetworkInfo.Ldn.Nodes[0].IsConnected = 1; + NetworkInfo.Ldn.NodeCount++; + + _parent.InvokeNetworkChange(NetworkInfo, true); + + return true; + } + + public void DestroyNetwork() + { + if (_tcp != null) + { + try + { + _tcp.DisconnectAndStop(); + } + finally + { + _tcp.Dispose(); + _tcp = null; + } + } + + ResetStations(); + } + + public NetworkError Connect(NetworkInfo networkInfo, UserConfig userConfig, uint localCommunicationVersion) + { + _apConnected.Reset(); + + if (networkInfo.Ldn.NodeCount == 0) + { + return NetworkError.Unknown; + } + + IPAddress address = NetworkHelpers.ConvertUint(networkInfo.Ldn.Nodes[0].Ipv4Address); + + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Connecting to host: {address}"); + + if (!InitTcp(false, address)) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Could not initialize TCPClient"); + + return NetworkError.ConnectNotFound; + } + + if (!_tcp.Connect()) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Failed to connect."); + + return NetworkError.ConnectFailure; + } + + NodeInfo myNode = GetNodeInfo(new NodeInfo(), userConfig, (ushort)localCommunicationVersion); + if (_protocol.SendPacket(_tcp, LanPacketType.Connect, SpanHelpers.AsSpan(ref myNode).ToArray()) < 0) + { + return NetworkError.Unknown; + } + + return _apConnected.WaitOne(FailureTimeout) ? NetworkError.None : NetworkError.ConnectTimeout; + } + + public void Dispose() + { + if (_initialized) + { + DisconnectAndStop(); + ResetStations(); + _initialized = false; + } + + _protocol.Accept -= OnConnect; + _protocol.SyncNetwork -= OnSyncNetwork; + _protocol.DisconnectStation -= DisconnectStation; + } + + public void DisconnectAndStop() + { + if (_udp != null) + { + try + { + _udp.Stop(); + } + finally + { + _udp.Dispose(); + _udp = null; + } + } + + if (_udp2 != null) + { + try + { + _udp2.Stop(); + } + finally + { + _udp2.Dispose(); + _udp2 = null; + } + } + + if (_tcp != null) + { + try + { + _tcp.DisconnectAndStop(); + } + finally + { + _tcp.Dispose(); + _tcp = null; + } + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs new file mode 100644 index 000000000..f22e430bd --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs @@ -0,0 +1,314 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm +{ + internal class LanProtocol + { + private const uint LanMagic = 0x11451400; + + public const int BufferSize = 2048; + public const int TcpTxBufferSize = 0x800; + public const int TcpRxBufferSize = 0x1000; + public const int TxBufferSizeMax = 0x2000; + public const int RxBufferSizeMax = 0x2000; + + private readonly int _headerSize = Marshal.SizeOf(); + + private readonly LanDiscovery _discovery; + + public event Action Accept; + public event Action Scan; + public event Action ScanResponse; + public event Action SyncNetwork; + public event Action Connect; + public event Action DisconnectStation; + + public LanProtocol(LanDiscovery parent) + { + _discovery = parent; + } + + public void InvokeAccept(LdnProxyTcpSession session) + { + Accept?.Invoke(session); + } + + public void InvokeDisconnectStation(LdnProxyTcpSession session) + { + DisconnectStation?.Invoke(session); + } + + private void DecodeAndHandle(LanPacketHeader header, byte[] data, EndPoint endPoint = null) + { + switch (header.Type) + { + case LanPacketType.Scan: + // UDP + if (_discovery.IsHost) + { + Scan?.Invoke(endPoint, LanPacketType.ScanResponse, SpanHelpers.AsSpan(ref _discovery.NetworkInfo).ToArray()); + } + break; + case LanPacketType.ScanResponse: + // UDP + ScanResponse?.Invoke(MemoryMarshal.Cast(data)[0]); + break; + case LanPacketType.SyncNetwork: + // TCP + SyncNetwork?.Invoke(MemoryMarshal.Cast(data)[0]); + break; + case LanPacketType.Connect: + // TCP Session / Station + Connect?.Invoke(MemoryMarshal.Cast(data)[0], endPoint); + break; + default: + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decode error: Unhandled type {header.Type}"); + break; + } + } + + public void Read(scoped ref byte[] buffer, scoped ref int bufferEnd, byte[] data, int offset, int size, EndPoint endPoint = null) + { + if (endPoint != null && _discovery.LocalAddr.Equals(((IPEndPoint)endPoint).Address)) + { + return; + } + + int index = 0; + while (index < size) + { + if (bufferEnd < _headerSize) + { + int copyable2 = Math.Min(size - index, Math.Min(size, _headerSize - bufferEnd)); + + Array.Copy(data, index + offset, buffer, bufferEnd, copyable2); + + index += copyable2; + bufferEnd += copyable2; + } + + if (bufferEnd >= _headerSize) + { + LanPacketHeader header = MemoryMarshal.Cast(buffer)[0]; + if (header.Magic != LanMagic) + { + bufferEnd = 0; + + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, $"Invalid magic number in received packet. [magic: {header.Magic}] [EP: {endPoint}]"); + + return; + } + + int totalSize = _headerSize + header.Length; + if (totalSize > BufferSize) + { + bufferEnd = 0; + + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Max packet size {BufferSize} exceeded."); + + return; + } + + int copyable = Math.Min(size - index, Math.Min(size, totalSize - bufferEnd)); + + Array.Copy(data, index + offset, buffer, bufferEnd, copyable); + + index += copyable; + bufferEnd += copyable; + + if (totalSize == bufferEnd) + { + byte[] ldnData = new byte[totalSize - _headerSize]; + Array.Copy(buffer, _headerSize, ldnData, 0, ldnData.Length); + + if (header.Compressed == 1) + { + if (Decompress(ldnData, out byte[] decompressedLdnData) != 0) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error:\n {header}, {_headerSize}\n {ldnData}, {ldnData.Length}"); + + return; + } + + if (decompressedLdnData.Length != header.DecompressLength) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error: length does not match. ({decompressedLdnData.Length} != {header.DecompressLength})"); + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error data: '{string.Join("", decompressedLdnData.Select(x => (int)x).ToArray())}'"); + + return; + } + + ldnData = decompressedLdnData; + } + + DecodeAndHandle(header, ldnData, endPoint); + + bufferEnd = 0; + } + } + } + } + + public int SendBroadcast(ILdnSocket s, LanPacketType type, int port) + { + return SendPacket(s, type, Array.Empty(), new IPEndPoint(_discovery.LocalBroadcastAddr, port)); + } + + public int SendPacket(ILdnSocket s, LanPacketType type, byte[] data, EndPoint endPoint = null) + { + byte[] buf = PreparePacket(type, data); + + return s.SendPacketAsync(endPoint, buf) ? 0 : -1; + } + + public int SendPacket(LdnProxyTcpSession s, LanPacketType type, byte[] data) + { + byte[] buf = PreparePacket(type, data); + + return s.SendAsync(buf) ? 0 : -1; + } + + private LanPacketHeader PrepareHeader(LanPacketHeader header, LanPacketType type) + { + header.Magic = LanMagic; + header.Type = type; + header.Compressed = 0; + header.Length = 0; + header.DecompressLength = 0; + header.Reserved = new Array2(); + + return header; + } + + private byte[] PreparePacket(LanPacketType type, byte[] data) + { + LanPacketHeader header = PrepareHeader(new LanPacketHeader(), type); + header.Length = (ushort)data.Length; + + byte[] buf; + if (data.Length > 0) + { + if (Compress(data, out byte[] compressed) == 0) + { + header.DecompressLength = header.Length; + header.Length = (ushort)compressed.Length; + header.Compressed = 1; + + buf = new byte[compressed.Length + _headerSize]; + + SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0); + compressed.CopyTo(buf, _headerSize); + } + else + { + buf = new byte[data.Length + _headerSize]; + + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Compressing packet data failed."); + + SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0); + data.CopyTo(buf, _headerSize); + } + } + else + { + buf = new byte[_headerSize]; + SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0); + } + + return buf; + } + + private int Compress(byte[] input, out byte[] output) + { + List outputList = new(); + int i = 0; + int maxCount = 0xFF; + + while (i < input.Length) + { + byte inputByte = input[i++]; + int count = 0; + + if (inputByte == 0) + { + while (i < input.Length && input[i] == 0 && count < maxCount) + { + count += 1; + i++; + } + } + + if (inputByte == 0) + { + outputList.Add(0); + + if (outputList.Count == BufferSize) + { + output = null; + + return -1; + } + + outputList.Add((byte)count); + } + else + { + outputList.Add(inputByte); + } + } + + output = outputList.ToArray(); + + return i == input.Length ? 0 : -1; + } + + private int Decompress(byte[] input, out byte[] output) + { + List outputList = new(); + int i = 0; + + while (i < input.Length && outputList.Count < BufferSize) + { + byte inputByte = input[i++]; + + outputList.Add(inputByte); + + if (inputByte == 0) + { + if (i == input.Length) + { + output = null; + + return -1; + } + + int count = input[i++]; + + for (int j = 0; j < count; j++) + { + if (outputList.Count == BufferSize) + { + break; + } + + outputList.Add(inputByte); + } + } + } + + output = outputList.ToArray(); + + return i == input.Length ? 0 : -1; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs new file mode 100644 index 000000000..068013053 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs @@ -0,0 +1,104 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Net.NetworkInformation; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm +{ + /// + /// Client implementation for ldn_mitm + /// + internal class LdnMitmClient : INetworkClient + { + public bool NeedsRealId => false; + + public event EventHandler NetworkChange; + + private readonly LanDiscovery _lanDiscovery; + + public LdnMitmClient(HLEConfiguration config) + { + UnicastIPAddressInformation localIpInterface = NetworkHelpers.GetLocalInterface(config.MultiplayerLanInterfaceId).Item2; + + _lanDiscovery = new LanDiscovery(this, localIpInterface.Address, localIpInterface.IPv4Mask); + } + + internal void InvokeNetworkChange(NetworkInfo info, bool connected, DisconnectReason reason = DisconnectReason.None) + { + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, connected: connected, disconnectReason: reason)); + } + + public NetworkError Connect(ConnectRequest request) + { + return _lanDiscovery.Connect(request.NetworkInfo, request.UserConfig, request.LocalCommunicationVersion); + } + + public NetworkError ConnectPrivate(ConnectPrivateRequest request) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient ConnectPrivate"); + + return NetworkError.None; + } + + public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) + { + return _lanDiscovery.CreateNetwork(request.SecurityConfig, request.UserConfig, request.NetworkConfig); + } + + public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient CreateNetworkPrivate"); + + return true; + } + + public void DisconnectAndStop() + { + _lanDiscovery.DisconnectAndStop(); + } + + public void DisconnectNetwork() + { + _lanDiscovery.DestroyNetwork(); + } + + public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient Reject"); + + return ResultCode.Success; + } + + public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) + { + return _lanDiscovery.Scan(channel, scanFilter); + } + + public void SetAdvertiseData(byte[] data) + { + _lanDiscovery.SetAdvertiseData(data); + } + + public void SetGameVersion(byte[] versionString) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetGameVersion"); + } + + public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy) + { + // NOTE: This method is not implemented in ldn_mitm + Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetStationAcceptPolicy"); + } + + public void Dispose() + { + _lanDiscovery.Dispose(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs new file mode 100644 index 000000000..b6e6cea9e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs @@ -0,0 +1,12 @@ +using System; +using System.Net; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal interface ILdnSocket : IDisposable + { + bool SendPacketAsync(EndPoint endpoint, byte[] buffer); + bool Start(); + bool Stop(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs new file mode 100644 index 000000000..97e3bd627 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal interface ILdnTcpSocket : ILdnSocket + { + bool Connect(); + void DisconnectAndStop(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs new file mode 100644 index 000000000..cfe9a8aae --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs @@ -0,0 +1,99 @@ +using Ryujinx.Common.Logging; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyTcpClient : NetCoreServer.TcpClient, ILdnTcpSocket + { + private readonly LanProtocol _protocol; + private byte[] _buffer; + private int _bufferEnd; + + public LdnProxyTcpClient(LanProtocol protocol, IPAddress address, int port) : base(address, port) + { + _protocol = protocol; + _buffer = new byte[LanProtocol.BufferSize]; + OptionSendBufferSize = LanProtocol.TcpTxBufferSize; + OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize; + OptionSendBufferLimit = LanProtocol.TxBufferSizeMax; + OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax; + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient connected!"); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size); + } + + public void DisconnectAndStop() + { + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + } + + public bool SendPacketAsync(EndPoint endPoint, byte[] data) + { + if (endPoint != null) + { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTcpClient is sending a packet but endpoint is not null."); + } + + if (IsConnecting && !IsConnected) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPClient needs to connect before sending packets. Waiting..."); + + while (IsConnecting && !IsConnected) + { + Thread.Yield(); + } + } + + return SendAsync(data); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient caught an error with code {error}"); + } + + protected override void Dispose(bool disposingManagedResources) + { + DisconnectAndStop(); + base.Dispose(disposingManagedResources); + } + + public override bool Connect() + { + // TODO: NetCoreServer has a Connect() method, but it currently leads to weird issues. + base.ConnectAsync(); + + while (IsConnecting) + { + Thread.Sleep(1); + } + + return IsConnected; + } + + public bool Start() + { + throw new InvalidOperationException("Start was called."); + } + + public bool Stop() + { + throw new InvalidOperationException("Stop was called."); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs new file mode 100644 index 000000000..0ca12b9f6 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs @@ -0,0 +1,54 @@ +using NetCoreServer; +using Ryujinx.Common.Logging; +using System; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyTcpServer : TcpServer, ILdnTcpSocket + { + private readonly LanProtocol _protocol; + + public LdnProxyTcpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port) + { + _protocol = protocol; + OptionReuseAddress = true; + OptionSendBufferSize = LanProtocol.TcpTxBufferSize; + OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize; + + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer created a server for this address: {address}:{port}"); + } + + protected override TcpSession CreateSession() + { + return new LdnProxyTcpSession(this, _protocol); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer caught an error with code {error}"); + } + + protected override void Dispose(bool disposingManagedResources) + { + Stop(); + base.Dispose(disposingManagedResources); + } + + public bool Connect() + { + throw new InvalidOperationException("Connect was called."); + } + + public void DisconnectAndStop() + { + Stop(); + } + + public bool SendPacketAsync(EndPoint endpoint, byte[] buffer) + { + throw new InvalidOperationException("SendPacketAsync was called."); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs new file mode 100644 index 000000000..f30c4b011 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs @@ -0,0 +1,83 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyTcpSession : NetCoreServer.TcpSession + { + private readonly LanProtocol _protocol; + + internal int NodeId; + internal NodeInfo NodeInfo; + + private byte[] _buffer; + private int _bufferEnd; + + public LdnProxyTcpSession(LdnProxyTcpServer server, LanProtocol protocol) : base(server) + { + _protocol = protocol; + _protocol.Connect += OnConnect; + _buffer = new byte[LanProtocol.BufferSize]; + OptionSendBufferSize = LanProtocol.TcpTxBufferSize; + OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize; + OptionSendBufferLimit = LanProtocol.TxBufferSizeMax; + OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax; + } + + public void OverrideInfo() + { + NodeInfo.NodeId = (byte)NodeId; + NodeInfo.IsConnected = (byte)(IsConnected ? 1 : 0); + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession connected!"); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession disconnected!"); + + _protocol.InvokeDisconnectStation(this); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, this.Socket.RemoteEndPoint); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession caught an error with code {error}"); + + Dispose(); + } + + protected override void Dispose(bool disposingManagedResources) + { + _protocol.Connect -= OnConnect; + base.Dispose(disposingManagedResources); + } + + private void OnConnect(NodeInfo info, EndPoint endPoint) + { + try + { + if (endPoint.Equals(this.Socket.RemoteEndPoint)) + { + NodeInfo = info; + _protocol.InvokeAccept(this); + } + } + catch (System.ObjectDisposedException) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession was disposed. [IP: {NodeInfo.Ipv4Address}]"); + + _protocol.InvokeDisconnectStation(this); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs new file mode 100644 index 000000000..b1519d1ff --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs @@ -0,0 +1,157 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy +{ + internal class LdnProxyUdpServer : NetCoreServer.UdpServer, ILdnSocket + { + private const long ScanFrequency = 1000; + + private readonly LanProtocol _protocol; + private byte[] _buffer; + private int _bufferEnd; + + private readonly object _scanLock = new(); + + private Dictionary _scanResultsLast = new(); + private Dictionary _scanResults = new(); + private readonly AutoResetEvent _scanResponse = new(false); + private long _lastScanTime; + + public LdnProxyUdpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port) + { + _protocol = protocol; + _protocol.Scan += HandleScan; + _protocol.ScanResponse += HandleScanResponse; + _buffer = new byte[LanProtocol.BufferSize]; + OptionReuseAddress = true; + OptionReceiveBufferSize = LanProtocol.RxBufferSizeMax; + OptionSendBufferSize = LanProtocol.TxBufferSizeMax; + + Start(); + } + + protected override Socket CreateSocket() + { + return new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp) + { + EnableBroadcast = true, + }; + } + + protected override void OnStarted() + { + ReceiveAsync(); + } + + protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size) + { + _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, endpoint); + ReceiveAsync(); + } + + protected override void OnError(SocketError error) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyUdpServer caught an error with code {error}"); + } + + protected override void Dispose(bool disposingManagedResources) + { + _protocol.Scan -= HandleScan; + _protocol.ScanResponse -= HandleScanResponse; + + _scanResponse.Dispose(); + + base.Dispose(disposingManagedResources); + } + + public bool SendPacketAsync(EndPoint endpoint, byte[] data) + { + return SendAsync(endpoint, data); + } + + private void HandleScan(EndPoint endpoint, LanPacketType type, byte[] data) + { + _protocol.SendPacket(this, type, data, endpoint); + } + + private void HandleScanResponse(NetworkInfo info) + { + Span mac = stackalloc byte[8]; + + info.Common.MacAddress.AsSpan().CopyTo(mac); + + lock (_scanLock) + { + _scanResults[BitConverter.ToUInt64(mac)] = info; + + _scanResponse.Set(); + } + } + + public void ClearScanResults() + { + // Rate limit scans. + + long timeMs = Stopwatch.GetTimestamp() / (Stopwatch.Frequency / 1000); + long delay = ScanFrequency - (timeMs - _lastScanTime); + + if (delay > 0) + { + Thread.Sleep((int)delay); + } + + _lastScanTime = timeMs; + + lock (_scanLock) + { + var newResults = _scanResultsLast; + newResults.Clear(); + + _scanResultsLast = _scanResults; + _scanResults = newResults; + + _scanResponse.Reset(); + } + } + + public Dictionary GetScanResults() + { + // NOTE: Try to minimize waiting time for scan results. + // After we receive the first response, wait a short time for follow-ups and return. + // Responses that were too late to catch will appear in the next scan. + + // ldn_mitm does not do this, but this improves latency for games that expect it to be low (it is on console). + + if (_scanResponse.WaitOne(1000)) + { + // Wait a short while longer in case there are some other responses. + Thread.Sleep(33); + } + + lock (_scanLock) + { + var results = new Dictionary(); + + foreach (KeyValuePair last in _scanResultsLast) + { + results[last.Key] = last.Value; + } + + foreach (KeyValuePair scan in _scanResults) + { + results[scan.Key] = scan.Value; + } + + return results; + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs new file mode 100644 index 000000000..4cebe414d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 12)] + internal struct LanPacketHeader + { + public uint Magic; + public LanPacketType Type; + public byte Compressed; + public ushort Length; + public ushort DecompressLength; + public Array2 Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs new file mode 100644 index 000000000..901f00b00 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types +{ + internal enum LanPacketType : byte + { + Scan, + ScanResponse, + Connect, + SyncNetwork, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs similarity index 91% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs index 1cc09c00d..b379d2680 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class NetworkChangeEventArgs : EventArgs { diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index c190d6ed1..e39c01978 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -1,7 +1,6 @@ using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Ldn.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; -using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator @@ -22,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _parent.NetworkClient.NetworkChange += NetworkChanged; } - private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e) + private void NetworkChanged(object sender, NetworkChangeEventArgs e) { LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs similarity index 86% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs index 47e48d0a1..058ce62d0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { [StructLayout(LayoutKind.Sequential, Size = 0xBC)] struct ConnectPrivateRequest diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs similarity index 84% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs index 9ff46cccb..136589b2a 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { [StructLayout(LayoutKind.Sequential, Size = 0x4FC)] struct ConnectRequest diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs similarity index 88% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs index 6e890618c..ec0668884 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { /// /// Advertise data is appended separately (remaining data in the buffer). diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs similarity index 86% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs index 4efe9165a..eecea5eb0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs @@ -1,7 +1,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { /// /// Advertise data is appended separately (remaining data in the buffer). diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs similarity index 80% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs index 70ebf7e38..cd576e055 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs @@ -1,4 +1,4 @@ -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { enum NetworkError : int { diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs similarity index 71% rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs index acb0b36ac..7e0c2a43f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types { [StructLayout(LayoutKind.Sequential, Size = 0x4)] struct NetworkErrorMessage diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 5e3aa0eac..f3439cc8f 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index 9d2df5f03..9ed8fd8cc 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -571,6 +571,7 @@ namespace Ryujinx.Ui.Common.Configuration { LanInterfaceId = new ReactiveObject(); Mode = new ReactiveObject(); + Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode)); } } diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index f4817277d..a9d4be109 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -1121,6 +1121,14 @@ namespace Ryujinx.Ui Graphics.Gpu.GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; } + public void UpdateInternetAccess() + { + if (_gameLoaded) + { + _emulationContext.Configuration.EnableInternetAccess = ConfigurationState.Instance.System.EnableInternetAccess.Value; + } + } + public static void SaveConfig() { ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.cs b/src/Ryujinx/Ui/Windows/SettingsWindow.cs index f5186d5c1..dabef14dd 100644 --- a/src/Ryujinx/Ui/Windows/SettingsWindow.cs +++ b/src/Ryujinx/Ui/Windows/SettingsWindow.cs @@ -671,6 +671,8 @@ namespace Ryujinx.Ui.Windows } ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + _parent.UpdateInternetAccess(); MainWindow.UpdateGraphicsConfig(); ThemeHelper.ApplyTheme(); } diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.glade b/src/Ryujinx/Ui/Windows/SettingsWindow.glade index fcc8c1d19..f0dbd6b63 100644 --- a/src/Ryujinx/Ui/Windows/SettingsWindow.glade +++ b/src/Ryujinx/Ui/Windows/SettingsWindow.glade @@ -2993,6 +2993,7 @@ Disabled Disabled + ldn_mitm @@ -3064,7 +3065,7 @@ True False - The network interface used for LAN features + The network interface used for LAN/LDN features end Network Interface: @@ -3079,7 +3080,7 @@ True False - The network interface used for LAN features + The network interface used for LAN/LDN features 0 Default From 9ef0be477bd6ea4c2c9aada53d94386824a87f00 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Mon, 30 Oct 2023 19:18:28 -0300 Subject: [PATCH 14/40] Skip some invalid texture flushes (#5755) --- src/Ryujinx.Graphics.Gpu/Image/Texture.cs | 15 ++++++++++++++- src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs | 2 -- src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs | 8 ++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index 022a3839f..dca6263aa 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -101,6 +101,11 @@ namespace Ryujinx.Graphics.Gpu.Image /// public bool AlwaysFlushOnOverlap { get; private set; } + /// + /// Indicates that the texture was fully unmapped since the modified flag was set, and flushes should be ignored until it is modified again. + /// + public bool FlushStale { get; private set; } + /// /// Increments when the host texture is swapped, or when the texture is removed from all pools. /// @@ -149,6 +154,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// public bool HadPoolOwner { get; private set; } + /// /// Physical memory ranges where the texture data is located. /// public MultiRange Range { get; private set; } @@ -1411,6 +1417,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void SignalModified() { + FlushStale = false; _scaledSetScore = Math.Max(0, _scaledSetScore - 1); if (_modifiedStale || Group.HasCopyDependencies) @@ -1431,6 +1438,7 @@ namespace Ryujinx.Graphics.Gpu.Image { if (bound) { + FlushStale = false; _scaledSetScore = Math.Max(0, _scaledSetScore - 1); } @@ -1695,12 +1703,17 @@ namespace Ryujinx.Graphics.Gpu.Image /// The range of memory being unmapped public void Unmapped(MultiRange unmapRange) { + if (unmapRange.Contains(Range)) + { + // If this is a full unmap, prevent flushes until the texture is mapped again. + FlushStale = true; + } + ChangedMapping = true; if (Group.Storage == this) { Group.Unmapped(); - Group.ClearModified(unmapRange); } } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs index 5048ccca4..432b10853 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs @@ -107,8 +107,6 @@ namespace Ryujinx.Graphics.Gpu.Image // Any texture that has been unmapped at any point or is partially unmapped // should update their pool references after the remap completes. - MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size); - foreach (var texture in _partiallyMappedTextures) { texture.UpdatePoolMappings(); diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index 746a95ffc..21d7939ad 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -1659,6 +1659,14 @@ namespace Ryujinx.Graphics.Gpu.Image return; } + // If size is zero, we have nothing to flush. + // If the flush is stale, we should ignore it because the texture was unmapped since the modified + // flag was set, and flushing it is not safe anymore as the GPU might no longer own the memory. + if (size == 0 || Storage.FlushStale) + { + return; + } + // There is a small gap here where the action is removed but _actionRegistered is still 1. // In this case it will skip registering the action, but here we are already handling it, // so there shouldn't be any issue as it's the same handler for all actions. From a16d582a105a6f9218e5f50fafd2670c64c1244c Mon Sep 17 00:00:00 2001 From: riperiperi Date: Mon, 30 Oct 2023 22:26:31 +0000 Subject: [PATCH 15/40] [HLE] Remove ServerBase 1ms polling (#5855) Added a KEvent for each ServerBase which signals whenever a session is added to the _sessions list, which allows it to rerun the ReplyAndReceive with the new session handle. This greatly reduces the presence of ServerBase on profiles, especially of games that aren't particularly busy. It should also increase responsiveness when adding session objects, as it doesn't take at most 1ms for them to start working. It also reduces the load on KTimeManager, which could allow it to spin less often. I have noticed that a bunch of games still do 1ms waits (they actually request a bit less than 1ms), so they still end up spinning to the next millisecond. Maybe for waits like this, it could attempt to nudge the timepoints to snap to each other when they're close enough, and also snap to whole millisecond waits when close enough. --- src/Ryujinx.HLE/HOS/Services/ServerBase.cs | 55 ++++++++++++++-------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs index 9d7e4d4c5..145680594 100644 --- a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs +++ b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs @@ -39,6 +39,8 @@ namespace Ryujinx.HLE.HOS.Services private readonly KernelContext _context; private KProcess _selfProcess; private KThread _selfThread; + private KEvent _wakeEvent; + private int _wakeHandle = 0; private readonly ReaderWriterLockSlim _handleLock = new(); private readonly Dictionary _sessions = new(); @@ -125,6 +127,8 @@ namespace Ryujinx.HLE.HOS.Services _handleLock.ExitWriteLock(); } } + + _wakeEvent.WritableEvent.Signal(); } private IpcService GetSessionObj(int serverSessionHandle) @@ -195,9 +199,11 @@ namespace Ryujinx.HLE.HOS.Services _selfProcess.CpuMemory.Write(messagePtr + 0x0, 0); _selfProcess.CpuMemory.Write(messagePtr + 0x4, 2 << 10); _selfProcess.CpuMemory.Write(messagePtr + 0x8, heapAddr | ((ulong)PointerBufferSize << 48)); - int replyTargetHandle = 0; + _wakeEvent = new KEvent(_context); + Result result = _selfProcess.HandleTable.GenerateHandle(_wakeEvent.ReadableEvent, out _wakeHandle); + while (true) { int portHandleCount; @@ -211,13 +217,15 @@ namespace Ryujinx.HLE.HOS.Services portHandleCount = _ports.Count; - handleCount = portHandleCount + _sessions.Count; + handleCount = portHandleCount + _sessions.Count + 1; handles = ArrayPool.Shared.Rent(handleCount); - _ports.Keys.CopyTo(handles, 0); + handles[0] = _wakeHandle; - _sessions.Keys.CopyTo(handles, portHandleCount); + _ports.Keys.CopyTo(handles, 1); + + _sessions.Keys.CopyTo(handles, portHandleCount + 1); } finally { @@ -227,8 +235,7 @@ namespace Ryujinx.HLE.HOS.Services } } - // We still need a timeout here to allow the service to pick up and listen new sessions... - var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, 1000000L); + var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, -1); _selfThread.HandlePostSyscall(); @@ -239,7 +246,7 @@ namespace Ryujinx.HLE.HOS.Services replyTargetHandle = 0; - if (rc == Result.Success && signaledIndex >= portHandleCount) + if (rc == Result.Success && signaledIndex >= portHandleCount + 1) { // We got a IPC request, process it, pass to the appropriate service if needed. int signaledHandle = handles[signaledIndex]; @@ -253,24 +260,32 @@ namespace Ryujinx.HLE.HOS.Services { if (rc == Result.Success) { - // We got a new connection, accept the session to allow servicing future requests. - if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success) + if (signaledIndex > 0) { - bool handleWriteLockTaken = false; - try + // We got a new connection, accept the session to allow servicing future requests. + if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success) { - handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite); - IpcService obj = _ports[handles[signaledIndex]].Invoke(); - _sessions.Add(serverSessionHandle, obj); - } - finally - { - if (handleWriteLockTaken) + bool handleWriteLockTaken = false; + try { - _handleLock.ExitWriteLock(); + handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite); + IpcService obj = _ports[handles[signaledIndex]].Invoke(); + _sessions.Add(serverSessionHandle, obj); + } + finally + { + if (handleWriteLockTaken) + { + _handleLock.ExitWriteLock(); + } } } } + else + { + // The _wakeEvent signalled, which means we have a new session. + _wakeEvent.WritableEvent.Clear(); + } } _selfProcess.CpuMemory.Write(messagePtr + 0x0, 0); @@ -499,6 +514,8 @@ namespace Ryujinx.HLE.HOS.Services if (Interlocked.Exchange(ref _isDisposed, 1) == 0) { + _selfProcess.HandleTable.CloseHandle(_wakeHandle); + foreach (IpcService service in _sessions.Values) { (service as IDisposable)?.Dispose(); From 841dd56f4ce850693aee5980cd750791624e47be Mon Sep 17 00:00:00 2001 From: gdkchan Date: Tue, 31 Oct 2023 19:00:39 -0300 Subject: [PATCH 16/40] Implement copy dependency for depth and color textures (#4365) * Implement copy dependency for depth and color textures * Revert changes added because R32 <-> D32 copies were illegal * Restore depth alias matches --- .../Image/TextureCache.cs | 6 +- .../Image/TextureCompatibility.cs | 61 ++++++++++++++----- .../Image/TextureCopy.cs | 4 +- .../Image/TextureView.cs | 29 +++++++++ src/Ryujinx.Graphics.Vulkan/TextureView.cs | 11 ++++ 5 files changed, 90 insertions(+), 21 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs index 432b10853..6b92c0aaf 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs @@ -733,9 +733,7 @@ namespace Ryujinx.Graphics.Gpu.Image { if (overlap.IsView) { - overlapCompatibility = overlapCompatibility == TextureViewCompatibility.FormatAlias ? - TextureViewCompatibility.Incompatible : - TextureViewCompatibility.CopyOnly; + overlapCompatibility = TextureViewCompatibility.CopyOnly; } else { @@ -813,7 +811,7 @@ namespace Ryujinx.Graphics.Gpu.Image Texture overlap = _textureOverlaps[index]; OverlapInfo oInfo = _overlapInfo[index]; - if (oInfo.Compatibility <= TextureViewCompatibility.LayoutIncompatible || oInfo.Compatibility == TextureViewCompatibility.FormatAlias) + if (oInfo.Compatibility <= TextureViewCompatibility.LayoutIncompatible) { if (!overlap.IsView && texture.DataOverlaps(overlap, oInfo.Compatibility)) { diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs index 3a0efcdda..5af0471c0 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -226,7 +226,7 @@ namespace Ryujinx.Graphics.Gpu.Image { // D32F and R32F texture have the same representation internally, // however the R32F format is used to sample from depth textures. - if (lhs.FormatInfo.Format == Format.D32Float && rhs.FormatInfo.Format == Format.R32Float && (forSampler || depthAlias)) + if (IsValidDepthAsColorAlias(lhs.FormatInfo.Format, rhs.FormatInfo.Format) && (forSampler || depthAlias)) { return TextureMatchQuality.FormatAlias; } @@ -239,14 +239,8 @@ namespace Ryujinx.Graphics.Gpu.Image { return TextureMatchQuality.FormatAlias; } - - if (lhs.FormatInfo.Format == Format.D16Unorm && rhs.FormatInfo.Format == Format.R16Unorm) - { - return TextureMatchQuality.FormatAlias; - } - - if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint || - lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm) + else if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint || + lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm) { return TextureMatchQuality.FormatAlias; } @@ -632,12 +626,27 @@ namespace Ryujinx.Graphics.Gpu.Image if (lhsFormat.Format.IsDepthOrStencil() || rhsFormat.Format.IsDepthOrStencil()) { - return FormatMatches(lhs, rhs, flags.HasFlag(TextureSearchFlags.ForSampler), flags.HasFlag(TextureSearchFlags.DepthAlias)) switch + bool forSampler = flags.HasFlag(TextureSearchFlags.ForSampler); + bool depthAlias = flags.HasFlag(TextureSearchFlags.DepthAlias); + + TextureMatchQuality matchQuality = FormatMatches(lhs, rhs, forSampler, depthAlias); + + if (matchQuality == TextureMatchQuality.Perfect) { - TextureMatchQuality.Perfect => TextureViewCompatibility.Full, - TextureMatchQuality.FormatAlias => TextureViewCompatibility.FormatAlias, - _ => TextureViewCompatibility.Incompatible, - }; + return TextureViewCompatibility.Full; + } + else if (matchQuality == TextureMatchQuality.FormatAlias) + { + return TextureViewCompatibility.FormatAlias; + } + else if (IsValidColorAsDepthAlias(lhsFormat.Format, rhsFormat.Format) || IsValidDepthAsColorAlias(lhsFormat.Format, rhsFormat.Format)) + { + return TextureViewCompatibility.CopyOnly; + } + else + { + return TextureViewCompatibility.Incompatible; + } } if (IsFormatHostIncompatible(lhs, caps) || IsFormatHostIncompatible(rhs, caps)) @@ -666,6 +675,30 @@ namespace Ryujinx.Graphics.Gpu.Image return TextureViewCompatibility.Incompatible; } + /// + /// Checks if it's valid to alias a color format as a depth format. + /// + /// Source format to be checked + /// Target format to be checked + /// True if it's valid to alias the formats + private static bool IsValidColorAsDepthAlias(Format lhsFormat, Format rhsFormat) + { + return (lhsFormat == Format.R32Float && rhsFormat == Format.D32Float) || + (lhsFormat == Format.R16Unorm && rhsFormat == Format.D16Unorm); + } + + /// + /// Checks if it's valid to alias a depth format as a color format. + /// + /// Source format to be checked + /// Target format to be checked + /// True if it's valid to alias the formats + private static bool IsValidDepthAsColorAlias(Format lhsFormat, Format rhsFormat) + { + return (lhsFormat == Format.D32Float && rhsFormat == Format.R32Float) || + (lhsFormat == Format.D16Unorm && rhsFormat == Format.R16Unorm); + } + /// /// Checks if aliasing of two formats that would normally be considered incompatible be allowed, /// using copy dependencies. diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs index e33940cb1..128f481f6 100644 --- a/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs +++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs @@ -367,7 +367,7 @@ namespace Ryujinx.Graphics.OpenGL.Image return to; } - private TextureView PboCopy(TextureView from, TextureView to, int srcLayer, int dstLayer, int srcLevel, int dstLevel, int width, int height) + public void PboCopy(TextureView from, TextureView to, int srcLayer, int dstLayer, int srcLevel, int dstLevel, int width, int height) { int dstWidth = width; int dstHeight = height; @@ -445,8 +445,6 @@ namespace Ryujinx.Graphics.OpenGL.Image } GL.BindBuffer(BufferTarget.PixelUnpackBuffer, 0); - - return to; } private void EnsurePbo(TextureView view) diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs index 0f5fe46a5..7f1b1c382 100644 --- a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs +++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs @@ -140,6 +140,28 @@ namespace Ryujinx.Graphics.OpenGL.Image int levels = Math.Min(Info.Levels, destinationView.Info.Levels - firstLevel); _renderer.TextureCopyIncompatible.CopyIncompatibleFormats(this, destinationView, 0, firstLayer, 0, firstLevel, layers, levels); } + else if (destinationView.Format.IsDepthOrStencil() != Format.IsDepthOrStencil()) + { + int layers = Math.Min(Info.GetLayers(), destinationView.Info.GetLayers() - firstLayer); + int levels = Math.Min(Info.Levels, destinationView.Info.Levels - firstLevel); + + for (int level = 0; level < levels; level++) + { + int srcWidth = Math.Max(1, Width >> level); + int srcHeight = Math.Max(1, Height >> level); + + int dstWidth = Math.Max(1, destinationView.Width >> (firstLevel + level)); + int dstHeight = Math.Max(1, destinationView.Height >> (firstLevel + level)); + + int minWidth = Math.Min(srcWidth, dstWidth); + int minHeight = Math.Min(srcHeight, dstHeight); + + for (int layer = 0; layer < layers; layer++) + { + _renderer.TextureCopy.PboCopy(this, destinationView, 0, firstLayer + layer, 0, firstLevel + level, minWidth, minHeight); + } + } + } else { _renderer.TextureCopy.CopyUnscaled(this, destinationView, 0, firstLayer, 0, firstLevel); @@ -169,6 +191,13 @@ namespace Ryujinx.Graphics.OpenGL.Image { _renderer.TextureCopyIncompatible.CopyIncompatibleFormats(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1); } + else if (destinationView.Format.IsDepthOrStencil() != Format.IsDepthOrStencil()) + { + int minWidth = Math.Min(Width, destinationView.Width); + int minHeight = Math.Min(Height, destinationView.Height); + + _renderer.TextureCopy.PboCopy(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, minWidth, minHeight); + } else { _renderer.TextureCopy.CopyUnscaled(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1); diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs index 09128f007..05dbd15ce 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs @@ -211,6 +211,13 @@ namespace Ryujinx.Graphics.Vulkan int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel); _gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, 0, firstLayer, 0, firstLevel, layers, levels); } + else if (src.Info.Format.IsDepthOrStencil() != dst.Info.Format.IsDepthOrStencil()) + { + int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer); + int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel); + + _gd.HelperShader.CopyColor(_gd, cbs, src, dst, 0, firstLayer, 0, FirstLevel, layers, levels); + } else { TextureCopy.Copy( @@ -260,6 +267,10 @@ namespace Ryujinx.Graphics.Vulkan { _gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1); } + else if (src.Info.Format.IsDepthOrStencil() != dst.Info.Format.IsDepthOrStencil()) + { + _gd.HelperShader.CopyColor(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1); + } else { TextureCopy.Copy( From 7b62f7475eae8cfef82f7a60d45aaef55c1efde7 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Wed, 1 Nov 2023 17:47:40 -0300 Subject: [PATCH 17/40] Fix AddSessionObj NRE regression (#5875) --- src/Ryujinx.HLE/HOS/Services/ServerBase.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs index 145680594..e892d6ab6 100644 --- a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs +++ b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs @@ -191,6 +191,9 @@ namespace Ryujinx.HLE.HOS.Services AddPort(serverPortHandle, SmObjectFactory); } + _wakeEvent = new KEvent(_context); + Result result = _selfProcess.HandleTable.GenerateHandle(_wakeEvent.ReadableEvent, out _wakeHandle); + InitDone.Set(); ulong messagePtr = _selfThread.TlsAddress; @@ -201,9 +204,6 @@ namespace Ryujinx.HLE.HOS.Services _selfProcess.CpuMemory.Write(messagePtr + 0x8, heapAddr | ((ulong)PointerBufferSize << 48)); int replyTargetHandle = 0; - _wakeEvent = new KEvent(_context); - Result result = _selfProcess.HandleTable.GenerateHandle(_wakeEvent.ReadableEvent, out _wakeHandle); - while (true) { int portHandleCount; From 617c5700ca520d80fd25fc0fc9f2389394a46150 Mon Sep 17 00:00:00 2001 From: Somebody Whoisbored <13044396+shadowninja108@users.noreply.github.com> Date: Sun, 5 Nov 2023 04:32:17 -0700 Subject: [PATCH 18/40] Better handle instruction aborts when hitting unmapped memory (#5869) * Adjust ARMeilleure to better handle instruction aborts when hitting unmapped memory * Update src/ARMeilleure/Decoders/Decoder.cs Co-authored-by: gdkchan --------- Co-authored-by: gdkchan --- src/ARMeilleure/Decoders/Decoder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ARMeilleure/Decoders/Decoder.cs b/src/ARMeilleure/Decoders/Decoder.cs index 6d07827a2..66d286928 100644 --- a/src/ARMeilleure/Decoders/Decoder.cs +++ b/src/ARMeilleure/Decoders/Decoder.cs @@ -38,7 +38,9 @@ namespace ARMeilleure.Decoders { block = new Block(blkAddress); - if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) || opsCount > instructionLimit || !memory.IsMapped(blkAddress)) + if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) || + opsCount > instructionLimit || + (visited.Count > 0 && !memory.IsMapped(blkAddress))) { block.Exit = true; block.EndAddress = blkAddress; From 623604c39186901fd64c8e04e9aa959d5c825529 Mon Sep 17 00:00:00 2001 From: SamusAranX Date: Mon, 6 Nov 2023 22:47:44 +0100 Subject: [PATCH 19/40] Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956) * Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata Added a migration for TimePlayed, just like in #4861 Consolidated ApplicationData's FileSize* properties into one FileSize property Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes Added new value converters for TimeSpans and file sizes for the Avalonia UI Added TimePlayedSortComparer Fixed sort order in LastPlayedSortComparer Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize Fixed crashes caused by SortHelper Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils Replaced SaveModel.GetSizeString() with ValueFormatUtils * Additional ApplicationLibrary changes that got lost in the last commit * Removed unneeded usings * Removed converters as they are no longer needed * Updated comment on FormatDateTime * Removed base10 parameter from ValueFormatUtils FormatFileSize now always returns base 2 values with base 10 units Made ParseFileSize capable of parsing both base 2 and base 10 units * Removed nullable attribute from TimePlayed property Centralized TimePlayed update code into ApplicationMetadata * Changed UpdateTimePlayed() to use TimeSpan logic * Removed JsonIgnore attributes from ApplicationData * Implemented requested format changes * Fixed mistakes in method documentation comments * Made it so the Last Played value "Never" is localized in the Avalonia UI * Implemented suggestions * Remove unused import * Did a comment refinement pass in ValueFormatUtils.cs * Reordered ValueFormatUtils methods and sorted them into #regions * Integrated functionality from #5056 Also removed Logger print from last_played migration code * Implemented suggestions * Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common * common: Respect proper value format convention and use base10 by default This could be discuss again in another issue/PR, for now revert to the previous behavior. Signed-off-by: Mary Guillemard --------- Signed-off-by: Mary Guillemard Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Co-authored-by: Mary Guillemard --- src/Ryujinx.Ava/AppHost.cs | 2 +- src/Ryujinx.Ava/Program.cs | 2 +- .../UI/Controls/ApplicationListView.axaml | 6 +- .../UI/Helpers/LocalizedNeverConverter.cs | 43 ++++ .../UI/Helpers/NullableDateTimeConverter.cs | 38 --- .../Models/Generic/LastPlayedSortComparer.cs | 13 +- .../Models/Generic/TimePlayedSortComparer.cs | 31 +++ src/Ryujinx.Ava/UI/Models/SaveModel.cs | 23 +- .../UI/ViewModels/MainWindowViewModel.cs | 31 +-- src/Ryujinx.Ui.Common/App/ApplicationData.cs | 26 +-- .../App/ApplicationLibrary.cs | 63 ++--- .../App/ApplicationMetadata.cs | 34 ++- .../Helper/ValueFormatUtils.cs | 219 ++++++++++++++++++ .../SystemInfo/LinuxSystemInfo.cs | 2 +- .../SystemInfo/MacOSSystemInfo.cs | 2 +- .../SystemInfo/SystemInfo.cs | 7 +- .../SystemInfo/WindowsSystemInfo.cs | 2 +- src/Ryujinx/Program.cs | 2 +- src/Ryujinx/Ui/Helper/SortHelper.cs | 81 +------ src/Ryujinx/Ui/MainWindow.cs | 14 +- 20 files changed, 398 insertions(+), 243 deletions(-) create mode 100644 src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs delete mode 100644 src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs create mode 100644 src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs create mode 100644 src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs rename src/{Ryujinx.Common => Ryujinx.Ui.Common}/SystemInfo/LinuxSystemInfo.cs (98%) rename src/{Ryujinx.Common => Ryujinx.Ui.Common}/SystemInfo/MacOSSystemInfo.cs (99%) rename src/{Ryujinx.Common => Ryujinx.Ui.Common}/SystemInfo/SystemInfo.cs (87%) rename src/{Ryujinx.Common => Ryujinx.Ui.Common}/SystemInfo/WindowsSystemInfo.cs (98%) diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index cd066efba..4d751e2a9 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -716,7 +716,7 @@ namespace Ryujinx.Ava ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => { - appMetadata.LastPlayed = DateTime.UtcNow; + appMetadata.UpdatePreGame(); }); return true; diff --git a/src/Ryujinx.Ava/Program.cs b/src/Ryujinx.Ava/Program.cs index 168e9216d..cc062a256 100644 --- a/src/Ryujinx.Ava/Program.cs +++ b/src/Ryujinx.Ava/Program.cs @@ -6,13 +6,13 @@ using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; -using Ryujinx.Common.SystemInfo; using Ryujinx.Common.SystemInterop; using Ryujinx.Modules; using Ryujinx.SDL2.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; +using Ryujinx.Ui.Common.SystemInfo; using System; using System.IO; using System.Runtime.InteropServices; diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml index 09011005b..9004f7518 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml @@ -126,17 +126,17 @@ Spacing="5"> diff --git a/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs b/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs new file mode 100644 index 000000000..737896986 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs @@ -0,0 +1,43 @@ +using Avalonia.Data.Converters; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ui.Common.Helper; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + /// + /// This makes sure that the string "Never" that's returned by is properly localized in the Avalonia UI. + /// After the Avalonia UI has been made the default and the GTK UI is removed, should be updated to directly return a localized string. + /// + internal class LocalizedNeverConverter : MarkupExtension, IValueConverter + { + private static readonly LocalizedNeverConverter _instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not string valStr) + { + return ""; + } + + if (valStr == "Never") + { + return LocaleManager.Instance[LocaleKeys.Never]; + } + + return valStr; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return _instance; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs b/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs deleted file mode 100644 index e91937612..000000000 --- a/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Avalonia.Data.Converters; -using Avalonia.Markup.Xaml; -using Ryujinx.Ava.Common.Locale; -using System; -using System.Globalization; - -namespace Ryujinx.Ava.UI.Helpers -{ - internal class NullableDateTimeConverter : MarkupExtension, IValueConverter - { - private static readonly NullableDateTimeConverter _instance = new(); - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value == null) - { - return LocaleManager.Instance[LocaleKeys.Never]; - } - - if (value is DateTime dateTime) - { - return dateTime.ToLocalTime().ToString(culture); - } - - throw new NotSupportedException(); - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } - - public override object ProvideValue(IServiceProvider serviceProvider) - { - return _instance; - } - } -} diff --git a/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs b/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs index 8a4346556..8340d39df 100644 --- a/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs +++ b/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs @@ -13,20 +13,19 @@ namespace Ryujinx.Ava.UI.Models.Generic public int Compare(ApplicationData x, ApplicationData y) { - var aValue = x.LastPlayed; - var bValue = y.LastPlayed; + DateTime aValue = DateTime.UnixEpoch, bValue = DateTime.UnixEpoch; - if (!aValue.HasValue) + if (x?.LastPlayed != null) { - aValue = DateTime.UnixEpoch; + aValue = x.LastPlayed.Value; } - if (!bValue.HasValue) + if (y?.LastPlayed != null) { - bValue = DateTime.UnixEpoch; + bValue = y.LastPlayed.Value; } - return (IsAscending ? 1 : -1) * DateTime.Compare(bValue.Value, aValue.Value); + return (IsAscending ? 1 : -1) * DateTime.Compare(aValue, bValue); } } } diff --git a/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs b/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs new file mode 100644 index 000000000..d53ff566f --- /dev/null +++ b/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs @@ -0,0 +1,31 @@ +using Ryujinx.Ui.App.Common; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Models.Generic +{ + internal class TimePlayedSortComparer : IComparer + { + public TimePlayedSortComparer() { } + public TimePlayedSortComparer(bool isAscending) { IsAscending = isAscending; } + + public bool IsAscending { get; } + + public int Compare(ApplicationData x, ApplicationData y) + { + TimeSpan aValue = TimeSpan.Zero, bValue = TimeSpan.Zero; + + if (x?.TimePlayed != null) + { + aValue = x.TimePlayed; + } + + if (y?.TimePlayed != null) + { + bValue = y.TimePlayed; + } + + return (IsAscending ? 1 : -1) * TimeSpan.Compare(aValue, bValue); + } + } +} diff --git a/src/Ryujinx.Ava/UI/Models/SaveModel.cs b/src/Ryujinx.Ava/UI/Models/SaveModel.cs index f15befbb3..7b476932b 100644 --- a/src/Ryujinx.Ava/UI/Models/SaveModel.cs +++ b/src/Ryujinx.Ava/UI/Models/SaveModel.cs @@ -4,7 +4,7 @@ using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.Windows; using Ryujinx.HLE.FileSystem; using Ryujinx.Ui.App.Common; -using System; +using Ryujinx.Ui.Common.Helper; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -38,26 +38,7 @@ namespace Ryujinx.Ava.UI.Models public bool SizeAvailable { get; set; } - public string SizeString => GetSizeString(); - - private string GetSizeString() - { - const int Scale = 1024; - string[] orders = { "GiB", "MiB", "KiB" }; - long max = (long)Math.Pow(Scale, orders.Length); - - foreach (string order in orders) - { - if (Size > max) - { - return $"{decimal.Divide(Size, max):##.##} {order}"; - } - - max /= Scale; - } - - return "0 KiB"; - } + public string SizeString => ValueFormatUtils.FormatFileSize(Size); public SaveModel(SaveDataInfo info) { diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index b14905204..80df5d398 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -930,21 +930,20 @@ namespace Ryujinx.Ava.UI.ViewModels return SortMode switch { #pragma warning disable IDE0055 // Disable formatting + ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName) + : SortExpressionComparer.Descending(app => app.TitleName), + ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer) + : SortExpressionComparer.Descending(app => app.Developer), ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending), - ApplicationSort.FileSize => IsAscending ? SortExpressionComparer.Ascending(app => app.FileSizeBytes) - : SortExpressionComparer.Descending(app => app.FileSizeBytes), - ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer.Ascending(app => app.TimePlayedNum) - : SortExpressionComparer.Descending(app => app.TimePlayedNum), - ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName) - : SortExpressionComparer.Descending(app => app.TitleName), + ApplicationSort.TotalTimePlayed => new TimePlayedSortComparer(IsAscending), + ApplicationSort.FileType => IsAscending ? SortExpressionComparer.Ascending(app => app.FileExtension) + : SortExpressionComparer.Descending(app => app.FileExtension), + ApplicationSort.FileSize => IsAscending ? SortExpressionComparer.Ascending(app => app.FileSize) + : SortExpressionComparer.Descending(app => app.FileSize), + ApplicationSort.Path => IsAscending ? SortExpressionComparer.Ascending(app => app.Path) + : SortExpressionComparer.Descending(app => app.Path), ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer.Ascending(app => app.Favorite) : SortExpressionComparer.Descending(app => app.Favorite), - ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer) - : SortExpressionComparer.Descending(app => app.Developer), - ApplicationSort.FileType => IsAscending ? SortExpressionComparer.Ascending(app => app.FileExtension) - : SortExpressionComparer.Descending(app => app.FileExtension), - ApplicationSort.Path => IsAscending ? SortExpressionComparer.Ascending(app => app.Path) - : SortExpressionComparer.Descending(app => app.Path), _ => null, #pragma warning restore IDE0055 }; @@ -1549,13 +1548,7 @@ namespace Ryujinx.Ava.UI.ViewModels { ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => { - if (appMetadata.LastPlayed.HasValue) - { - double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds; - appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); - } - - appMetadata.LastPlayed = DateTime.UtcNow; + appMetadata.UpdatePostGame(); }); } diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index 1be883ee1..65ab01eeb 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -9,10 +9,9 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.Common.Helper; using System; -using System.Globalization; using System.IO; -using System.Text.Json.Serialization; namespace Ryujinx.Ui.App.Common { @@ -24,29 +23,18 @@ namespace Ryujinx.Ui.App.Common public string TitleId { get; set; } public string Developer { get; set; } public string Version { get; set; } - public string TimePlayed { get; set; } - public double TimePlayedNum { get; set; } + public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } - public string FileSize { get; set; } - public double FileSizeBytes { get; set; } + public long FileSize { get; set; } public string Path { get; set; } public BlitStruct ControlHolder { get; set; } - [JsonIgnore] - public string LastPlayedString - { - get - { - if (!LastPlayed.HasValue) - { - // TODO: maybe put localized string here instead of just "Never" - return "Never"; - } + public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed); - return LastPlayed.Value.ToLocalTime().ToString(CultureInfo.CurrentCulture); - } - } + public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed); + + public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath) { diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 2f688126a..46f29851c 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -155,7 +155,7 @@ namespace Ryujinx.Ui.App.Common return; } - double fileSize = new FileInfo(applicationPath).Length * 0.000000000931; + long fileSize = new FileInfo(applicationPath).Length; string titleName = "Unknown"; string titleId = "0000000000000000"; string developer = "Unknown"; @@ -425,25 +425,25 @@ namespace Ryujinx.Ui.App.Common { appMetadata.Title = titleName; - if (appMetadata.LastPlayedOld == default || appMetadata.LastPlayed.HasValue) + // Only do the migration if time_played has a value and timespan_played hasn't been updated yet. + if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero) { - // Don't do the migration if last_played doesn't exist or last_played_utc already has a value. - return; + appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); + appMetadata.TimePlayedOld = default; } - // Migrate from string-based last_played to DateTime-based last_played_utc. - if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) + // Only do the migration if last_played has a value and last_played_utc doesn't exist yet. + if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue) { - Logger.Info?.Print(LogClass.Application, $"last_played found: \"{appMetadata.LastPlayedOld}\", migrating to last_played_utc"); - appMetadata.LastPlayed = lastPlayedOldParsed; + // Migrate from string-based last_played to DateTime-based last_played_utc. + if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) + { + appMetadata.LastPlayed = lastPlayedOldParsed; + + // Migration successful: deleting last_played from the metadata file. + appMetadata.LastPlayedOld = default; + } - // Migration successful: deleting last_played from the metadata file. - appMetadata.LastPlayedOld = default; - } - else - { - // Migration failed: emitting warning but leaving the unparsable value in the metadata file so the user can fix it. - Logger.Warning?.Print(LogClass.Application, $"Last played string \"{appMetadata.LastPlayedOld}\" is invalid for current system culture, skipping (did current culture change?)"); } }); @@ -455,12 +455,10 @@ namespace Ryujinx.Ui.App.Common TitleId = titleId, Developer = developer, Version = version, - TimePlayed = ConvertSecondsToFormattedString(appMetadata.TimePlayed), - TimePlayedNum = appMetadata.TimePlayed, + TimePlayed = appMetadata.TimePlayed, LastPlayed = appMetadata.LastPlayed, - FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1), - FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + " MiB" : fileSize.ToString("0.##") + " GiB", - FileSizeBytes = fileSize, + FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(), + FileSize = fileSize, Path = applicationPath, ControlHolder = controlHolder, }; @@ -716,31 +714,6 @@ namespace Ryujinx.Ui.App.Common return applicationIcon ?? _ncaIcon; } - private static string ConvertSecondsToFormattedString(double seconds) - { - TimeSpan time = TimeSpan.FromSeconds(seconds); - - string timeString; - if (time.Days != 0) - { - timeString = $"{time.Days}d {time.Hours:D2}h {time.Minutes:D2}m"; - } - else if (time.Hours != 0) - { - timeString = $"{time.Hours:D2}h {time.Minutes:D2}m"; - } - else if (time.Minutes != 0) - { - timeString = $"{time.Minutes:D2}m"; - } - else - { - timeString = "Never"; - } - - return timeString; - } - private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version) { _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); diff --git a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs index 01b857a62..9e2ca6870 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs @@ -7,13 +7,45 @@ namespace Ryujinx.Ui.App.Common { public string Title { get; set; } public bool Favorite { get; set; } - public double TimePlayed { get; set; } + + [JsonPropertyName("timespan_played")] + public TimeSpan TimePlayed { get; set; } = TimeSpan.Zero; [JsonPropertyName("last_played_utc")] public DateTime? LastPlayed { get; set; } = null; + [JsonPropertyName("time_played")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public double TimePlayedOld { get; set; } + [JsonPropertyName("last_played")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string LastPlayedOld { get; set; } + + /// + /// Updates . Call this before launching a game. + /// + public void UpdatePreGame() + { + LastPlayed = DateTime.UtcNow; + } + + /// + /// Updates and . Call this after a game ends. + /// + public void UpdatePostGame() + { + DateTime? prevLastPlayed = LastPlayed; + UpdatePreGame(); + + if (!prevLastPlayed.HasValue) + { + return; + } + + TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value; + double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds; + TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero)); + } } } diff --git a/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs new file mode 100644 index 000000000..951cd089e --- /dev/null +++ b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs @@ -0,0 +1,219 @@ +using System; +using System.Globalization; +using System.Linq; + +namespace Ryujinx.Ui.Common.Helper +{ + public static class ValueFormatUtils + { + private static readonly string[] _fileSizeUnitStrings = + { + "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", // Base 10 units, used for formatting and parsing + "KB", "MB", "GB", "TB", "PB", "EB", // Base 2 units, used for parsing legacy values + }; + + /// + /// Used by . + /// + public enum FileSizeUnits + { + Auto = -1, + Bytes = 0, + Kibibytes = 1, + Mebibytes = 2, + Gibibytes = 3, + Tebibytes = 4, + Pebibytes = 5, + Exbibytes = 6, + Kilobytes = 7, + Megabytes = 8, + Gigabytes = 9, + Terabytes = 10, + Petabytes = 11, + Exabytes = 12, + } + + private const double SizeBase10 = 1000; + private const double SizeBase2 = 1024; + private const int UnitEBIndex = 6; + + #region Value formatters + + /// + /// Creates a human-readable string from a . + /// + /// The to be formatted. + /// A formatted string that can be displayed in the UI. + public static string FormatTimeSpan(TimeSpan? timeSpan) + { + if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1) + { + // Game was never played + return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture); + } + + if (timeSpan.Value.TotalDays < 1) + { + // Game was played for less than a day + return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture); + } + + // Game was played for more than a day + TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days)); + string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture); + + return $"{timeSpan.Value.Days}d, {onlyTimeString}"; + } + + /// + /// Creates a human-readable string from a . + /// + /// The to be formatted. This is expected to be UTC-based. + /// The that's used in formatting. Defaults to . + /// A formatted string that can be displayed in the UI. + public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null) + { + culture ??= CultureInfo.CurrentCulture; + + if (!utcDateTime.HasValue) + { + // In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter. + return "Never"; + } + + return utcDateTime.Value.ToLocalTime().ToString(culture); + } + + /// + /// Creates a human-readable file size string. + /// + /// The file size in bytes. + /// Formats the passed size value as this unit, bypassing the automatic unit choice. + /// A human-readable file size string. + public static string FormatFileSize(long size, FileSizeUnits forceUnit = FileSizeUnits.Auto) + { + if (size <= 0) + { + return $"0 {_fileSizeUnitStrings[0]}"; + } + + int unitIndex = (int)forceUnit; + if (forceUnit == FileSizeUnits.Auto) + { + unitIndex = Convert.ToInt32(Math.Floor(Math.Log(size, SizeBase10))); + + // Apply an upper bound so that exabytes are the biggest unit used when formatting. + if (unitIndex > UnitEBIndex) + { + unitIndex = UnitEBIndex; + } + } + + double sizeRounded; + + if (unitIndex > UnitEBIndex) + { + sizeRounded = Math.Round(size / Math.Pow(SizeBase10, unitIndex - UnitEBIndex), 1); + } + else + { + sizeRounded = Math.Round(size / Math.Pow(SizeBase2, unitIndex), 1); + } + + string sizeFormatted = sizeRounded.ToString(CultureInfo.InvariantCulture); + + return $"{sizeFormatted} {_fileSizeUnitStrings[unitIndex]}"; + } + + #endregion + + #region Value parsers + + /// + /// Parses a string generated by and returns the original . + /// + /// A string representing a . + /// A object. If the input string couldn't been parsed, is returned. + public static TimeSpan ParseTimeSpan(string timeSpanString) + { + TimeSpan returnTimeSpan = TimeSpan.Zero; + + // An input string can either look like "01:23:45" or "1d, 01:23:45" if the timespan represents a duration of more than a day. + // Here, we split the input string to check if it's the former or the latter. + var valueSplit = timeSpanString.Split(", "); + if (valueSplit.Length > 1) + { + var dayPart = valueSplit[0].Split("d")[0]; + if (int.TryParse(dayPart, out int days)) + { + returnTimeSpan = returnTimeSpan.Add(TimeSpan.FromDays(days)); + } + } + + if (TimeSpan.TryParse(valueSplit.Last(), out TimeSpan parsedTimeSpan)) + { + returnTimeSpan = returnTimeSpan.Add(parsedTimeSpan); + } + + return returnTimeSpan; + } + + /// + /// Parses a string generated by and returns the original . + /// + /// The string representing a . + /// A object. If the input string couldn't be parsed, is returned. + public static DateTime ParseDateTime(string dateTimeString) + { + if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime)) + { + // Games that were never played are supposed to appear before the oldest played games in the list, + // so returning DateTime.UnixEpoch here makes sense. + return DateTime.UnixEpoch; + } + + return parsedDateTime; + } + + /// + /// Parses a string generated by and returns a representing a number of bytes. + /// + /// A string representing a file size formatted with . + /// A representing a number of bytes. + public static long ParseFileSize(string sizeString) + { + // Enumerating over the units backwards because otherwise, sizeString.EndsWith("B") would exit the loop in the first iteration. + for (int i = _fileSizeUnitStrings.Length - 1; i >= 0; i--) + { + string unit = _fileSizeUnitStrings[i]; + if (!sizeString.EndsWith(unit)) + { + continue; + } + + string numberString = sizeString.Split(" ")[0]; + if (!double.TryParse(numberString, CultureInfo.InvariantCulture, out double number)) + { + break; + } + + double sizeBase = SizeBase2; + + // If the unit index is one that points to a base 10 unit in the FileSizeUnitStrings array, subtract 6 to arrive at a usable power value. + if (i > UnitEBIndex) + { + i -= UnitEBIndex; + sizeBase = SizeBase10; + } + + number *= Math.Pow(sizeBase, i); + + return Convert.ToInt64(number); + } + + return 0; + } + + #endregion + } +} diff --git a/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs similarity index 98% rename from src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs rename to src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs index 08aa452eb..5f1ab5416 100644 --- a/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs @@ -5,7 +5,7 @@ using System.Globalization; using System.IO; using System.Runtime.Versioning; -namespace Ryujinx.Common.SystemInfo +namespace Ryujinx.Ui.Common.SystemInfo { [SupportedOSPlatform("linux")] class LinuxSystemInfo : SystemInfo diff --git a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs similarity index 99% rename from src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs rename to src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs index a968ad17b..3508ae3a4 100644 --- a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; -namespace Ryujinx.Common.SystemInfo +namespace Ryujinx.Ui.Common.SystemInfo { [SupportedOSPlatform("macos")] partial class MacOSSystemInfo : SystemInfo diff --git a/src/Ryujinx.Common/SystemInfo/SystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs similarity index 87% rename from src/Ryujinx.Common/SystemInfo/SystemInfo.cs rename to src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs index 55ec0127c..6a4fe6803 100644 --- a/src/Ryujinx.Common/SystemInfo/SystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs @@ -1,10 +1,11 @@ using Ryujinx.Common.Logging; +using Ryujinx.Ui.Common.Helper; using System; using System.Runtime.InteropServices; using System.Runtime.Intrinsics.X86; using System.Text; -namespace Ryujinx.Common.SystemInfo +namespace Ryujinx.Ui.Common.SystemInfo { public class SystemInfo { @@ -20,13 +21,13 @@ namespace Ryujinx.Common.SystemInfo CpuName = "Unknown"; } - private static string ToMiBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : $"{bytesValue / 1024 / 1024} MiB"; + private static string ToGBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : ValueFormatUtils.FormatFileSize((long)bytesValue, ValueFormatUtils.FileSizeUnits.Gibibytes); public void Print() { Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}"); Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}"); - Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToMiBString(RamTotal)} ; Available {ToMiBString(RamAvailable)}"); + Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToGBString(RamTotal)} ; Available {ToGBString(RamAvailable)}"); } public static SystemInfo Gather() diff --git a/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs similarity index 98% rename from src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs rename to src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs index 3b36d6e2e..9bb0fbf74 100644 --- a/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs +++ b/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs @@ -4,7 +4,7 @@ using System.Management; using System.Runtime.InteropServices; using System.Runtime.Versioning; -namespace Ryujinx.Common.SystemInfo +namespace Ryujinx.Ui.Common.SystemInfo { [SupportedOSPlatform("windows")] partial class WindowsSystemInfo : SystemInfo diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 50151d733..afb6a9925 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -3,7 +3,6 @@ using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; -using Ryujinx.Common.SystemInfo; using Ryujinx.Common.SystemInterop; using Ryujinx.Modules; using Ryujinx.SDL2.Common; @@ -11,6 +10,7 @@ using Ryujinx.Ui; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; +using Ryujinx.Ui.Common.SystemInfo; using Ryujinx.Ui.Widgets; using SixLabors.ImageSharp.Formats.Jpeg; using System; diff --git a/src/Ryujinx/Ui/Helper/SortHelper.cs b/src/Ryujinx/Ui/Helper/SortHelper.cs index 0c0eefd2c..c7a72ab9b 100644 --- a/src/Ryujinx/Ui/Helper/SortHelper.cs +++ b/src/Ryujinx/Ui/Helper/SortHelper.cs @@ -1,4 +1,5 @@ using Gtk; +using Ryujinx.Ui.Common.Helper; using System; namespace Ryujinx.Ui.Helper @@ -7,88 +8,26 @@ namespace Ryujinx.Ui.Helper { public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b) { - static string ReverseFormat(string time) - { - if (time == "Never") - { - return "00"; - } + TimeSpan aTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(a, 5).ToString()); + TimeSpan bTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(b, 5).ToString()); - var numbers = time.Split(new char[] { 'd', 'h', 'm' }); - - time = time.Replace(" ", "").Replace("d", ".").Replace("h", ":").Replace("m", ""); - - if (numbers.Length == 2) - { - return $"00.00:{time}"; - } - else if (numbers.Length == 3) - { - return $"00.{time}"; - } - - return time; - } - - string aValue = ReverseFormat(model.GetValue(a, 5).ToString()); - string bValue = ReverseFormat(model.GetValue(b, 5).ToString()); - - return TimeSpan.Compare(TimeSpan.Parse(aValue), TimeSpan.Parse(bValue)); + return TimeSpan.Compare(aTimeSpan, bTimeSpan); } public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b) { - string aValue = model.GetValue(a, 6).ToString(); - string bValue = model.GetValue(b, 6).ToString(); + DateTime aDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(a, 6).ToString()); + DateTime bDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(b, 6).ToString()); - if (aValue == "Never") - { - aValue = DateTime.UnixEpoch.ToString(); - } - - if (bValue == "Never") - { - bValue = DateTime.UnixEpoch.ToString(); - } - - return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue)); + return DateTime.Compare(aDateTime, bDateTime); } public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b) { - string aValue = model.GetValue(a, 8).ToString(); - string bValue = model.GetValue(b, 8).ToString(); + long aSize = ValueFormatUtils.ParseFileSize(model.GetValue(a, 8).ToString()); + long bSize = ValueFormatUtils.ParseFileSize(model.GetValue(b, 8).ToString()); - if (aValue[^3..] == "GiB") - { - aValue = (float.Parse(aValue[0..^3]) * 1024).ToString(); - } - else - { - aValue = aValue[0..^3]; - } - - if (bValue[^3..] == "GiB") - { - bValue = (float.Parse(bValue[0..^3]) * 1024).ToString(); - } - else - { - bValue = bValue[0..^3]; - } - - if (float.Parse(aValue) > float.Parse(bValue)) - { - return -1; - } - else if (float.Parse(bValue) > float.Parse(aValue)) - { - return 1; - } - else - { - return 0; - } + return aSize.CompareTo(bSize); } } } diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index a9d4be109..8b0b35e6c 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -954,7 +954,7 @@ namespace Ryujinx.Ui ApplicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata => { - appMetadata.LastPlayed = DateTime.UtcNow; + appMetadata.UpdatePreGame(); }); } } @@ -1097,13 +1097,7 @@ namespace Ryujinx.Ui { ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => { - if (appMetadata.LastPlayed.HasValue) - { - double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds; - appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); - } - - appMetadata.LastPlayed = DateTime.UtcNow; + appMetadata.UpdatePostGame(); }); } } @@ -1177,10 +1171,10 @@ namespace Ryujinx.Ui $"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}", args.AppData.Developer, args.AppData.Version, - args.AppData.TimePlayed, + args.AppData.TimePlayedString, args.AppData.LastPlayedString, args.AppData.FileExtension, - args.AppData.FileSize, + args.AppData.FileSizeString, args.AppData.Path, args.AppData.ControlHolder); }); From 815819767c5794624e3e7bc2bebcabe8ea4de0f6 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Tue, 7 Nov 2023 13:24:10 -0300 Subject: [PATCH 20/40] Force all exclusive memory accesses to be ordered on AppleHv (#5898) * Implement reprotect method on virtual memory manager (currently stubbed) * Force all exclusive memory accesses to be ordered on AppleHv * Format whitespace * Fix test build * Fix comment copy/paste * Fix wrong bit for isLoad * Update src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs Co-authored-by: riperiperi --------- Co-authored-by: riperiperi --- src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs | 62 +++++++++++++++++++ src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs | 33 +++++----- src/Ryujinx.Cpu/Jit/MemoryManager.cs | 23 ++----- .../Jit/MemoryManagerHostMapped.cs | 6 ++ .../HOS/Kernel/Memory/KMemoryPermission.cs | 46 ++++++++++++++ .../HOS/Kernel/Memory/KPageTable.cs | 10 +-- .../HOS/Kernel/Memory/KPageTableBase.cs | 6 +- .../HOS/Kernel/Memory/MemoryPermission.cs | 20 ------ src/Ryujinx.Memory/AddressSpaceManager.cs | 5 ++ src/Ryujinx.Memory/IVirtualMemoryManager.cs | 14 +++++ .../MockVirtualMemoryManager.cs | 5 ++ 11 files changed, 171 insertions(+), 59 deletions(-) create mode 100644 src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs create mode 100644 src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs delete mode 100644 src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs diff --git a/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs b/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs new file mode 100644 index 000000000..876597b78 --- /dev/null +++ b/src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs @@ -0,0 +1,62 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace Ryujinx.Cpu.AppleHv +{ + static class HvCodePatcher + { + private const uint XMask = 0x3f808000u; + private const uint XValue = 0x8000000u; + + private const uint ZrIndex = 31u; + + public static void RewriteUnorderedExclusiveInstructions(Span code) + { + Span codeUint = MemoryMarshal.Cast(code); + Span> codeVector = MemoryMarshal.Cast>(code); + + Vector128 mask = Vector128.Create(XMask); + Vector128 value = Vector128.Create(XValue); + + for (int index = 0; index < codeVector.Length; index++) + { + Vector128 v = codeVector[index]; + + if (Vector128.EqualsAny(Vector128.BitwiseAnd(v, mask), value)) + { + int baseIndex = index * 4; + + for (int instIndex = baseIndex; instIndex < baseIndex + 4; instIndex++) + { + ref uint inst = ref codeUint[instIndex]; + + if ((inst & XMask) != XValue) + { + continue; + } + + bool isPair = (inst & (1u << 21)) != 0; + bool isLoad = (inst & (1u << 22)) != 0; + + uint rt2 = (inst >> 10) & 0x1fu; + uint rs = (inst >> 16) & 0x1fu; + + if (isLoad && rs != ZrIndex) + { + continue; + } + + if (!isPair && rt2 != ZrIndex) + { + continue; + } + + // Set the ordered flag. + inst |= 1u << 15; + } + } + } + } + } +} diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs index d5ce817a4..947c37100 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs @@ -128,21 +128,6 @@ namespace Ryujinx.Cpu.AppleHv } } -#pragma warning disable IDE0051 // Remove unused private member - /// - /// Ensures the combination of virtual address and size is part of the addressable space and fully mapped. - /// - /// Virtual address of the range - /// Size of the range in bytes - private void AssertMapped(ulong va, ulong size) - { - if (!ValidateAddressAndSize(va, size) || !IsRangeMappedImpl(va, size)) - { - throw new InvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}"); - } - } -#pragma warning restore IDE0051 - /// public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags) { @@ -736,6 +721,24 @@ namespace Ryujinx.Cpu.AppleHv return (int)(vaSpan / PageSize); } + /// + public void Reprotect(ulong va, ulong size, MemoryPermission protection) + { + if (protection.HasFlag(MemoryPermission.Execute)) + { + // Some applications use unordered exclusive memory access instructions + // where it is not valid to do so, leading to memory re-ordering that + // makes the code behave incorrectly on some CPUs. + // To work around this, we force all such accesses to be ordered. + + using WritableRegion writableRegion = GetWritableRegion(va, (int)size); + + HvCodePatcher.RewriteUnorderedExclusiveInstructions(writableRegion.Memory.Span); + } + + // TODO + } + /// public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) { diff --git a/src/Ryujinx.Cpu/Jit/MemoryManager.cs b/src/Ryujinx.Cpu/Jit/MemoryManager.cs index 1c27e97fa..912e3f7e3 100644 --- a/src/Ryujinx.Cpu/Jit/MemoryManager.cs +++ b/src/Ryujinx.Cpu/Jit/MemoryManager.cs @@ -575,24 +575,17 @@ namespace Ryujinx.Cpu.Jit } } -#pragma warning disable IDE0051 // Remove unused private member - private ulong GetPhysicalAddress(ulong va) - { - // We return -1L if the virtual address is invalid or unmapped. - if (!ValidateAddress(va) || !IsMapped(va)) - { - return ulong.MaxValue; - } - - return GetPhysicalAddressInternal(va); - } -#pragma warning restore IDE0051 - private ulong GetPhysicalAddressInternal(ulong va) { return PteToPa(_pageTable.Read((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask); } + /// + public void Reprotect(ulong va, ulong size, MemoryPermission protection) + { + // TODO + } + /// public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) { @@ -698,9 +691,5 @@ namespace Ryujinx.Cpu.Jit /// Disposes of resources used by the memory manager. /// protected override void Destroy() => _pageTable.Dispose(); - -#pragma warning disable IDE0051 // Remove unused private member - private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message); -#pragma warning restore IDE0051 } } diff --git a/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs b/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs index 010a0bc25..6d32787ac 100644 --- a/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs +++ b/src/Ryujinx.Cpu/Jit/MemoryManagerHostMapped.cs @@ -615,6 +615,12 @@ namespace Ryujinx.Cpu.Jit return (int)(vaSpan / PageSize); } + /// + public void Reprotect(ulong va, ulong size, MemoryPermission protection) + { + // TODO + } + /// public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) { diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs new file mode 100644 index 000000000..32734574e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs @@ -0,0 +1,46 @@ +using Ryujinx.Memory; +using System; + +namespace Ryujinx.HLE.HOS.Kernel.Memory +{ + [Flags] + enum KMemoryPermission : uint + { + None = 0, + UserMask = Read | Write | Execute, + Mask = uint.MaxValue, + + Read = 1 << 0, + Write = 1 << 1, + Execute = 1 << 2, + DontCare = 1 << 28, + + ReadAndWrite = Read | Write, + ReadAndExecute = Read | Execute, + } + + static class KMemoryPermissionExtensions + { + public static MemoryPermission Convert(this KMemoryPermission permission) + { + MemoryPermission output = MemoryPermission.None; + + if (permission.HasFlag(KMemoryPermission.Read)) + { + output = MemoryPermission.Read; + } + + if (permission.HasFlag(KMemoryPermission.Write)) + { + output |= MemoryPermission.Write; + } + + if (permission.HasFlag(KMemoryPermission.Execute)) + { + output |= MemoryPermission.Execute; + } + + return output; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs index dcfc8f4ff..4cd3e6fdd 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTable.cs @@ -203,15 +203,17 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory /// protected override Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission) { - // TODO. + _cpuMemory.Reprotect(address, pagesCount * PageSize, permission.Convert()); + return Result.Success; } /// - protected override Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission) + protected override Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission) { - // TODO. - return Result.Success; + // TODO: Flush JIT cache. + + return Reprotect(address, pagesCount, permission); } /// diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs index 2b00f802a..2b6d4e4e9 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KPageTableBase.cs @@ -1255,7 +1255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory if ((oldPermission & KMemoryPermission.Execute) != 0) { - result = ReprotectWithAttributes(address, pagesCount, permission); + result = ReprotectAndFlush(address, pagesCount, permission); } else { @@ -3036,13 +3036,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory protected abstract Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission); /// - /// Changes the permissions of a given virtual memory region. + /// Changes the permissions of a given virtual memory region, while also flushing the cache. /// /// Virtual address of the region to have the permission changes /// Number of pages to have their permissions changed /// New permission /// Result of the permission change operation - protected abstract Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission); + protected abstract Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission); /// /// Alerts the memory tracking that a given region has been read from or written to. diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs deleted file mode 100644 index 068cdbb88..000000000 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/MemoryPermission.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Ryujinx.HLE.HOS.Kernel.Memory -{ - [Flags] - enum KMemoryPermission : uint - { - None = 0, - UserMask = Read | Write | Execute, - Mask = uint.MaxValue, - - Read = 1 << 0, - Write = 1 << 1, - Execute = 1 << 2, - DontCare = 1 << 28, - - ReadAndWrite = Read | Write, - ReadAndExecute = Read | Execute, - } -} diff --git a/src/Ryujinx.Memory/AddressSpaceManager.cs b/src/Ryujinx.Memory/AddressSpaceManager.cs index 65b4d48f2..b8d48365c 100644 --- a/src/Ryujinx.Memory/AddressSpaceManager.cs +++ b/src/Ryujinx.Memory/AddressSpaceManager.cs @@ -455,6 +455,11 @@ namespace Ryujinx.Memory return _pageTable.Read(va) + (nuint)(va & PageMask); } + /// + public void Reprotect(ulong va, ulong size, MemoryPermission protection) + { + } + /// public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) { diff --git a/src/Ryujinx.Memory/IVirtualMemoryManager.cs b/src/Ryujinx.Memory/IVirtualMemoryManager.cs index edbfc8855..8c9ca1684 100644 --- a/src/Ryujinx.Memory/IVirtualMemoryManager.cs +++ b/src/Ryujinx.Memory/IVirtualMemoryManager.cs @@ -104,6 +104,12 @@ namespace Ryujinx.Memory /// True if the data was changed, false otherwise bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan data); + /// + /// Fills the specified memory region with the value specified in . + /// + /// Virtual address to fill the value into + /// Size of the memory region to fill + /// Value to fill with void Fill(ulong va, ulong size, byte value) { const int MaxChunkSize = 1 << 24; @@ -194,6 +200,14 @@ namespace Ryujinx.Memory /// Optional ID of the handles that should not be signalled void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null); + /// + /// Reprotect a region of virtual memory for guest access. + /// + /// Virtual address base + /// Size of the region to protect + /// Memory protection to set + void Reprotect(ulong va, ulong size, MemoryPermission protection); + /// /// Reprotect a region of virtual memory for tracking. /// diff --git a/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs b/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs index 59dc1a525..435bb35ae 100644 --- a/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs +++ b/src/Ryujinx.Tests.Memory/MockVirtualMemoryManager.cs @@ -102,6 +102,11 @@ namespace Ryujinx.Tests.Memory throw new NotImplementedException(); } + public void Reprotect(ulong va, ulong size, MemoryPermission protection) + { + throw new NotImplementedException(); + } + public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection) { OnProtect?.Invoke(va, size, protection); From c3555cb5d6c088bdb2e84ef16889a9f8c76d4319 Mon Sep 17 00:00:00 2001 From: jcm Date: Sat, 11 Nov 2023 08:27:53 -0600 Subject: [PATCH 21/40] UI: Change default hide cursor mode to OnIdle (#5906) Co-authored-by: jcm --- src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index 9ed8fd8cc..c79fa56c6 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -784,7 +784,7 @@ namespace Ryujinx.Ui.Common.Configuration EnableDiscordIntegration.Value = true; CheckUpdatesOnStart.Value = true; ShowConfirmExit.Value = true; - HideCursor.Value = HideCursorMode.Never; + HideCursor.Value = HideCursorMode.OnIdle; Graphics.EnableVsync.Value = true; Graphics.EnableShaderCache.Value = true; Graphics.EnableTextureRecompression.Value = false; From 7e6342e44ddb004b0600f6bb4e6169aba6e4bf7c Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Sat, 11 Nov 2023 09:57:15 -0500 Subject: [PATCH 22/40] Add accelerator keys for Options and Help (#5884) --- src/Ryujinx.Ava/Assets/Locales/en_US.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index 62aac1227..bc2bbfe82 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -14,7 +14,7 @@ "MenuBarFileOpenEmuFolder": "Open Ryujinx Folder", "MenuBarFileOpenLogsFolder": "Open Logs Folder", "MenuBarFileExit": "_Exit", - "MenuBarOptions": "Options", + "MenuBarOptions": "_Options", "MenuBarOptionsToggleFullscreen": "Toggle Fullscreen", "MenuBarOptionsStartGamesInFullscreen": "Start Games in Fullscreen Mode", "MenuBarOptionsStopEmulation": "Stop Emulation", @@ -30,7 +30,7 @@ "MenuBarToolsManageFileTypes": "Manage file types", "MenuBarToolsInstallFileTypes": "Install file types", "MenuBarToolsUninstallFileTypes": "Uninstall file types", - "MenuBarHelp": "Help", + "MenuBarHelp": "_Help", "MenuBarHelpCheckForUpdates": "Check for Updates", "MenuBarHelpAbout": "About", "MenuSearch": "Search...", From 55557525b16f8256d91f769e026874b5c70c3b2d Mon Sep 17 00:00:00 2001 From: NitroTears <73270647+NitroTears@users.noreply.github.com> Date: Sun, 12 Nov 2023 01:08:42 +1000 Subject: [PATCH 23/40] Create Desktop Shortcut fixes (#5852) * remove duplicate basePath arg, add --fullscreen arg * Changing FriendlyName to set "Ryujinx" text * Fix GetArgsString using the base path * Change desktop path to the Applications folder when creating shortcut on Mac Co-authored-by: Nicko Anastassiu <134955950+nickoanastassiu@users.noreply.github.com> * Move Create Shortcut button to top of context menu --------- Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Co-authored-by: Nicko Anastassiu <134955950+nickoanastassiu@users.noreply.github.com> --- .../UI/Controls/ApplicationContextMenu.axaml | 10 +++++----- src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs | 18 +++++++----------- .../Widgets/GameTableContextMenu.Designer.cs | 3 ++- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml index d81050f83..b8fe7e76f 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml @@ -12,6 +12,11 @@ Click="ToggleFavorite_Click" Header="{locale:Locale GameListContextMenuToggleFavorite}" ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" /> + - diff --git a/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs index dab473fa3..103b78c24 100644 --- a/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs +++ b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs @@ -30,7 +30,7 @@ namespace Ryujinx.Ui.Common.Helper graphic.DrawImage(image, 0, 0, 128, 128); SaveBitmapAsIcon(bitmap, iconPath); - var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(basePath, applicationFilePath), iconPath, 0); + var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath), iconPath, 0); shortcut.StringData.NameString = cleanedAppName; shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk")); } @@ -46,16 +46,16 @@ namespace Ryujinx.Ui.Common.Helper image.SaveAsPng(iconPath); using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop")); - outputFile.Write(desktopFile, cleanedAppName, iconPath, GetArgsString(basePath, applicationFilePath)); + outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath)}"); } [SupportedOSPlatform("macos")] private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName) { - string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName); + string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx"); var plistFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.plist"); // Macos .App folder - string contentFolderPath = Path.Combine(desktopPath, cleanedAppName + ".app", "Contents"); + string contentFolderPath = Path.Combine("/Applications", cleanedAppName + ".app", "Contents"); string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS"); if (!Directory.Exists(scriptFolderPath)) @@ -69,7 +69,7 @@ namespace Ryujinx.Ui.Common.Helper using StreamWriter scriptFile = new(scriptPath); scriptFile.WriteLine("#!/bin/sh"); - scriptFile.WriteLine(GetArgsString(basePath, appFilePath)); + scriptFile.WriteLine($"{basePath} {GetArgsString(appFilePath)}"); // Set execute permission FileInfo fileInfo = new(scriptPath); @@ -125,13 +125,10 @@ namespace Ryujinx.Ui.Common.Helper throw new NotImplementedException("Shortcut support has not been implemented yet for this OS."); } - private static string GetArgsString(string basePath, string appFilePath) + private static string GetArgsString(string appFilePath) { // args are first defined as a list, for easier adjustments in the future - var argsList = new List - { - basePath, - }; + var argsList = new List(); if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg)) { @@ -141,7 +138,6 @@ namespace Ryujinx.Ui.Common.Helper argsList.Add($"\"{appFilePath}\""); - return String.Join(" ", argsList); } diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs index 75b166136..734437eea 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs @@ -211,6 +211,8 @@ namespace Ryujinx.Ui.Widgets _manageSubMenu.Append(_openPtcDirMenuItem); _manageSubMenu.Append(_openShaderCacheDirMenuItem); + Add(_createShortcutMenuItem); + Add(new SeparatorMenuItem()); Add(_openSaveUserDirMenuItem); Add(_openSaveDeviceDirMenuItem); Add(_openSaveBcatDirMenuItem); @@ -223,7 +225,6 @@ namespace Ryujinx.Ui.Widgets Add(new SeparatorMenuItem()); Add(_manageCacheMenuItem); Add(_extractMenuItem); - Add(_createShortcutMenuItem); ShowAll(); } From 5c3cfb84c09b0566da677425915afa0b2d76da55 Mon Sep 17 00:00:00 2001 From: TSRBerry <20988865+TSRBerry@users.noreply.github.com> Date: Sat, 11 Nov 2023 21:56:57 +0100 Subject: [PATCH 24/40] Add support for multi game XCIs (#5638) * Add default values to ApplicationData directly * Refactor application loading It should now be possible to load multi game XCIs. Included updates won't be detected for now. Opening a game from the command line currently only opens the first one. * Only include program NCAs where at least one tuple item is not null * Get application data by title id and add programIndex check back * Refactor application loading again and remove duplicate code * Actually use patch ncas for updates * Fix number of applications found with multi game xcis * Don't load bundled updates from multi game xcis * Change ApplicationData.TitleId type to ulong & Add TitleIdString property * Use cnmt files and ContentCollection to load programs * Ava: Add updates and DLCs from gamecarts * Get the cnmt file from its NCA * Ava: Identify bundled updates in updater window * Fix the (hopefully) last few bugs * Add idOffset parameter to GetNcaByType * Handle missing file for dlc.json * Ava: Shorten error message for invalid files * Gtk: Add additional string for bundled updates in TitleUpdateWindow * Hopefully fix DLC issues * Apply formatting * Finally fix DLC issues * Adjust property names and fileSize field * Read the correct update file * Fix wrong casing for application id strings * Rename TitleId to ApplicationId * Address review comments * Fix formatting issues * Apply suggestions from code review Co-authored-by: gdkchan * Gracefully fail when loading pfs for update and dlc window * Fix applications with multiple programs * Fix DLCWindow crash on GTK * Fix some GUI issues * Remove IsXci again --------- Co-authored-by: gdkchan --- src/Ryujinx.Ava/AppHost.cs | 9 +- src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 + src/Ryujinx.Ava/Common/ApplicationHelper.cs | 9 +- .../Controls/ApplicationContextMenu.axaml.cs | 54 +- .../UI/Controls/ApplicationGridView.axaml | 2 +- .../UI/Controls/ApplicationListView.axaml | 4 +- .../UI/Models/DownloadableContentModel.cs | 6 +- src/Ryujinx.Ava/UI/Models/SaveModel.cs | 4 +- src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs | 5 +- .../DownloadableContentManagerViewModel.cs | 60 +- .../UI/ViewModels/MainWindowViewModel.cs | 47 +- .../UI/ViewModels/TitleUpdateViewModel.cs | 56 +- .../UI/Views/Main/MainMenuBarView.axaml.cs | 10 +- .../UI/Views/Main/MainViewControls.axaml | 2 +- .../UI/Windows/CheatWindow.axaml.cs | 7 +- .../DownloadableContentManagerWindow.axaml | 2 +- .../DownloadableContentManagerWindow.axaml.cs | 12 +- .../UI/Windows/MainWindow.axaml.cs | 19 +- .../UI/Windows/TitleUpdateWindow.axaml.cs | 14 +- .../FileSystem/ContentCollection.cs | 61 ++ .../Processes/Extensions/NcaExtensions.cs | 100 +- .../PartitionFileSystemExtensions.cs | 153 +-- .../Loaders/Processes/ProcessLoader.cs | 8 +- .../Loaders/Processes/ProcessLoaderHelper.cs | 9 +- src/Ryujinx.HLE/Switch.cs | 8 +- src/Ryujinx.Ui.Common/App/ApplicationData.cs | 18 +- .../App/ApplicationLibrary.cs | 916 +++++++++--------- src/Ryujinx/Program.cs | 8 +- src/Ryujinx/Ui/MainWindow.cs | 95 +- .../Ui/Widgets/GameTableContextMenu.cs | 89 +- src/Ryujinx/Ui/Windows/CheatWindow.cs | 9 +- src/Ryujinx/Ui/Windows/DlcWindow.cs | 123 +-- src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs | 63 +- 33 files changed, 1168 insertions(+), 816 deletions(-) create mode 100644 src/Ryujinx.HLE/FileSystem/ContentCollection.cs diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index 4d751e2a9..053d5b521 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -54,8 +54,6 @@ using System.Threading.Tasks; using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; using Image = SixLabors.ImageSharp.Image; -using InputManager = Ryujinx.Input.HLE.InputManager; -using IRenderer = Ryujinx.Graphics.GAL.IRenderer; using Key = Ryujinx.Input.Key; using MouseButton = Ryujinx.Input.MouseButton; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; @@ -123,12 +121,14 @@ namespace Ryujinx.Ava public int Width { get; private set; } public int Height { get; private set; } public string ApplicationPath { get; private set; } + public ulong ApplicationId { get; private set; } public bool ScreenshotRequested { get; set; } public AppHost( RendererHost renderer, InputManager inputManager, string applicationPath, + ulong applicationId, VirtualFileSystem virtualFileSystem, ContentManager contentManager, AccountManager accountManager, @@ -152,6 +152,7 @@ namespace Ryujinx.Ava NpadManager = _inputManager.CreateNpadManager(); TouchScreenManager = _inputManager.CreateTouchScreenManager(); ApplicationPath = applicationPath; + ApplicationId = applicationId; VirtualFileSystem = virtualFileSystem; ContentManager = contentManager; @@ -641,7 +642,7 @@ namespace Ryujinx.Ava { Logger.Info?.Print(LogClass.Application, "Loading as XCI."); - if (!Device.LoadXci(ApplicationPath)) + if (!Device.LoadXci(ApplicationPath, ApplicationId)) { Device.Dispose(); @@ -668,7 +669,7 @@ namespace Ryujinx.Ava { Logger.Info?.Print(LogClass.Application, "Loading as NSP."); - if (!Device.LoadNsp(ApplicationPath)) + if (!Device.LoadNsp(ApplicationPath, ApplicationId)) { Device.Dispose(); diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index bc2bbfe82..be3e35a9c 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -539,6 +539,8 @@ "OpenSetupGuideMessage": "Open the Setup Guide", "NoUpdate": "No Update", "TitleUpdateVersionLabel": "Version {0}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Confirmation", "FileDialogAllTypes": "All types", diff --git a/src/Ryujinx.Ava/Common/ApplicationHelper.cs b/src/Ryujinx.Ava/Common/ApplicationHelper.cs index 91ca8f4d5..dd4643297 100644 --- a/src/Ryujinx.Ava/Common/ApplicationHelper.cs +++ b/src/Ryujinx.Ava/Common/ApplicationHelper.cs @@ -18,7 +18,8 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.Ui.App.Common; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; using System; using System.Buffers; @@ -226,7 +227,11 @@ namespace Ryujinx.Ava.Common return; } - (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _); if (updatePatchNca != null) { patchNca = updatePatchNca; diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs index 0f0071065..69465c7c7 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs @@ -1,7 +1,6 @@ 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,7 +14,6 @@ using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using Path = System.IO.Path; @@ -41,7 +39,7 @@ namespace Ryujinx.Ava.UI.Controls { viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; - ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata => + ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata => { appMetadata.Favorite = viewModel.SelectedApplication.Favorite; }); @@ -76,19 +74,9 @@ namespace Ryujinx.Ava.UI.Controls { if (viewModel?.SelectedApplication != null) { - if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - Dispatcher.UIThread.InvokeAsync(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); - }); + var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default); - return; - } - - var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default); - - ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName); + ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name); } } @@ -98,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); } } @@ -108,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); + await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); } } @@ -120,8 +108,8 @@ namespace Ryujinx.Ava.UI.Controls { await new CheatWindow( viewModel.VirtualFileSystem, - viewModel.SelectedApplication.TitleId, - viewModel.SelectedApplication.TitleName, + viewModel.SelectedApplication.IdString, + viewModel.SelectedApplication.Name, viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window); } } @@ -133,7 +121,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.IdString); OpenHelper.OpenFolder(titleModsPath); } @@ -146,7 +134,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId); + string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.IdString); OpenHelper.OpenFolder(titleModsPath); } @@ -160,15 +148,15 @@ namespace Ryujinx.Ava.UI.Controls { UserResult result = await ContentDialogHelper.CreateConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name), LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); if (result == UserResult.Yes) { - DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1")); + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); List cacheFiles = new(); @@ -208,14 +196,14 @@ namespace Ryujinx.Ava.UI.Controls { UserResult result = await ContentDialogHelper.CreateConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name), LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); if (result == UserResult.Yes) { - DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader")); + DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader")); List oldCacheDirectories = new(); List newCacheFiles = new(); @@ -263,7 +251,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu"); + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu"); string mainDir = Path.Combine(ptcDir, "0"); string backupDir = Path.Combine(ptcDir, "1"); @@ -284,7 +272,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"); + string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"); if (!Directory.Exists(shaderCacheDir)) { @@ -305,7 +293,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Code, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.TitleName); + viewModel.SelectedApplication.Name); } } @@ -319,7 +307,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Data, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.TitleName); + viewModel.SelectedApplication.Name); } } @@ -333,7 +321,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Logo, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.TitleName); + viewModel.SelectedApplication.Name); } } @@ -344,7 +332,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { ApplicationData selectedApplication = viewModel.SelectedApplication; - ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon); + ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.Name, selectedApplication.IdString, selectedApplication.Icon); } } @@ -354,7 +342,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await viewModel.LoadApplication(viewModel.SelectedApplication.Path); + await viewModel.LoadApplication(viewModel.SelectedApplication); } } } diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml index bbdb4c4a7..5919652e2 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml @@ -82,7 +82,7 @@ diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml index 9004f7518..24ec2b357 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml @@ -85,7 +85,7 @@ Path.GetFileName(ContainerPath); + public string Label => + Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName; + public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled) { TitleId = titleId; diff --git a/src/Ryujinx.Ava/UI/Models/SaveModel.cs b/src/Ryujinx.Ava/UI/Models/SaveModel.cs index 7b476932b..2e3ed3bae 100644 --- a/src/Ryujinx.Ava/UI/Models/SaveModel.cs +++ b/src/Ryujinx.Ava/UI/Models/SaveModel.cs @@ -46,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models TitleId = info.ProgramId; UserId = info.UserId; - var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString); + var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString); InGameList = appData != null; if (InGameList) { Icon = appData.Icon; - Title = appData.TitleName; + Title = appData.Name; } else { diff --git a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs index 3b44e8ee6..fae2a08d0 100644 --- a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs +++ b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs @@ -8,7 +8,10 @@ namespace Ryujinx.Ava.UI.Models public ApplicationControlProperty Control { get; } public string Path { get; } - public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString()); + public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue( + System.IO.Path.GetExtension(Path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel, + Control.DisplayVersionString.ToString() + ); public TitleUpdateModel(ApplicationControlProperty control, string path) { diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index cdecae77d..9f3a0045d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -17,11 +17,12 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.Ui.App.Common; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using Application = Avalonia.Application; using Path = System.IO.Path; @@ -38,7 +39,7 @@ namespace Ryujinx.Ava.UI.ViewModels private AvaloniaList _selectedDownloadableContents = new(); private string _search; - private readonly ulong _titleId; + private readonly ApplicationData _applicationData; private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -92,18 +93,25 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) + public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { _virtualFileSystem = virtualFileSystem; - _titleId = titleId; + _applicationData = applicationData; if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { StorageProvider = desktop.MainWindow.StorageProvider; } - _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json"); + _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json"); + + if (!File.Exists(_downloadableContentJsonPath)) + { + _downloadableContentContainerList = new List(); + + Save(); + } try { @@ -120,6 +128,9 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadDownloadableContents() { + // NOTE: Try to load downloadable contents from PFS first. + AddDownloadableContent(_applicationData.Path); + foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList) { if (File.Exists(downloadableContentContainer.ContainerPath)) @@ -127,7 +138,11 @@ namespace Ryujinx.Ava.UI.ViewModels using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); PartitionFileSystem partitionFileSystem = new(); - partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + + if (partitionFileSystem.Initialize(containerFile.AsStorage()).IsFailure()) + { + continue; + } _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -220,22 +235,34 @@ namespace Ryujinx.Ava.UI.ViewModels foreach (var file in result) { - await AddDownloadableContent(file.Path.LocalPath); + if (!AddDownloadableContent(file.Path.LocalPath)) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); + } } } - private async Task AddDownloadableContent(string path) + private bool AddDownloadableContent(string path) { if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null) { - return; + return true; } using FileStream containerFile = File.OpenRead(path); - PartitionFileSystem partitionFileSystem = new(); - partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - bool containsDownloadableContent = false; + IFileSystem partitionFileSystem; + + if (Path.GetExtension(path).ToLower() == ".xci") + { + partitionFileSystem = new Xci(_virtualFileSystem.KeySet, containerFile.AsStorage()).OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + partitionFileSystem = pfsTemp; + } _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -253,7 +280,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (nca.Header.ContentType == NcaContentType.PublicData) { - if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId) + if (nca.GetProgramIdBase() != _applicationData.IdBase) { break; } @@ -265,14 +292,11 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(nameof(UpdateCount)); Sort(); - containsDownloadableContent = true; + return true; } } - if (!containsDownloadableContent) - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); - } + return false; } public void Remove(DownloadableContentModel model) diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 80df5d398..692df483d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -95,7 +95,7 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _canUpdate = true; private Cursor _cursor; private string _title; - private string _currentEmulatedGamePath; + private ApplicationData _currentApplicationData; private readonly AutoResetEvent _rendererWaitEvent; private WindowState _windowState; private double _windowWidth; @@ -106,7 +106,6 @@ namespace Ryujinx.Ava.UI.ViewModels public ApplicationData ListSelectedApplication; public ApplicationData GridSelectedApplication; - private string TitleName { get; set; } internal AppHost AppHost { get; set; } public MainWindowViewModel() @@ -930,8 +929,8 @@ namespace Ryujinx.Ava.UI.ViewModels return SortMode switch { #pragma warning disable IDE0055 // Disable formatting - ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName) - : SortExpressionComparer.Descending(app => app.TitleName), + ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.Name) + : SortExpressionComparer.Descending(app => app.Name), ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer) : SortExpressionComparer.Descending(app => app.Developer), ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending), @@ -968,7 +967,7 @@ namespace Ryujinx.Ava.UI.ViewModels { if (arg is ApplicationData app) { - return string.IsNullOrWhiteSpace(_searchText) || app.TitleName.ToLower().Contains(_searchText.ToLower()); + return string.IsNullOrWhiteSpace(_searchText) || app.Name.ToLower().Contains(_searchText.ToLower()); } return false; @@ -1097,7 +1096,7 @@ namespace Ryujinx.Ava.UI.ViewModels IsLoadingIndeterminate = false; break; case LoadState.Loaded: - LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); IsLoadingIndeterminate = true; CacheLoadStatus = ""; break; @@ -1117,7 +1116,7 @@ namespace Ryujinx.Ava.UI.ViewModels IsLoadingIndeterminate = false; break; case ShaderCacheLoadingState.Loaded: - LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); IsLoadingIndeterminate = true; CacheLoadStatus = ""; break; @@ -1168,13 +1167,13 @@ namespace Ryujinx.Ava.UI.ViewModels { UserChannelPersistence.ShouldRestart = false; - await LoadApplication(_currentEmulatedGamePath); + await LoadApplication(_currentApplicationData); } else { // Otherwise, clear state. UserChannelPersistence = new UserChannelPersistence(); - _currentEmulatedGamePath = null; + _currentApplicationData = null; } } @@ -1451,7 +1450,12 @@ namespace Ryujinx.Ava.UI.ViewModels if (result.Count > 0) { - await LoadApplication(result[0].Path.LocalPath); + ApplicationData applicationData = new() + { + Path = result[0].Path.LocalPath, + }; + + await LoadApplication(applicationData); } } @@ -1465,11 +1469,17 @@ namespace Ryujinx.Ava.UI.ViewModels if (result.Count > 0) { - await LoadApplication(result[0].Path.LocalPath); + ApplicationData applicationData = new() + { + Name = Path.GetFileNameWithoutExtension(result[0].Path.LocalPath), + Path = result[0].Path.LocalPath, + }; + + await LoadApplication(applicationData); } } - public async Task LoadApplication(string path, bool startFullscreen = false, string titleName = "") + public async Task LoadApplication(ApplicationData application, bool startFullscreen = false) { if (AppHost != null) { @@ -1489,7 +1499,7 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language); + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); PrepareLoadScreen(); @@ -1498,7 +1508,8 @@ namespace Ryujinx.Ava.UI.ViewModels AppHost = new AppHost( RendererHostControl, InputManager, - path, + application.Path, + application.Id, VirtualFileSystem, ContentManager, AccountManager, @@ -1516,17 +1527,17 @@ namespace Ryujinx.Ava.UI.ViewModels CanUpdate = false; - LoadHeading = TitleName = titleName; + LoadHeading = application.Name; - if (string.IsNullOrWhiteSpace(titleName)) + if (string.IsNullOrWhiteSpace(application.Name)) { LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name); - TitleName = AppHost.Device.Processes.ActiveApplication.Name; + application.Name = AppHost.Device.Processes.ActiveApplication.Name; } SwitchToRenderer(startFullscreen); - _currentEmulatedGamePath = path; + _currentApplicationData = application; Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; gameThread.Start(); diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs index 5090a8c70..7bb96131d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs @@ -1,4 +1,3 @@ -using Avalonia; using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; @@ -8,6 +7,7 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Ns; +using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common.Locale; @@ -17,12 +17,16 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; +using Ryujinx.Ui.Common.Configuration; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Application = Avalonia.Application; +using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; using SpanHelpers = LibHac.Common.SpanHelpers; @@ -33,7 +37,7 @@ namespace Ryujinx.Ava.UI.ViewModels public TitleUpdateMetadata TitleUpdateWindowData; public readonly string TitleUpdateJsonPath; private VirtualFileSystem VirtualFileSystem { get; } - private ulong TitleId { get; } + private ApplicationData ApplicationData { get; } private AvaloniaList _titleUpdates = new(); private AvaloniaList _views = new(); @@ -73,18 +77,18 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) + public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { VirtualFileSystem = virtualFileSystem; - TitleId = titleId; + ApplicationData = applicationData; if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { StorageProvider = desktop.MainWindow.StorageProvider; } - TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); + TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json"); try { @@ -92,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels } catch { - Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {TitleId} at {TitleUpdateJsonPath}"); + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdString} at {TitleUpdateJsonPath}"); TitleUpdateWindowData = new TitleUpdateMetadata { @@ -108,6 +112,9 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadUpdates() { + // Try to load updates from PFS first + AddUpdate(ApplicationData.Path, true); + foreach (string path in TitleUpdateWindowData.Paths) { AddUpdate(path); @@ -162,17 +169,41 @@ namespace Ryujinx.Ava.UI.ViewModels } } - private void AddUpdate(string path) + private void AddUpdate(string path, bool ignoreNotFound = false) { if (File.Exists(path) && TitleUpdates.All(x => x.Path != path)) { + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + using FileStream file = new(path, FileMode.Open, FileAccess.Read); + IFileSystem pfs; + try { - var pfs = new PartitionFileSystem(); - pfs.Initialize(file.AsStorage()).ThrowIfFailure(); - (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0); + if (Path.GetExtension(path).ToLower() == ".xci") + { + pfs = new Xci(VirtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; + } + + Dictionary updates = pfs.GetUpdateData(VirtualFileSystem, checkLevel); + + Nca patchNca = null; + Nca controlNca = null; + + if (updates.TryGetValue(ApplicationData.Id, out ContentCollection content)) + { + patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program); + controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control); + } if (controlNca != null && patchNca != null) { @@ -187,7 +218,10 @@ namespace Ryujinx.Ava.UI.ViewModels } else { - Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); + if (!ignoreNotFound) + { + Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); + } } } catch (Exception ex) diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs index 4f2d262da..af3c5deb6 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs @@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Utilities; using Ryujinx.Modules; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -131,7 +132,14 @@ namespace Ryujinx.Ava.UI.Views.Main if (!string.IsNullOrEmpty(contentPath)) { - await ViewModel.LoadApplication(contentPath, false, "Mii Applet"); + ApplicationData applicationData = new() + { + Name = "miiEdit", + Id = 0x0100000000001009, + Path = contentPath, + }; + + await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen); } } diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml index cc21b5c60..7a716fb2a 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml @@ -104,7 +104,7 @@ Content="{locale:Locale GameListHeaderApplication}" GroupName="Sort" IsChecked="{Binding IsSortedByTitle, Mode=OneTime}" - Tag="Title" /> + Tag="Application" /> (); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper()); - BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath); + BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath); InitializeComponent(); diff --git a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml index 99cf28e77..98aac09ce 100644 --- a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml @@ -97,7 +97,7 @@ MaxLines="2" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" - Text="{Binding FileName}" /> + Text="{Binding Label}" /> x.OfType().Name("DialogSpace").Child().OfType()); diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs index c78f4160d..352ac4e54 100644 --- a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Threading; using FluentAvalonia.UI.Controls; +using LibHac.Tools.FsSystem; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; @@ -23,7 +24,6 @@ using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; using System; -using System.IO; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -139,9 +139,7 @@ namespace Ryujinx.Ava.UI.Windows { ViewModel.SelectedIcon = args.Application.Icon; - string path = new FileInfo(args.Application.Path).FullName; - - ViewModel.LoadApplication(path).Wait(); + ViewModel.LoadApplication(args.Application).Wait(); } args.Handled = true; @@ -190,7 +188,11 @@ namespace Ryujinx.Ava.UI.Windows LibHacHorizonManager.InitializeBcatServer(); LibHacHorizonManager.InitializeSystemClients(); - ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel); // Save data created before we supported extra data in directory save data will not work properly if // given empty extra data. Luckily some of that extra data can be created using the data from the @@ -297,7 +299,12 @@ namespace Ryujinx.Ava.UI.Windows { _deferLoad = false; - ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait(); + ApplicationData applicationData = new() + { + Path = _launchPath, + }; + + ViewModel.LoadApplication(applicationData, _startFullscreen).Wait(); } } else diff --git a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs index 7ece63355..8ecf165ce 100644 --- a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs @@ -7,15 +7,15 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System.Threading.Tasks; -using Button = Avalonia.Controls.Button; namespace Ryujinx.Ava.UI.Windows { public partial class TitleUpdateWindow : UserControl { - public TitleUpdateViewModel ViewModel; + public readonly TitleUpdateViewModel ViewModel; public TitleUpdateWindow() { @@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); } - public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId) + public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { - DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId); + DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData); InitializeComponent(); } - public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) { ContentDialog contentDialog = new() { PrimaryButtonText = "", SecondaryButtonText = "", CloseButtonText = "", - Content = new TitleUpdateWindow(virtualFileSystem, titleId), - Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")), + Content = new TitleUpdateWindow(virtualFileSystem, applicationData), + Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString), }; Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); diff --git a/src/Ryujinx.HLE/FileSystem/ContentCollection.cs b/src/Ryujinx.HLE/FileSystem/ContentCollection.cs new file mode 100644 index 000000000..1c19887be --- /dev/null +++ b/src/Ryujinx.HLE/FileSystem/ContentCollection.cs @@ -0,0 +1,61 @@ +using LibHac.Common.Keys; +using LibHac.Fs.Fsa; +using LibHac.Ncm; +using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Tools.Ncm; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using System; + +namespace Ryujinx.HLE.FileSystem +{ + /// + /// Thin wrapper around + /// + public class ContentCollection + { + private readonly IFileSystem _pfs; + private readonly Cnmt _cnmt; + + public ulong Id => _cnmt.TitleId; + public TitleVersion Version => _cnmt.TitleVersion; + public ContentMetaType Type => _cnmt.Type; + public ulong ApplicationId => _cnmt.ApplicationTitleId; + public ulong PatchId => _cnmt.PatchTitleId; + public TitleVersion RequiredSystemVersion => _cnmt.MinimumSystemVersion; + public TitleVersion RequiredApplicationVersion => _cnmt.MinimumApplicationVersion; + public byte[] Digest => _cnmt.Hash; + + public ulong ProgramBaseId => Id & ~0x1FFFUL; + public bool IsSystemTitle => _cnmt.Type < ContentMetaType.Application; + + public ContentCollection(IFileSystem pfs, Cnmt cnmt) + { + _pfs = pfs; + _cnmt = cnmt; + } + + public Nca GetNcaByType(KeySet keySet, ContentType type, int programIndex = 0) + { + // TODO: Replace this with a check for IdOffset as soon as LibHac supports it: + // && entry.IdOffset == programIndex + + foreach (var entry in _cnmt.ContentEntries) + { + if (entry.Type != type) + { + continue; + } + + string ncaId = BitConverter.ToString(entry.NcaId).Replace("-", null).ToLower(); + Nca nca = _pfs.GetNca(keySet, $"/{ncaId}.nca"); + + if (nca.GetProgramIndex() == programIndex) + { + return nca; + } + } + + return null; + } + } +} diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index 4568b44da..6863d1a7c 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -2,21 +2,31 @@ using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; +using LibHac.FsSystem; using LibHac.Loader; using LibHac.Ncm; using LibHac.Ns; +using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Tools.Ncm; +using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using System.IO; using System.Linq; using ApplicationId = LibHac.Ncm.ApplicationId; +using ContentType = LibHac.Ncm.ContentType; +using Path = System.IO.Path; namespace Ryujinx.HLE.Loaders.Processes.Extensions { - static class NcaExtensions + public static class NcaExtensions { + private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca) { // Extract RomFs and ExeFs from NCA. @@ -47,7 +57,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions nacpData = controlNca.GetNacp(device); } - /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update. + /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update. // Load program 0 control NCA as we are going to need it for display version. (_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); @@ -86,6 +96,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return processResult; } + public static ulong GetProgramIdBase(this Nca nca) + { + return nca.Header.TitleId & ~0x1FFFUL; + } + public static int GetProgramIndex(this Nca nca) { return (int)(nca.Header.TitleId & 0xF); @@ -96,6 +111,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Program; } + public static bool IsMain(this Nca nca) + { + return nca.IsProgram() && !nca.IsPatch(); + } + public static bool IsPatch(this Nca nca) { int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); @@ -108,6 +128,56 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Control; } + public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath) + { + updatePath = "(unknown)"; + + // Load Update NCAs. + Nca updatePatchNca = null; + Nca updateControlNca = null; + + // Clear the program index part. + ulong titleIdBase = mainNca.GetProgramIdBase(); + + // Load update information if exists. + string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json"); + if (File.Exists(titleUpdateMetadataPath)) + { + updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; + if (File.Exists(updatePath)) + { + var updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read); + + IFileSystem updatePartitionFileSystem; + + if (Path.GetExtension(updatePath).ToLower() == ".xci") + { + updatePartitionFileSystem = new Xci(fileSystem.KeySet, updateFile.AsStorage()).OpenPartition(XciPartitionType.Secure); + } + else + { + PartitionFileSystem pfsTemp = new(); + pfsTemp.Initialize(updateFile.AsStorage()).ThrowIfFailure(); + updatePartitionFileSystem = pfsTemp; + } + + foreach ((ulong updateTitleId, ContentCollection content) in updatePartitionFileSystem.GetUpdateData(fileSystem, checkLevel)) + { + if ((updateTitleId & ~0x1FFFUL) != titleIdBase) + { + continue; + } + + updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex); + updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex); + break; + } + } + } + + return (updatePatchNca, updateControlNca); + } + public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null) { IFileSystem exeFs = null; @@ -172,5 +242,31 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nacpData; } + + public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType) + { + string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt"; + using var cnmtFile = new UniqueRef(); + + try + { + Result result = cnmtNca.OpenFileSystem(0, checkLevel) + .OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read); + + if (result.IsSuccess()) + { + return new Cnmt(cnmtFile.Release().AsStream()); + } + } + catch (HorizonResultException ex) + { + if (!ResultFs.PathNotFound.Includes(ex.ResultValue)) + { + Logger.Warning?.Print(LogClass.Application, $"Failed get cnmt for '{cnmtNca.Header.TitleId:x16}' from nca: {ex.Message}"); + } + } + + return null; + } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs index 50f7d5853..5f45cd459 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs @@ -1,26 +1,87 @@ using LibHac.Common; +using LibHac.Common.Keys; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; +using LibHac.Ncm; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using LibHac.Tools.Ncm; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; +using ContentType = LibHac.Ncm.ContentType; namespace Ryujinx.HLE.Loaders.Processes.Extensions { public static class PartitionFileSystemExtensions { private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, out string errorMessage) + public static Dictionary GetApplicationData(this IFileSystem partitionFileSystem, + VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel) + { + fileSystem.ImportTickets(partitionFileSystem); + + var programs = new Dictionary(); + + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca")) + { + Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Application); + + if (cnmt == null) + { + continue; + } + + ContentCollection content = new(partitionFileSystem, cnmt); + + if (content.Type != ContentMetaType.Application) + { + continue; + } + + programs.TryAdd(content.ApplicationId, content); + } + + return programs; + } + + public static Dictionary GetUpdateData(this IFileSystem partitionFileSystem, + VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel) + { + fileSystem.ImportTickets(partitionFileSystem); + + var programs = new Dictionary(); + + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca")) + { + Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Patch); + + if (cnmt == null) + { + continue; + } + + ContentCollection content = new(partitionFileSystem, cnmt); + + if (content.Type != ContentMetaType.Patch) + { + continue; + } + + programs.TryAdd(content.ApplicationId, content); + } + + return programs; + } + + internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, ulong titleId, out string errorMessage) where TMetaData : PartitionFileSystemMetaCore, new() where TFormat : IPartitionFileSystemFormat where THeader : unmanaged, IPartitionFileSystemHeader @@ -35,31 +96,22 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions try { - device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem); + Dictionary applications = partitionFileSystem.GetApplicationData(device.FileSystem, device.System.FsIntegrityCheckLevel); - // TODO: To support multi-games container, this should use CNMT NCA instead. - foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) + if (titleId == 0) { - Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); - - if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index) + foreach ((ulong _, ContentCollection content) in applications) { - continue; - } - - if (nca.IsPatch()) - { - patchNca = nca; - } - else if (nca.IsProgram()) - { - mainNca = nca; - } - else if (nca.IsControl()) - { - controlNca = nca; + mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index); + controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index); + break; } } + else if (applications.TryGetValue(titleId, out ContentCollection content)) + { + mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index); + controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index); + } ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure(); } @@ -79,54 +131,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (false, ProcessResult.Failed); } - // Load Update NCAs. - Nca updatePatchNca = null; - Nca updateControlNca = null; - - if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) - { - // Clear the program index part. - titleIdBase &= ~0xFUL; - - // Load update information if exists. - string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); - if (File.Exists(titleUpdateMetadataPath)) - { - string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; - if (File.Exists(updatePath)) - { - PartitionFileSystem updatePartitionFileSystem = new(); - updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure(); - - device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem); - - // TODO: This should use CNMT NCA instead. - foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca")) - { - Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath); - - if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index) - { - continue; - } - - if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16")) - { - break; - } - - if (nca.IsProgram()) - { - updatePatchNca = nca; - } - else if (nca.IsControl()) - { - updateControlNca = nca; - } - } - } - } - } + (Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _); if (updatePatchNca != null) { @@ -168,18 +173,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (true, mainNca.Load(device, patchNca, controlNca)); } - errorMessage = "Unable to load: Could not find Main NCA"; + errorMessage = $"Unable to load: Could not find Main NCA for title \"{titleId:X16}\""; return (false, ProcessResult.Failed); } - public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path) + public static Nca GetNca(this IFileSystem fileSystem, KeySet keySet, string path) { using var ncaFile = new UniqueRef(); fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage()); + return new Nca(keySet, ncaFile.Release().AsStorage()); } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 220b868db..6b4a64be8 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes _processesByPid = new ConcurrentDictionary(); } - public bool LoadXci(string path) + public bool LoadXci(string path, ulong titleId) { FileStream stream = new(path, FileMode.Open, FileAccess.Read); Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage()); @@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage); + (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, titleId, out string errorMessage); if (!success) { @@ -66,13 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - public bool LoadNsp(string path) + public bool LoadNsp(string path, ulong titleId) { FileStream file = new(path, FileMode.Open, FileAccess.Read); PartitionFileSystem partitionFileSystem = new(); partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure(); - (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage); + (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, titleId, out string errorMessage); if (processResult.ProcessId == 0) { diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index c229b1742..110bb0928 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -42,15 +42,14 @@ namespace Ryujinx.HLE.Loaders.Processes foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) { - Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); + Nca nca = partitionFileSystem.GetNca(device.FileSystem.KeySet, fileEntry.FullPath); - if (!nca.IsProgram() && nca.IsPatch()) + if (!nca.IsProgram()) { continue; } - ulong currentProgramId = nca.Header.TitleId; - ulong currentMainProgramId = currentProgramId & ~0xFFFul; + ulong currentMainProgramId = nca.GetProgramIdBase(); if (applicationId == 0 && currentMainProgramId != 0) { @@ -67,7 +66,7 @@ namespace Ryujinx.HLE.Loaders.Processes break; } - hasIndex[(int)(currentProgramId & 0xF)] = true; + hasIndex[nca.GetProgramIndex()] = true; } if (programCount == 0) diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index ae063a47d..3516049c9 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -72,9 +72,9 @@ namespace Ryujinx.HLE return Processes.LoadUnpackedNca(exeFsDir, romFsFile); } - public bool LoadXci(string xciFile) + public bool LoadXci(string xciFile, ulong titleId = 0) { - return Processes.LoadXci(xciFile); + return Processes.LoadXci(xciFile, titleId); } public bool LoadNca(string ncaFile) @@ -82,9 +82,9 @@ namespace Ryujinx.HLE return Processes.LoadNca(ncaFile); } - public bool LoadNsp(string nspFile) + public bool LoadNsp(string nspFile, ulong titleId = 0) { - return Processes.LoadNsp(nspFile); + return Processes.LoadNsp(nspFile, titleId); } public bool LoadProgram(string fileName) diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index 65ab01eeb..7495ccb56 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -9,9 +9,11 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.Common.Helper; using System; using System.IO; +using System.Text.Json.Serialization; namespace Ryujinx.Ui.App.Common { @@ -19,10 +21,10 @@ namespace Ryujinx.Ui.App.Common { public bool Favorite { get; set; } public byte[] Icon { get; set; } - public string TitleName { get; set; } - public string TitleId { get; set; } - public string Developer { get; set; } - public string Version { get; set; } + public string Name { get; set; } = "Unknown"; + public ulong Id { get; set; } + public string Developer { get; set; } = "Unknown"; + public string Version { get; set; } = "0"; public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } @@ -36,7 +38,11 @@ namespace Ryujinx.Ui.App.Common public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); - public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath) + [JsonIgnore] public string IdString => Id.ToString("x16"); + + [JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL; + + public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath) { using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); @@ -105,7 +111,7 @@ namespace Ryujinx.Ui.App.Common return string.Empty; } - (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); + (Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _); if (updatePatchNca != null) { diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 46f29851c..976129717 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -14,17 +14,18 @@ using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Loaders.Npdm; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration.System; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; +using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; using TimeSpan = System.TimeSpan; @@ -42,15 +43,16 @@ namespace Ryujinx.Ui.App.Common private readonly byte[] _nsoIcon; private readonly VirtualFileSystem _virtualFileSystem; + private readonly IntegrityCheckLevel _checkLevel; private Language _desiredTitleLanguage; private CancellationTokenSource _cancellationToken; private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public ApplicationLibrary(VirtualFileSystem virtualFileSystem) + public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel) { _virtualFileSystem = virtualFileSystem; + _checkLevel = checkLevel; _nspIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSP.png"); _xciIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_XCI.png"); @@ -69,6 +71,390 @@ namespace Ryujinx.Ui.App.Common return resourceByteArray; } + private ApplicationData GetApplicationFromExeFs(PartitionFileSystem pfs, string filePath) + { + ApplicationData data = new() + { + Icon = _nspIcon, + }; + + using UniqueRef npdmFile = new(); + + try + { + Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); + + if (ResultFs.PathNotFound.Includes(result)) + { + Npdm npdm = new(npdmFile.Get.AsStream()); + + data.Name = npdm.TitleName; + data.Id = npdm.Aci0.TitleId; + } + + return data; + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception.Message}"); + + return null; + } + } + + private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath) + { + bool isExeFs = false; + + // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. + bool hasMainNca = false; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca") + { + using UniqueRef ncaFile = new(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + // Some main NCAs don't have a data partition, so check if the partition exists before opening it + if (nca.Header.ContentType == NcaContentType.Program && + !(nca.SectionExists(NcaSectionType.Data) && + nca.Header.GetFsHeader(dataIndex).IsPatchSection())) + { + hasMainNca = true; + + break; + } + } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } + + if (hasMainNca) + { + List applications = GetApplicationsFromPfs(pfs, filePath); + + switch (applications.Count) + { + case 1: + return applications[0]; + case >= 1: + Logger.Warning?.Print(LogClass.Application, $"File '{filePath}' contains more applications than expected: {applications.Count}"); + return applications[0]; + default: + return null; + } + } + + if (isExeFs) + { + return GetApplicationFromExeFs(pfs, filePath); + } + + return null; + } + + private List GetApplicationsFromPfs(IFileSystem pfs, string filePath) + { + var applications = new List(); + string extension = Path.GetExtension(filePath).ToLower(); + + foreach ((ulong titleId, ContentCollection content) in pfs.GetApplicationData(_virtualFileSystem, _checkLevel)) + { + ApplicationData applicationData = new() + { + Id = titleId, + }; + + try + { + Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program); + Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control); + + BlitStruct controlHolder = new(1); + + IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + + // Check if there is an update available. + if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs)) + { + // Replace the original ControlFs by the updated one. + controlFs = updatedControlFs; + } + + ReadControlData(controlFs, controlHolder.ByteSpan); + + GetApplicationInformation(ref controlHolder.Value, ref applicationData); + + // Read the icon from the ControlFS and store it as a byte array + try + { + using UniqueRef icon = new(); + + controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationData.Icon = stream.ToArray(); + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } + + using var icon = new UniqueRef(); + + controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationData.Icon = stream.ToArray(); + + if (applicationData.Icon != null) + { + break; + } + } + + applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon; + } + + applicationData.ControlHolder = controlHolder; + + applications.Add(applicationData); + } + catch (MissingKeyException exception) + { + applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}"); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}"); + } + } + + return applications; + } + + private bool TryGetApplicationsFromFile(string applicationPath, out List applications) + { + applications = new List(); + + long fileSizeBytes = new FileInfo(applicationPath).Length; + + double fileSize = fileSizeBytes * 0.000000000931; + + BlitStruct controlHolder = new(1); + + try + { + string extension = Path.GetExtension(applicationPath).ToLower(); + + using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); + + switch (extension) + { + case ".xci": + { + Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); + + applications = GetApplicationsFromPfs(xci.OpenPartition(XciPartitionType.Secure), applicationPath); + + if (applications.Count == 0) + { + return false; + } + + break; + } + case ".nsp": + case ".pfs0": + var pfs = new PartitionFileSystem(); + pfs.Initialize(file.AsStorage()).ThrowIfFailure(); + + ApplicationData result = GetApplicationFromNsp(pfs, applicationPath); + + if (result == null) + { + return false; + } + + applications.Add(result); + + break; + case ".nro": + { + BinaryReader reader = new(file); + ApplicationData application = new(); + + byte[] Read(long position, int size) + { + file.Seek(position, SeekOrigin.Begin); + + return reader.ReadBytes(size); + } + + try + { + file.Seek(24, SeekOrigin.Begin); + + int assetOffset = reader.ReadInt32(); + + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + { + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + + ulong nacpOffset = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); + + // Reads and stores game icon as byte array + if (iconSize > 0) + { + application.Icon = Read(assetOffset + iconOffset, (int)iconSize); + } + else + { + application.Icon = _nroIcon; + } + + // Read the NACP data + Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); + + GetApplicationInformation(ref controlHolder.Value, ref application); + } + else + { + application.Icon = _nroIcon; + application.Name = Path.GetFileNameWithoutExtension(applicationPath); + } + + application.ControlHolder = controlHolder; + applications.Add(application); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + return false; + } + + break; + } + case ".nca": + { + try + { + ApplicationData application = new(); + + Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); + + if (!nca.IsProgram() || nca.IsPatch()) + { + return false; + } + + application.Icon = _ncaIcon; + application.Name = Path.GetFileNameWithoutExtension(applicationPath); + application.ControlHolder = controlHolder; + + applications.Add(application); + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + return false; + } + + break; + } + // If its an NSO we just set defaults + case ".nso": + { + ApplicationData application = new() + { + Icon = _nsoIcon, + Name = Path.GetFileNameWithoutExtension(applicationPath), + }; + + applications.Add(application); + break; + } + } + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + + return false; + } + + foreach (var data in applications) + { + ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata => + { + appMetadata.Title = data.Name; + + // Only do the migration if time_played has a value and timespan_played hasn't been updated yet. + if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero) + { + appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); + appMetadata.TimePlayedOld = default; + } + + // Only do the migration if last_played has a value and last_played_utc doesn't exist yet. + if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue) + { + // Migrate from string-based last_played to DateTime-based last_played_utc. + if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) + { + appMetadata.LastPlayed = lastPlayedOldParsed; + + // Migration successful: deleting last_played from the metadata file. + appMetadata.LastPlayedOld = default; + } + + } + }); + + data.Favorite = appMetadata.Favorite; + data.TimePlayed = appMetadata.TimePlayed; + data.LastPlayed = appMetadata.LastPlayed; + data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(); + data.FileSize = new FileInfo(applicationPath).Length; + data.Path = applicationPath; + } + + return true; + } + public void CancelLoading() { _cancellationToken?.Cancel(); @@ -92,7 +478,7 @@ namespace Ryujinx.Ui.App.Common _cancellationToken = new CancellationTokenSource(); // Builds the applications list with paths to found applications - List applications = new(); + List applicationPaths = new(); try { @@ -136,7 +522,7 @@ namespace Ryujinx.Ui.App.Common if (!fileInfo.Attributes.HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso") { var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; - applications.Add(fullPath); + applicationPaths.Add(fullPath); numApplicationsFound++; } } @@ -148,328 +534,35 @@ namespace Ryujinx.Ui.App.Common } // Loops through applications list, creating a struct and then firing an event containing the struct for each application - foreach (string applicationPath in applications) + foreach (string applicationPath in applicationPaths) { if (_cancellationToken.Token.IsCancellationRequested) { return; } - long fileSize = new FileInfo(applicationPath).Length; - string titleName = "Unknown"; - string titleId = "0000000000000000"; - string developer = "Unknown"; - string version = "0"; - byte[] applicationIcon = null; - - BlitStruct controlHolder = new(1); - - try + if (TryGetApplicationsFromFile(applicationPath, out List applications)) { - string extension = Path.GetExtension(applicationPath).ToLower(); - - using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); - - if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") + foreach (var application in applications) { - try + OnApplicationAdded(new ApplicationAddedEventArgs { - IFileSystem pfs; - - bool isExeFs = false; - - if (extension == ".xci") - { - Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); - - pfs = xci.OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); - pfs = pfsTemp; - - // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. - bool hasMainNca = false; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) - { - if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") - { - using UniqueRef ncaFile = new(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - // Some main NCAs don't have a data partition, so check if the partition exists before opening it - if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) - { - hasMainNca = true; - - break; - } - } - else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") - { - isExeFs = true; - } - } - - if (!hasMainNca && !isExeFs) - { - numApplicationsFound--; - - continue; - } - } - - if (isExeFs) - { - applicationIcon = _nspIcon; - - using UniqueRef npdmFile = new(); - - Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); - - if (ResultFs.PathNotFound.Includes(result)) - { - Npdm npdm = new(npdmFile.Get.AsStream()); - - titleName = npdm.TitleName; - titleId = npdm.Aci0.TitleId.ToString("x16"); - } - } - else - { - GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); - - // Check if there is an update available. - if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs)) - { - // Replace the original ControlFs by the updated one. - controlFs = updatedControlFs; - } - - ReadControlData(controlFs, controlHolder.ByteSpan); - - GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version); - - // Read the icon from the ControlFS and store it as a byte array - try - { - using UniqueRef icon = new(); - - controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = new(); - - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - } - catch (HorizonResultException) - { - foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) - { - if (entry.Name == "control.nacp") - { - continue; - } - - using var icon = new UniqueRef(); - - controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = new(); - - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - - if (applicationIcon != null) - { - break; - } - } - - applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; - } - } - } - catch (MissingKeyException exception) - { - applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; - - Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); - } - catch (InvalidDataException) - { - applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; - - Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); - } - catch (Exception exception) - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); - - numApplicationsFound--; - - continue; - } + AppData = application, + }); } - else if (extension == ".nro") + + if (applications.Count > 1) { - BinaryReader reader = new(file); - - byte[] Read(long position, int size) - { - file.Seek(position, SeekOrigin.Begin); - - return reader.ReadBytes(size); - } - - try - { - file.Seek(24, SeekOrigin.Begin); - - int assetOffset = reader.ReadInt32(); - - if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") - { - byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); - - long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); - long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); - - ulong nacpOffset = reader.ReadUInt64(); - ulong nacpSize = reader.ReadUInt64(); - - // Reads and stores game icon as byte array - if (iconSize > 0) - { - applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); - } - else - { - applicationIcon = _nroIcon; - } - - // Read the NACP data - Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); - - GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version); - } - else - { - applicationIcon = _nroIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); - } - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - numApplicationsFound--; - - continue; - } + numApplicationsFound += applications.Count - 1; } - else if (extension == ".nca") - { - try - { - Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) - { - numApplicationsFound--; - - continue; - } - } - catch (InvalidDataException) - { - Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - numApplicationsFound--; - - continue; - } - - applicationIcon = _ncaIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); - } - // If its an NSO we just set defaults - else if (extension == ".nso") - { - applicationIcon = _nsoIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); - } + numApplicationsLoaded += applications.Count; } - catch (IOException exception) + else { - Logger.Warning?.Print(LogClass.Application, exception.Message); - numApplicationsFound--; - - continue; } - ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata => - { - appMetadata.Title = titleName; - - // Only do the migration if time_played has a value and timespan_played hasn't been updated yet. - if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero) - { - appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); - appMetadata.TimePlayedOld = default; - } - - // Only do the migration if last_played has a value and last_played_utc doesn't exist yet. - if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue) - { - // Migrate from string-based last_played to DateTime-based last_played_utc. - if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) - { - appMetadata.LastPlayed = lastPlayedOldParsed; - - // Migration successful: deleting last_played from the metadata file. - appMetadata.LastPlayedOld = default; - } - - } - }); - - ApplicationData data = new() - { - Favorite = appMetadata.Favorite, - Icon = applicationIcon, - TitleName = titleName, - TitleId = titleId, - Developer = developer, - Version = version, - TimePlayed = appMetadata.TimePlayed, - LastPlayed = appMetadata.LastPlayed, - FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(), - FileSize = fileSize, - Path = applicationPath, - ControlHolder = controlHolder, - }; - - numApplicationsLoaded++; - - OnApplicationAdded(new ApplicationAddedEventArgs - { - AppData = data, - }); - OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs { NumAppsFound = numApplicationsFound, @@ -500,15 +593,6 @@ namespace Ryujinx.Ui.App.Common ApplicationCountUpdated?.Invoke(null, e); } - private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId) - { - (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0); - - // Return the ControlFS - controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); - titleId = controlNca?.Header.TitleId.ToString("x16"); - } - public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); @@ -546,10 +630,29 @@ namespace Ryujinx.Ui.App.Common return appMetadata; } - public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage) + public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong titleId) { byte[] applicationIcon = null; + if (titleId == 0) + { + if (Directory.Exists(applicationPath)) + { + return _ncaIcon; + } + + return Path.GetExtension(applicationPath).ToLower() switch + { + ".nsp" => _nspIcon, + ".pfs0" => _nspIcon, + ".xci" => _xciIcon, + ".nso" => _nsoIcon, + ".nro" => _nroIcon, + ".nca" => _ncaIcon, + _ => _ncaIcon, + }; + } + try { // Look for icon only if applicationPath is not a directory @@ -595,7 +698,16 @@ namespace Ryujinx.Ui.App.Common else { // Store the ControlFS in variable called controlFs - GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _); + Dictionary programs = pfs.GetApplicationData(_virtualFileSystem, _checkLevel); + IFileSystem controlFs = null; + + if (programs.ContainsKey(titleId)) + { + if (programs[titleId].GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control) is { } controlNca) + { + controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + } + } // Read the icon from the ControlFS and store it as a byte array try @@ -622,16 +734,11 @@ namespace Ryujinx.Ui.App.Common controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - using (MemoryStream stream = new()) - { - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - } + using MemoryStream stream = new(); + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); - if (applicationIcon != null) - { - break; - } + break; } applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; @@ -714,41 +821,41 @@ namespace Ryujinx.Ui.App.Common return applicationIcon ?? _ncaIcon; } - private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version) + private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data) { _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage) { - titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); - publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); + data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); + data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); } else { - titleName = null; - publisher = null; + data.Name = null; + data.Developer = null; } - if (string.IsNullOrWhiteSpace(titleName)) + if (string.IsNullOrWhiteSpace(data.Name)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.NameString.IsEmpty()) { - titleName = controlTitle.NameString.ToString(); + data.Name = controlTitle.NameString.ToString(); break; } } } - if (string.IsNullOrWhiteSpace(publisher)) + if (string.IsNullOrWhiteSpace(data.Developer)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.PublisherString.IsEmpty()) { - publisher = controlTitle.PublisherString.ToString(); + data.Developer = controlTitle.PublisherString.ToString(); break; } @@ -757,25 +864,21 @@ namespace Ryujinx.Ui.App.Common if (controlData.PresenceGroupId != 0) { - titleId = controlData.PresenceGroupId.ToString("x16"); + data.Id = controlData.PresenceGroupId; } else if (controlData.SaveDataOwnerId != 0) { - titleId = controlData.SaveDataOwnerId.ToString(); + data.Id = controlData.SaveDataOwnerId; } else if (controlData.AddOnContentBaseId != 0) { - titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); - } - else - { - titleId = "0000000000000000"; + data.Id = (controlData.AddOnContentBaseId - 0x1000); } - version = controlData.DisplayVersionString.ToString(); + data.Version = controlData.DisplayVersionString.ToString(); } - private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs) + private bool IsUpdateApplied(Nca mainNca, out IFileSystem updatedControlFs) { updatedControlFs = null; @@ -783,11 +886,11 @@ namespace Ryujinx.Ui.App.Common try { - (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); + (Nca patchNca, Nca controlNca) = mainNca.GetUpdateData(_virtualFileSystem, _checkLevel, 0, out updatePath); if (patchNca != null && controlNca != null) { - updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); return true; } @@ -803,120 +906,5 @@ namespace Ryujinx.Ui.App.Common return false; } - - public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex) - { - Nca mainNca = null; - Nca patchNca = null; - Nca controlNca = null; - - fileSystem.ImportTickets(pfs); - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); - - int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); - - if (ncaProgramIndex != programIndex) - { - continue; - } - - if (nca.Header.ContentType == NcaContentType.Program) - { - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()) - { - patchNca = nca; - } - else - { - mainNca = nca; - } - } - else if (nca.Header.ContentType == NcaContentType.Control) - { - controlNca = nca; - } - } - - return (mainNca, patchNca, controlNca); - } - - public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex) - { - Nca patchNca = null; - Nca controlNca = null; - - fileSystem.ImportTickets(pfs); - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); - - int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); - - if (ncaProgramIndex != programIndex) - { - continue; - } - - if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) - { - break; - } - - if (nca.Header.ContentType == NcaContentType.Program) - { - patchNca = nca; - } - else if (nca.Header.ContentType == NcaContentType.Control) - { - controlNca = nca; - } - } - - return (patchNca, controlNca); - } - - public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath) - { - updatePath = null; - - if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) - { - // Clear the program index part. - titleIdBase &= ~0xFUL; - - // Load update information if exists. - string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); - - if (File.Exists(titleUpdateMetadataPath)) - { - updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; - - if (File.Exists(updatePath)) - { - FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new(); - nsp.Initialize(file.AsStorage()).ThrowIfFailure(); - - return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); - } - } - } - - return (null, null); - } } } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index afb6a9925..14062481a 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -7,6 +7,7 @@ using Ryujinx.Common.SystemInterop; using Ryujinx.Modules; using Ryujinx.SDL2.Common; using Ryujinx.Ui; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -332,7 +333,12 @@ namespace Ryujinx if (CommandLineState.LaunchPathArg != null) { - mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg); + ApplicationData applicationData = new() + { + Path = CommandLineState.LaunchPathArg, + }; + + mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg); } if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index 8b0b35e6c..884f6687e 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -39,6 +39,7 @@ using Silk.NET.Vulkan; using SPB.Graphics.Vulkan; using System; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Reflection; using System.Threading; @@ -70,7 +71,7 @@ namespace Ryujinx.Ui private bool _gameLoaded; private bool _ending; - private string _currentEmulatedGamePath = null; + private ApplicationData _currentApplicationData = null; private string _lastScannedAmiiboId = ""; private bool _lastScannedAmiiboShowAll = false; @@ -181,8 +182,12 @@ namespace Ryujinx.Ui _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile); _userChannelPersistence = new UserChannelPersistence(); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + // Instantiate GUI objects. - _applicationLibrary = new ApplicationLibrary(_virtualFileSystem); + _applicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel); _uiHandler = new GtkHostUiHandler(this); _deviceExitStatus = new AutoResetEvent(false); @@ -784,7 +789,7 @@ namespace Ryujinx.Ui } } - private bool LoadApplication(string path, bool isFirmwareTitle) + private bool LoadApplication(string path, ulong titleId, bool isFirmwareTitle) { SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion(); @@ -858,7 +863,7 @@ namespace Ryujinx.Ui case ".xci": Logger.Info?.Print(LogClass.Application, "Loading as XCI."); - return _emulationContext.LoadXci(path); + return _emulationContext.LoadXci(path, titleId); case ".nca": Logger.Info?.Print(LogClass.Application, "Loading as NCA."); @@ -867,7 +872,7 @@ namespace Ryujinx.Ui case ".pfs0": Logger.Info?.Print(LogClass.Application, "Loading as NSP."); - return _emulationContext.LoadNsp(path); + return _emulationContext.LoadNsp(path, titleId); default: Logger.Info?.Print(LogClass.Application, "Loading as Homebrew."); try @@ -888,7 +893,7 @@ namespace Ryujinx.Ui return false; } - public void RunApplication(string path, bool startFullscreen = false) + public void RunApplication(ApplicationData application, bool startFullscreen = false) { if (_gameLoaded) { @@ -910,14 +915,14 @@ namespace Ryujinx.Ui bool isFirmwareTitle = false; - if (path.StartsWith("@SystemContent")) + if (application.Path.StartsWith("@SystemContent")) { - path = VirtualFileSystem.SwitchPathToSystemPath(path); + application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path); isFirmwareTitle = true; } - if (!LoadApplication(path, isFirmwareTitle)) + if (!LoadApplication(application.Path, application.Id, isFirmwareTitle)) { _emulationContext.Dispose(); SwitchToGameTable(); @@ -927,7 +932,7 @@ namespace Ryujinx.Ui SetupProgressUiHandlers(); - _currentEmulatedGamePath = path; + _currentApplicationData = application; _deviceExitStatus.Reset(); @@ -1168,7 +1173,7 @@ namespace Ryujinx.Ui _tableStore.AppendValues( args.AppData.Favorite, new Gdk.Pixbuf(args.AppData.Icon, 75, 75), - $"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}", + $"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}", args.AppData.Developer, args.AppData.Version, args.AppData.TimePlayedString, @@ -1256,9 +1261,22 @@ namespace Ryujinx.Ui { _gameTableSelection.GetSelected(out TreeIter treeIter); - string path = (string)_tableStore.GetValue(treeIter, 9); + ApplicationData application = new() + { + Favorite = (bool)_tableStore.GetValue(treeIter, 0), + Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0], + Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber), + Developer = (string)_tableStore.GetValue(treeIter, 3), + Version = (string)_tableStore.GetValue(treeIter, 4), + TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)), + LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)), + FileExtension = (string)_tableStore.GetValue(treeIter, 7), + FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)), + Path = (string)_tableStore.GetValue(treeIter, 9), + ControlHolder = (BlitStruct)_tableStore.GetValue(treeIter, 10), + }; - RunApplication(path); + RunApplication(application); } private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args) @@ -1316,13 +1334,22 @@ namespace Ryujinx.Ui return; } - string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString(); - string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0]; - string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); + ApplicationData application = new() + { + Favorite = (bool)_tableStore.GetValue(treeIter, 0), + Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0], + Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber), + Developer = (string)_tableStore.GetValue(treeIter, 3), + Version = (string)_tableStore.GetValue(treeIter, 4), + TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)), + LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)), + FileExtension = (string)_tableStore.GetValue(treeIter, 7), + FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)), + Path = (string)_tableStore.GetValue(treeIter, 9), + ControlHolder = (BlitStruct)_tableStore.GetValue(treeIter, 10), + }; - BlitStruct controlData = (BlitStruct)_tableStore.GetValue(treeIter, 10); - - _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData); + _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, application); } private void Load_Application_File(object sender, EventArgs args) @@ -1344,7 +1371,12 @@ namespace Ryujinx.Ui if (fileChooser.Run() == (int)ResponseType.Accept) { - RunApplication(fileChooser.Filename); + ApplicationData applicationData = new() + { + Path = fileChooser.Filename, + }; + + RunApplication(applicationData); } } @@ -1354,7 +1386,13 @@ namespace Ryujinx.Ui if (fileChooser.Run() == (int)ResponseType.Accept) { - RunApplication(fileChooser.Filename); + ApplicationData applicationData = new() + { + Name = System.IO.Path.GetFileNameWithoutExtension(fileChooser.Filename), + Path = fileChooser.Filename, + }; + + RunApplication(applicationData); } } @@ -1369,7 +1407,14 @@ namespace Ryujinx.Ui { string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); - RunApplication(contentPath); + ApplicationData applicationData = new() + { + Name = "miiEdit", + Id = 0x0100000000001009ul, + Path = contentPath, + }; + + RunApplication(applicationData); } private void Open_Ryu_Folder(object sender, EventArgs args) @@ -1645,13 +1690,13 @@ namespace Ryujinx.Ui { _userChannelPersistence.ShouldRestart = false; - RunApplication(_currentEmulatedGamePath); + RunApplication(_currentApplicationData); } else { // otherwise, clear state. _userChannelPersistence = new UserChannelPersistence(); - _currentEmulatedGamePath = null; + _currentApplicationData = null; _actionMenu.Sensitive = false; _firmwareInstallFile.Sensitive = true; _firmwareInstallDirectory.Sensitive = true; @@ -1713,7 +1758,7 @@ namespace Ryujinx.Ui _emulationContext.Processes.ActiveApplication.ProgramId, _emulationContext.Processes.ActiveApplication.ApplicationControlProperties .Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(), - _currentEmulatedGamePath); + _currentApplicationData.Path); window.Destroyed += CheatWindow_Destroyed; window.Show(); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index 5af181b08..6903c9419 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -16,6 +16,7 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -23,7 +24,6 @@ using Ryujinx.Ui.Windows; using System; using System.Buffers; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Reflection; using System.Threading; @@ -36,17 +36,13 @@ namespace Ryujinx.Ui.Widgets private readonly VirtualFileSystem _virtualFileSystem; private readonly AccountManager _accountManager; private readonly HorizonClient _horizonClient; - private readonly BlitStruct _controlData; - private readonly string _titleFilePath; - private readonly string _titleName; - private readonly string _titleIdText; - private readonly ulong _titleId; + private readonly ApplicationData _title; private MessageDialog _dialog; private bool _cancel; - public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct controlData) + public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, ApplicationData applicationData) { _parent = parent; @@ -55,23 +51,13 @@ namespace Ryujinx.Ui.Widgets _virtualFileSystem = virtualFileSystem; _accountManager = accountManager; _horizonClient = horizonClient; - _titleFilePath = titleFilePath; - _titleName = titleName; - _titleIdText = titleId; - _controlData = controlData; + _title = applicationData; - if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId)) - { - GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id"); + _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.UserAccountSaveDataSize > 0; + _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.DeviceSaveDataSize > 0; + _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; - return; - } - - _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0; - _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0; - _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0; - - string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower(); + string fileExt = System.IO.Path.GetExtension(_title.Path).ToLower(); bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci"; _extractRomFsMenuItem.Sensitive = hasNca; @@ -137,7 +123,7 @@ namespace Ryujinx.Ui.Widgets private void OpenSaveDir(in SaveDataFilter saveDataFilter) { - if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId)) + if (!TryFindSaveData(_title.Name, _title.Id, _title.ControlHolder, in saveDataFilter, out ulong saveDataId)) { return; } @@ -190,7 +176,7 @@ namespace Ryujinx.Ui.Widgets { Title = "Ryujinx - NCA Section Extractor", Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"), - SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...", + SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_title.Path)}...", WindowPosition = WindowPosition.Center, }; @@ -202,18 +188,18 @@ namespace Ryujinx.Ui.Widgets } }); - using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read); + using FileStream file = new(_title.Path, FileMode.Open, FileAccess.Read); Nca mainNca = null; Nca patchNca = null; - if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") || - (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || - (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) + if ((System.IO.Path.GetExtension(_title.Path).ToLower() == ".nsp") || + (System.IO.Path.GetExtension(_title.Path).ToLower() == ".pfs0") || + (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci")) { IFileSystem pfs; - if (System.IO.Path.GetExtension(_titleFilePath) == ".xci") + if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci") { Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); @@ -249,7 +235,7 @@ namespace Ryujinx.Ui.Widgets } } } - else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca") + else if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".nca") { mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); } @@ -266,7 +252,11 @@ namespace Ryujinx.Ui.Widgets return; } - (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + + (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _); if (updatePatchNca != null) { @@ -460,44 +450,44 @@ namespace Ryujinx.Ui.Widgets private void OpenSaveUserDir_Clicked(object sender, EventArgs args) { var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low); - var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_title.Id, saveType: default, userId, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args) { - var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Device, userId: default, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void OpenSaveBcatDir_Clicked(object sender, EventArgs args) { - var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void ManageTitleUpdates_Clicked(object sender, EventArgs args) { - new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show(); + new TitleUpdateWindow(_parent, _virtualFileSystem, _title).Show(); } private void ManageDlc_Clicked(object sender, EventArgs args) { - new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show(); + new DlcWindow(_virtualFileSystem, _title.IdString, _title).Show(); } private void ManageCheats_Clicked(object sender, EventArgs args) { - new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show(); + new CheatWindow(_virtualFileSystem, _title.Id, _title.Name, _title.Path).Show(); } private void OpenTitleModDir_Clicked(object sender, EventArgs args) { string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _title.IdString); OpenHelper.OpenFolder(titleModsPath); } @@ -505,7 +495,7 @@ namespace Ryujinx.Ui.Widgets private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) { string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); + string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _title.IdString); OpenHelper.OpenFolder(titleModsPath); } @@ -527,7 +517,7 @@ namespace Ryujinx.Ui.Widgets private void OpenPtcDir_Clicked(object sender, EventArgs args) { - string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu"); + string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu"); string mainPath = System.IO.Path.Combine(ptcDir, "0"); string backupPath = System.IO.Path.Combine(ptcDir, "1"); @@ -544,7 +534,7 @@ namespace Ryujinx.Ui.Widgets private void OpenShaderCacheDir_Clicked(object sender, EventArgs args) { - string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"); + string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader"); if (!Directory.Exists(shaderCacheDir)) { @@ -556,10 +546,10 @@ namespace Ryujinx.Ui.Widgets private void PurgePtcCache_Clicked(object sender, EventArgs args) { - DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1")); + DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "1")); - MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_titleName}\n\nAre you sure you want to proceed?"); + MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_title.Name}\n\nAre you sure you want to proceed?"); List cacheFiles = new(); @@ -593,9 +583,9 @@ namespace Ryujinx.Ui.Widgets private void PurgeShaderCache_Clicked(object sender, EventArgs args) { - DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader")); + DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader")); - using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_titleName}\n\nAre you sure you want to proceed?"); + using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_title.Name}\n\nAre you sure you want to proceed?"); List oldCacheDirectories = new(); List newCacheFiles = new(); @@ -637,8 +627,11 @@ namespace Ryujinx.Ui.Widgets private void CreateShortcut_Clicked(object sender, EventArgs args) { - byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language); - ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon); + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + byte[] appIcon = new ApplicationLibrary(_virtualFileSystem, checkLevel).GetApplicationIcon(_title.Path, ConfigurationState.Instance.System.Language, _title.Id); + ShortcutHelper.CreateAppShortcut(_title.Path, _title.Name, _title.IdString, appIcon); } } } diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.cs b/src/Ryujinx/Ui/Windows/CheatWindow.cs index 1eca732b2..9bbae1c6c 100644 --- a/src/Ryujinx/Ui/Windows/CheatWindow.cs +++ b/src/Ryujinx/Ui/Windows/CheatWindow.cs @@ -1,7 +1,9 @@ using Gtk; +using LibHac.Tools.FsSystem; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.Ui.App.Common; +using Ryujinx.Ui.Common.Configuration; using System; using System.Collections.Generic; using System.IO; @@ -27,8 +29,13 @@ namespace Ryujinx.Ui.Windows private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow")) { builder.Autoconnect(this); + + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; - _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}"; + _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath)}"; string modsBasePath = ModLoader.GetModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16")); diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.cs b/src/Ryujinx/Ui/Windows/DlcWindow.cs index 9f7179467..dbffc4209 100644 --- a/src/Ryujinx/Ui/Windows/DlcWindow.cs +++ b/src/Ryujinx/Ui/Windows/DlcWindow.cs @@ -9,9 +9,12 @@ using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using GUI = Gtk.Builder.ObjectAttribute; @@ -20,7 +23,7 @@ namespace Ryujinx.Ui.Windows public class DlcWindow : Window { private readonly VirtualFileSystem _virtualFileSystem; - private readonly string _titleId; + private readonly string _applicationId; private readonly string _dlcJsonPath; private readonly List _dlcContainerList; @@ -32,16 +35,16 @@ namespace Ryujinx.Ui.Windows [GUI] TreeSelection _dlcTreeSelection; #pragma warning restore CS0649, IDE0044 - public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { } + public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, ApplicationData title) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, title) { } - private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow")) + private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string applicationId, ApplicationData title) : base(builder.GetRawOwnedObject("_dlcWindow")) { builder.Autoconnect(this); - _titleId = titleId; + _applicationId = applicationId; _virtualFileSystem = virtualFileSystem; - _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json"); - _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]"; + _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationId, "dlc.json"); + _baseTitleInfoLabel.Text = $"DLC Available for {title.Name} [{applicationId.ToUpper()}]"; try { @@ -72,9 +75,12 @@ namespace Ryujinx.Ui.Windows }; _dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0); - _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1); + _dlcTreeView.AppendColumn("ApplicationId", new CellRendererText(), "text", 1); _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); + // NOTE: Try to load downloadable contents from PFS first. + AddDlc(title.Path, true); + foreach (DownloadableContentContainer dlcContainer in _dlcContainerList) { if (File.Exists(dlcContainer.ContainerPath)) @@ -89,7 +95,10 @@ namespace Ryujinx.Ui.Windows using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); PartitionFileSystem pfs = new(); - pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + if (pfs.Initialize(containerFile.AsStorage()).IsFailure()) + { + continue; + } _virtualFileSystem.ImportTickets(pfs); @@ -128,6 +137,57 @@ namespace Ryujinx.Ui.Windows return null; } + private void AddDlc(string path, bool ignoreNotFound = false) + { + if (!File.Exists(path)) + { + return; + } + + using FileStream containerFile = File.OpenRead(path); + + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + + bool containsDlc = false; + + _virtualFileSystem.ImportTickets(pfs); + + TreeIter? parentIter = null; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path); + + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + if (nca.GetProgramIdBase() != (ulong.Parse(_applicationId, NumberStyles.HexNumber) & ~0x1FFFUL)) + { + break; + } + + parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", path); + + ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); + containsDlc = true; + } + } + + if (!containsDlc && !ignoreNotFound) + { + GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); + } + } + private void AddButton_Clicked(object sender, EventArgs args) { FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel") @@ -147,52 +207,7 @@ namespace Ryujinx.Ui.Windows { foreach (string containerPath in fileChooser.Filenames) { - if (!File.Exists(containerPath)) - { - return; - } - - using FileStream containerFile = File.OpenRead(containerPath); - - PartitionFileSystem pfs = new(); - pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - bool containsDlc = false; - - _virtualFileSystem.ImportTickets(pfs); - - TreeIter? parentIter = null; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath); - - if (nca == null) - { - continue; - } - - if (nca.Header.ContentType == NcaContentType.PublicData) - { - if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId) - { - break; - } - - parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath); - - ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); - containsDlc = true; - } - } - - if (!containsDlc) - { - GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); - } + AddDlc(containerPath); } } diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs index 51918eeab..2f7f14f1f 100644 --- a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs +++ b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs @@ -4,12 +4,15 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Ns; +using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; +using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; @@ -24,7 +27,7 @@ namespace Ryujinx.Ui.Windows { private readonly MainWindow _parent; private readonly VirtualFileSystem _virtualFileSystem; - private readonly string _titleId; + private readonly ApplicationData _title; private readonly string _updateJsonPath; private TitleUpdateMetadata _titleUpdateWindowData; @@ -38,17 +41,17 @@ namespace Ryujinx.Ui.Windows [GUI] RadioButton _noUpdateRadioButton; #pragma warning restore CS0649, IDE0044 - public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } + public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, applicationData) { } - private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) + private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) { _parent = parent; builder.Autoconnect(this); - _titleId = titleId; + _title = applicationData; _virtualFileSystem = virtualFileSystem; - _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json"); + _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "updates.json"); _radioButtonToPathDictionary = new Dictionary(); try @@ -64,7 +67,10 @@ namespace Ryujinx.Ui.Windows }; } - _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]"; + _baseTitleInfoLabel.Text = $"Updates Available for {applicationData.Name} [{applicationData.IdString}]"; + + // Try to get updates from PFS first + AddUpdate(_title.Path, true); foreach (string path in _titleUpdateWindowData.Paths) { @@ -84,18 +90,41 @@ namespace Ryujinx.Ui.Windows } } - private void AddUpdate(string path) + private void AddUpdate(string path, bool ignoreNotFound = false) { if (File.Exists(path)) { + IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks + ? IntegrityCheckLevel.ErrorOnInvalid + : IntegrityCheckLevel.None; + using FileStream file = new(path, FileMode.Open, FileAccess.Read); - PartitionFileSystem nsp = new(); - nsp.Initialize(file.AsStorage()).ThrowIfFailure(); + IFileSystem pfs; try { - (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); + if (System.IO.Path.GetExtension(path).ToLower() == ".xci") + { + pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; + } + + Dictionary updates = pfs.GetUpdateData(_virtualFileSystem, checkLevel); + + Nca patchNca = null; + Nca controlNca = null; + + if (updates.TryGetValue(_title.Id, out ContentCollection update)) + { + patchNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Program); + controlNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Control); + } if (controlNca != null && patchNca != null) { @@ -106,7 +135,14 @@ namespace Ryujinx.Ui.Windows controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}"); + string radioLabel = $"Version {controlData.DisplayVersionString.ToString()} - {path}"; + + if (System.IO.Path.GetExtension(path).ToLower() == ".xci") + { + radioLabel = "Bundled: " + radioLabel; + } + + RadioButton radioButton = new(radioLabel); radioButton.JoinGroup(_noUpdateRadioButton); _availableUpdatesBox.Add(radioButton); @@ -117,7 +153,10 @@ namespace Ryujinx.Ui.Windows } else { - GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); + if (!ignoreNotFound) + { + GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); + } } } catch (Exception exception) From 98e7c33630af5a47246e3aa53c528528dbcc79bc Mon Sep 17 00:00:00 2001 From: Mary Guillemard Date: Sat, 11 Nov 2023 22:35:58 +0100 Subject: [PATCH 25/40] infra: Update to LLVM 15 for macOS release --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 988264a31..6089c2fd6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -156,11 +156,11 @@ jobs: with: global-json-file: global.json - - name: Setup LLVM 14 + - name: Setup LLVM 15 run: | wget https://apt.llvm.org/llvm.sh chmod +x llvm.sh - sudo ./llvm.sh 14 + sudo ./llvm.sh 15 - name: Install rcodesign run: | @@ -215,4 +215,4 @@ jobs: needs: release with: ryujinx_version: "1.1.${{ github.run_number }}" - secrets: inherit \ No newline at end of file + secrets: inherit From 6228331fd1fb63a32d929bf1cae7f709bc9fd271 Mon Sep 17 00:00:00 2001 From: Mary Guillemard Date: Sat, 11 Nov 2023 22:38:54 +0100 Subject: [PATCH 26/40] infra: switch back to ubuntu 20.04 LTS for macOS release --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6089c2fd6..008561f9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ env: jobs: tag: name: Create tag - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Get version info id: version_info From 51065d91290e41a9d2518f44c9bdf83a9b0017ab Mon Sep 17 00:00:00 2001 From: gdkchan Date: Sat, 11 Nov 2023 23:35:30 -0300 Subject: [PATCH 27/40] Revert "Add support for multi game XCIs (#5638)" (#5914) This reverts commit 5c3cfb84c09b0566da677425915afa0b2d76da55. --- src/Ryujinx.Ava/AppHost.cs | 9 +- src/Ryujinx.Ava/Assets/Locales/en_US.json | 2 - src/Ryujinx.Ava/Common/ApplicationHelper.cs | 9 +- .../Controls/ApplicationContextMenu.axaml.cs | 54 +- .../UI/Controls/ApplicationGridView.axaml | 2 +- .../UI/Controls/ApplicationListView.axaml | 4 +- .../UI/Models/DownloadableContentModel.cs | 6 +- src/Ryujinx.Ava/UI/Models/SaveModel.cs | 4 +- src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs | 5 +- .../DownloadableContentManagerViewModel.cs | 60 +- .../UI/ViewModels/MainWindowViewModel.cs | 47 +- .../UI/ViewModels/TitleUpdateViewModel.cs | 56 +- .../UI/Views/Main/MainMenuBarView.axaml.cs | 10 +- .../UI/Views/Main/MainViewControls.axaml | 2 +- .../UI/Windows/CheatWindow.axaml.cs | 7 +- .../DownloadableContentManagerWindow.axaml | 2 +- .../DownloadableContentManagerWindow.axaml.cs | 12 +- .../UI/Windows/MainWindow.axaml.cs | 19 +- .../UI/Windows/TitleUpdateWindow.axaml.cs | 14 +- .../FileSystem/ContentCollection.cs | 61 -- .../Processes/Extensions/NcaExtensions.cs | 100 +- .../PartitionFileSystemExtensions.cs | 153 ++- .../Loaders/Processes/ProcessLoader.cs | 8 +- .../Loaders/Processes/ProcessLoaderHelper.cs | 9 +- src/Ryujinx.HLE/Switch.cs | 8 +- src/Ryujinx.Ui.Common/App/ApplicationData.cs | 18 +- .../App/ApplicationLibrary.cs | 922 +++++++++--------- src/Ryujinx/Program.cs | 8 +- src/Ryujinx/Ui/MainWindow.cs | 95 +- .../Ui/Widgets/GameTableContextMenu.cs | 89 +- src/Ryujinx/Ui/Windows/CheatWindow.cs | 9 +- src/Ryujinx/Ui/Windows/DlcWindow.cs | 123 +-- src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs | 63 +- 33 files changed, 819 insertions(+), 1171 deletions(-) delete mode 100644 src/Ryujinx.HLE/FileSystem/ContentCollection.cs diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index 053d5b521..4d751e2a9 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -54,6 +54,8 @@ using System.Threading.Tasks; using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing; using Image = SixLabors.ImageSharp.Image; +using InputManager = Ryujinx.Input.HLE.InputManager; +using IRenderer = Ryujinx.Graphics.GAL.IRenderer; using Key = Ryujinx.Input.Key; using MouseButton = Ryujinx.Input.MouseButton; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; @@ -121,14 +123,12 @@ namespace Ryujinx.Ava public int Width { get; private set; } public int Height { get; private set; } public string ApplicationPath { get; private set; } - public ulong ApplicationId { get; private set; } public bool ScreenshotRequested { get; set; } public AppHost( RendererHost renderer, InputManager inputManager, string applicationPath, - ulong applicationId, VirtualFileSystem virtualFileSystem, ContentManager contentManager, AccountManager accountManager, @@ -152,7 +152,6 @@ namespace Ryujinx.Ava NpadManager = _inputManager.CreateNpadManager(); TouchScreenManager = _inputManager.CreateTouchScreenManager(); ApplicationPath = applicationPath; - ApplicationId = applicationId; VirtualFileSystem = virtualFileSystem; ContentManager = contentManager; @@ -642,7 +641,7 @@ namespace Ryujinx.Ava { Logger.Info?.Print(LogClass.Application, "Loading as XCI."); - if (!Device.LoadXci(ApplicationPath, ApplicationId)) + if (!Device.LoadXci(ApplicationPath)) { Device.Dispose(); @@ -669,7 +668,7 @@ namespace Ryujinx.Ava { Logger.Info?.Print(LogClass.Application, "Loading as NSP."); - if (!Device.LoadNsp(ApplicationPath, ApplicationId)) + if (!Device.LoadNsp(ApplicationPath)) { Device.Dispose(); diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json index be3e35a9c..bc2bbfe82 100644 --- a/src/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json @@ -539,8 +539,6 @@ "OpenSetupGuideMessage": "Open the Setup Guide", "NoUpdate": "No Update", "TitleUpdateVersionLabel": "Version {0}", - "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", - "TitleBundledDlcLabel": "Bundled:", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Confirmation", "FileDialogAllTypes": "All types", diff --git a/src/Ryujinx.Ava/Common/ApplicationHelper.cs b/src/Ryujinx.Ava/Common/ApplicationHelper.cs index dd4643297..91ca8f4d5 100644 --- a/src/Ryujinx.Ava/Common/ApplicationHelper.cs +++ b/src/Ryujinx.Ava/Common/ApplicationHelper.cs @@ -18,8 +18,7 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.HLE.Loaders.Processes.Extensions; -using Ryujinx.Ui.Common.Configuration; +using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System; using System.Buffers; @@ -227,11 +226,7 @@ namespace Ryujinx.Ava.Common return; } - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - - (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _); + (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); if (updatePatchNca != null) { patchNca = updatePatchNca; diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs index 69465c7c7..0f0071065 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs @@ -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; @@ -14,6 +15,7 @@ using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using Path = System.IO.Path; @@ -39,7 +41,7 @@ namespace Ryujinx.Ava.UI.Controls { viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite; - ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata => + ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata => { appMetadata.Favorite = viewModel.SelectedApplication.Favorite; }); @@ -74,9 +76,19 @@ namespace Ryujinx.Ava.UI.Controls { if (viewModel?.SelectedApplication != null) { - var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default); + if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) + { + Dispatcher.UIThread.InvokeAsync(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]); + }); - ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name); + return; + } + + var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default); + + ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName); } } @@ -86,7 +98,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); + await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); } } @@ -96,7 +108,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication); + await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName); } } @@ -108,8 +120,8 @@ namespace Ryujinx.Ava.UI.Controls { await new CheatWindow( viewModel.VirtualFileSystem, - viewModel.SelectedApplication.IdString, - viewModel.SelectedApplication.Name, + viewModel.SelectedApplication.TitleId, + viewModel.SelectedApplication.TitleName, viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window); } } @@ -121,7 +133,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.IdString); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId); OpenHelper.OpenFolder(titleModsPath); } @@ -134,7 +146,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.IdString); + string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId); OpenHelper.OpenFolder(titleModsPath); } @@ -148,15 +160,15 @@ namespace Ryujinx.Ava.UI.Controls { UserResult result = await ContentDialogHelper.CreateConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName), LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); if (result == UserResult.Yes) { - DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1")); + DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1")); List cacheFiles = new(); @@ -196,14 +208,14 @@ namespace Ryujinx.Ava.UI.Controls { UserResult result = await ContentDialogHelper.CreateConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogWarning], - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName), LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); if (result == UserResult.Yes) { - DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader")); + DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader")); List oldCacheDirectories = new(); List newCacheFiles = new(); @@ -251,7 +263,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu"); + string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu"); string mainDir = Path.Combine(ptcDir, "0"); string backupDir = Path.Combine(ptcDir, "1"); @@ -272,7 +284,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"); + string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"); if (!Directory.Exists(shaderCacheDir)) { @@ -293,7 +305,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Code, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.Name); + viewModel.SelectedApplication.TitleName); } } @@ -307,7 +319,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Data, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.Name); + viewModel.SelectedApplication.TitleName); } } @@ -321,7 +333,7 @@ namespace Ryujinx.Ava.UI.Controls viewModel.StorageProvider, NcaSectionType.Logo, viewModel.SelectedApplication.Path, - viewModel.SelectedApplication.Name); + viewModel.SelectedApplication.TitleName); } } @@ -332,7 +344,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { ApplicationData selectedApplication = viewModel.SelectedApplication; - ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.Name, selectedApplication.IdString, selectedApplication.Icon); + ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon); } } @@ -342,7 +354,7 @@ namespace Ryujinx.Ava.UI.Controls if (viewModel?.SelectedApplication != null) { - await viewModel.LoadApplication(viewModel.SelectedApplication); + await viewModel.LoadApplication(viewModel.SelectedApplication.Path); } } } diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml index 5919652e2..bbdb4c4a7 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationGridView.axaml @@ -82,7 +82,7 @@ diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml index 24ec2b357..9004f7518 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml @@ -85,7 +85,7 @@ Path.GetFileName(ContainerPath); - public string Label => - Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName; - public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled) { TitleId = titleId; diff --git a/src/Ryujinx.Ava/UI/Models/SaveModel.cs b/src/Ryujinx.Ava/UI/Models/SaveModel.cs index 2e3ed3bae..7b476932b 100644 --- a/src/Ryujinx.Ava/UI/Models/SaveModel.cs +++ b/src/Ryujinx.Ava/UI/Models/SaveModel.cs @@ -46,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models TitleId = info.ProgramId; UserId = info.UserId; - var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString); + var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString); InGameList = appData != null; if (InGameList) { Icon = appData.Icon; - Title = appData.Name; + Title = appData.TitleName; } else { diff --git a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs index fae2a08d0..3b44e8ee6 100644 --- a/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs +++ b/src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs @@ -8,10 +8,7 @@ namespace Ryujinx.Ava.UI.Models public ApplicationControlProperty Control { get; } public string Path { get; } - public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue( - System.IO.Path.GetExtension(Path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel, - Control.DisplayVersionString.ToString() - ); + public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString()); public TitleUpdateModel(ApplicationControlProperty control, string path) { diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index 9f3a0045d..cdecae77d 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -17,12 +17,11 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; -using Ryujinx.Ui.App.Common; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Application = Avalonia.Application; using Path = System.IO.Path; @@ -39,7 +38,7 @@ namespace Ryujinx.Ava.UI.ViewModels private AvaloniaList _selectedDownloadableContents = new(); private string _search; - private readonly ApplicationData _applicationData; + private readonly ulong _titleId; private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -93,25 +92,18 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) { _virtualFileSystem = virtualFileSystem; - _applicationData = applicationData; + _titleId = titleId; if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { StorageProvider = desktop.MainWindow.StorageProvider; } - _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json"); - - if (!File.Exists(_downloadableContentJsonPath)) - { - _downloadableContentContainerList = new List(); - - Save(); - } + _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json"); try { @@ -128,9 +120,6 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadDownloadableContents() { - // NOTE: Try to load downloadable contents from PFS first. - AddDownloadableContent(_applicationData.Path); - foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList) { if (File.Exists(downloadableContentContainer.ContainerPath)) @@ -138,11 +127,7 @@ namespace Ryujinx.Ava.UI.ViewModels using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); PartitionFileSystem partitionFileSystem = new(); - - if (partitionFileSystem.Initialize(containerFile.AsStorage()).IsFailure()) - { - continue; - } + partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -235,34 +220,22 @@ namespace Ryujinx.Ava.UI.ViewModels foreach (var file in result) { - if (!AddDownloadableContent(file.Path.LocalPath)) - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); - } + await AddDownloadableContent(file.Path.LocalPath); } } - private bool AddDownloadableContent(string path) + private async Task AddDownloadableContent(string path) { if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null) { - return true; + return; } using FileStream containerFile = File.OpenRead(path); - IFileSystem partitionFileSystem; - - if (Path.GetExtension(path).ToLower() == ".xci") - { - partitionFileSystem = new Xci(_virtualFileSystem.KeySet, containerFile.AsStorage()).OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - partitionFileSystem = pfsTemp; - } + PartitionFileSystem partitionFileSystem = new(); + partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + bool containsDownloadableContent = false; _virtualFileSystem.ImportTickets(partitionFileSystem); @@ -280,7 +253,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (nca.Header.ContentType == NcaContentType.PublicData) { - if (nca.GetProgramIdBase() != _applicationData.IdBase) + if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId) { break; } @@ -292,11 +265,14 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(nameof(UpdateCount)); Sort(); - return true; + containsDownloadableContent = true; } } - return false; + if (!containsDownloadableContent) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); + } } public void Remove(DownloadableContentModel model) diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 692df483d..80df5d398 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -95,7 +95,7 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _canUpdate = true; private Cursor _cursor; private string _title; - private ApplicationData _currentApplicationData; + private string _currentEmulatedGamePath; private readonly AutoResetEvent _rendererWaitEvent; private WindowState _windowState; private double _windowWidth; @@ -106,6 +106,7 @@ namespace Ryujinx.Ava.UI.ViewModels public ApplicationData ListSelectedApplication; public ApplicationData GridSelectedApplication; + private string TitleName { get; set; } internal AppHost AppHost { get; set; } public MainWindowViewModel() @@ -929,8 +930,8 @@ namespace Ryujinx.Ava.UI.ViewModels return SortMode switch { #pragma warning disable IDE0055 // Disable formatting - ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.Name) - : SortExpressionComparer.Descending(app => app.Name), + ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName) + : SortExpressionComparer.Descending(app => app.TitleName), ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer) : SortExpressionComparer.Descending(app => app.Developer), ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending), @@ -967,7 +968,7 @@ namespace Ryujinx.Ava.UI.ViewModels { if (arg is ApplicationData app) { - return string.IsNullOrWhiteSpace(_searchText) || app.Name.ToLower().Contains(_searchText.ToLower()); + return string.IsNullOrWhiteSpace(_searchText) || app.TitleName.ToLower().Contains(_searchText.ToLower()); } return false; @@ -1096,7 +1097,7 @@ namespace Ryujinx.Ava.UI.ViewModels IsLoadingIndeterminate = false; break; case LoadState.Loaded: - LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); IsLoadingIndeterminate = true; CacheLoadStatus = ""; break; @@ -1116,7 +1117,7 @@ namespace Ryujinx.Ava.UI.ViewModels IsLoadingIndeterminate = false; break; case ShaderCacheLoadingState.Loaded: - LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name); + LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName); IsLoadingIndeterminate = true; CacheLoadStatus = ""; break; @@ -1167,13 +1168,13 @@ namespace Ryujinx.Ava.UI.ViewModels { UserChannelPersistence.ShouldRestart = false; - await LoadApplication(_currentApplicationData); + await LoadApplication(_currentEmulatedGamePath); } else { // Otherwise, clear state. UserChannelPersistence = new UserChannelPersistence(); - _currentApplicationData = null; + _currentEmulatedGamePath = null; } } @@ -1450,12 +1451,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (result.Count > 0) { - ApplicationData applicationData = new() - { - Path = result[0].Path.LocalPath, - }; - - await LoadApplication(applicationData); + await LoadApplication(result[0].Path.LocalPath); } } @@ -1469,17 +1465,11 @@ namespace Ryujinx.Ava.UI.ViewModels if (result.Count > 0) { - ApplicationData applicationData = new() - { - Name = Path.GetFileNameWithoutExtension(result[0].Path.LocalPath), - Path = result[0].Path.LocalPath, - }; - - await LoadApplication(applicationData); + await LoadApplication(result[0].Path.LocalPath); } } - public async Task LoadApplication(ApplicationData application, bool startFullscreen = false) + public async Task LoadApplication(string path, bool startFullscreen = false, string titleName = "") { if (AppHost != null) { @@ -1499,7 +1489,7 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id); + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language); PrepareLoadScreen(); @@ -1508,8 +1498,7 @@ namespace Ryujinx.Ava.UI.ViewModels AppHost = new AppHost( RendererHostControl, InputManager, - application.Path, - application.Id, + path, VirtualFileSystem, ContentManager, AccountManager, @@ -1527,17 +1516,17 @@ namespace Ryujinx.Ava.UI.ViewModels CanUpdate = false; - LoadHeading = application.Name; + LoadHeading = TitleName = titleName; - if (string.IsNullOrWhiteSpace(application.Name)) + if (string.IsNullOrWhiteSpace(titleName)) { LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name); - application.Name = AppHost.Device.Processes.ActiveApplication.Name; + TitleName = AppHost.Device.Processes.ActiveApplication.Name; } SwitchToRenderer(startFullscreen); - _currentApplicationData = application; + _currentEmulatedGamePath = path; Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" }; gameThread.Start(); diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs index 7bb96131d..5090a8c70 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs @@ -1,3 +1,4 @@ +using Avalonia; using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; @@ -7,7 +8,6 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Ns; -using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common.Locale; @@ -17,16 +17,12 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; -using Ryujinx.Ui.Common.Configuration; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using Application = Avalonia.Application; -using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; using SpanHelpers = LibHac.Common.SpanHelpers; @@ -37,7 +33,7 @@ namespace Ryujinx.Ava.UI.ViewModels public TitleUpdateMetadata TitleUpdateWindowData; public readonly string TitleUpdateJsonPath; private VirtualFileSystem VirtualFileSystem { get; } - private ApplicationData ApplicationData { get; } + private ulong TitleId { get; } private AvaloniaList _titleUpdates = new(); private AvaloniaList _views = new(); @@ -77,18 +73,18 @@ namespace Ryujinx.Ava.UI.ViewModels public IStorageProvider StorageProvider; - public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId) { VirtualFileSystem = virtualFileSystem; - ApplicationData = applicationData; + TitleId = titleId; if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { StorageProvider = desktop.MainWindow.StorageProvider; } - TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json"); + TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); try { @@ -96,7 +92,7 @@ namespace Ryujinx.Ava.UI.ViewModels } catch { - Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdString} at {TitleUpdateJsonPath}"); + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {TitleId} at {TitleUpdateJsonPath}"); TitleUpdateWindowData = new TitleUpdateMetadata { @@ -112,9 +108,6 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadUpdates() { - // Try to load updates from PFS first - AddUpdate(ApplicationData.Path, true); - foreach (string path in TitleUpdateWindowData.Paths) { AddUpdate(path); @@ -169,41 +162,17 @@ namespace Ryujinx.Ava.UI.ViewModels } } - private void AddUpdate(string path, bool ignoreNotFound = false) + private void AddUpdate(string path) { if (File.Exists(path) && TitleUpdates.All(x => x.Path != path)) { - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - using FileStream file = new(path, FileMode.Open, FileAccess.Read); - IFileSystem pfs; - try { - if (Path.GetExtension(path).ToLower() == ".xci") - { - pfs = new Xci(VirtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); - pfs = pfsTemp; - } - - Dictionary updates = pfs.GetUpdateData(VirtualFileSystem, checkLevel); - - Nca patchNca = null; - Nca controlNca = null; - - if (updates.TryGetValue(ApplicationData.Id, out ContentCollection content)) - { - patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program); - controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control); - } + var pfs = new PartitionFileSystem(); + pfs.Initialize(file.AsStorage()).ThrowIfFailure(); + (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0); if (controlNca != null && patchNca != null) { @@ -218,10 +187,7 @@ namespace Ryujinx.Ava.UI.ViewModels } else { - if (!ignoreNotFound) - { - Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); - } + Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage])); } } catch (Exception ex) diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs index af3c5deb6..4f2d262da 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs @@ -10,7 +10,6 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Utilities; using Ryujinx.Modules; -using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -132,14 +131,7 @@ namespace Ryujinx.Ava.UI.Views.Main if (!string.IsNullOrEmpty(contentPath)) { - ApplicationData applicationData = new() - { - Name = "miiEdit", - Id = 0x0100000000001009, - Path = contentPath, - }; - - await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen); + await ViewModel.LoadApplication(contentPath, false, "Mii Applet"); } } diff --git a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml index 7a716fb2a..cc21b5c60 100644 --- a/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml @@ -104,7 +104,7 @@ Content="{locale:Locale GameListHeaderApplication}" GroupName="Sort" IsChecked="{Binding IsSortedByTitle, Mode=OneTime}" - Tag="Application" /> + Tag="Title" /> (); - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper()); - BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath); + BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath); InitializeComponent(); diff --git a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml index 98aac09ce..99cf28e77 100644 --- a/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml @@ -97,7 +97,7 @@ MaxLines="2" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" - Text="{Binding Label}" /> + Text="{Binding FileName}" /> x.OfType().Name("DialogSpace").Child().OfType()); diff --git a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs index 352ac4e54..c78f4160d 100644 --- a/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs @@ -4,7 +4,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Threading; using FluentAvalonia.UI.Controls; -using LibHac.Tools.FsSystem; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; @@ -24,6 +23,7 @@ using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; using System; +using System.IO; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -139,7 +139,9 @@ namespace Ryujinx.Ava.UI.Windows { ViewModel.SelectedIcon = args.Application.Icon; - ViewModel.LoadApplication(args.Application).Wait(); + string path = new FileInfo(args.Application.Path).FullName; + + ViewModel.LoadApplication(path).Wait(); } args.Handled = true; @@ -188,11 +190,7 @@ namespace Ryujinx.Ava.UI.Windows LibHacHorizonManager.InitializeBcatServer(); LibHacHorizonManager.InitializeSystemClients(); - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - - ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel); + ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem); // Save data created before we supported extra data in directory save data will not work properly if // given empty extra data. Luckily some of that extra data can be created using the data from the @@ -299,12 +297,7 @@ namespace Ryujinx.Ava.UI.Windows { _deferLoad = false; - ApplicationData applicationData = new() - { - Path = _launchPath, - }; - - ViewModel.LoadApplication(applicationData, _startFullscreen).Wait(); + ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait(); } } else diff --git a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs index 8ecf165ce..7ece63355 100644 --- a/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs +++ b/src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs @@ -7,15 +7,15 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.HLE.FileSystem; -using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Helper; using System.Threading.Tasks; +using Button = Avalonia.Controls.Button; namespace Ryujinx.Ava.UI.Windows { public partial class TitleUpdateWindow : UserControl { - public readonly TitleUpdateViewModel ViewModel; + public TitleUpdateViewModel ViewModel; public TitleUpdateWindow() { @@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); } - public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId) { - DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData); + DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId); InitializeComponent(); } - public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData) + public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) { ContentDialog contentDialog = new() { PrimaryButtonText = "", SecondaryButtonText = "", CloseButtonText = "", - Content = new TitleUpdateWindow(virtualFileSystem, applicationData), - Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString), + Content = new TitleUpdateWindow(virtualFileSystem, titleId), + Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")), }; Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); diff --git a/src/Ryujinx.HLE/FileSystem/ContentCollection.cs b/src/Ryujinx.HLE/FileSystem/ContentCollection.cs deleted file mode 100644 index 1c19887be..000000000 --- a/src/Ryujinx.HLE/FileSystem/ContentCollection.cs +++ /dev/null @@ -1,61 +0,0 @@ -using LibHac.Common.Keys; -using LibHac.Fs.Fsa; -using LibHac.Ncm; -using LibHac.Tools.FsSystem.NcaUtils; -using LibHac.Tools.Ncm; -using Ryujinx.HLE.Loaders.Processes.Extensions; -using System; - -namespace Ryujinx.HLE.FileSystem -{ - /// - /// Thin wrapper around - /// - public class ContentCollection - { - private readonly IFileSystem _pfs; - private readonly Cnmt _cnmt; - - public ulong Id => _cnmt.TitleId; - public TitleVersion Version => _cnmt.TitleVersion; - public ContentMetaType Type => _cnmt.Type; - public ulong ApplicationId => _cnmt.ApplicationTitleId; - public ulong PatchId => _cnmt.PatchTitleId; - public TitleVersion RequiredSystemVersion => _cnmt.MinimumSystemVersion; - public TitleVersion RequiredApplicationVersion => _cnmt.MinimumApplicationVersion; - public byte[] Digest => _cnmt.Hash; - - public ulong ProgramBaseId => Id & ~0x1FFFUL; - public bool IsSystemTitle => _cnmt.Type < ContentMetaType.Application; - - public ContentCollection(IFileSystem pfs, Cnmt cnmt) - { - _pfs = pfs; - _cnmt = cnmt; - } - - public Nca GetNcaByType(KeySet keySet, ContentType type, int programIndex = 0) - { - // TODO: Replace this with a check for IdOffset as soon as LibHac supports it: - // && entry.IdOffset == programIndex - - foreach (var entry in _cnmt.ContentEntries) - { - if (entry.Type != type) - { - continue; - } - - string ncaId = BitConverter.ToString(entry.NcaId).Replace("-", null).ToLower(); - Nca nca = _pfs.GetNca(keySet, $"/{ncaId}.nca"); - - if (nca.GetProgramIndex() == programIndex) - { - return nca; - } - } - - return null; - } - } -} diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index 6863d1a7c..4568b44da 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -2,31 +2,21 @@ using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; -using LibHac.FsSystem; using LibHac.Loader; using LibHac.Ncm; using LibHac.Ns; -using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; -using LibHac.Tools.Ncm; -using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; -using Ryujinx.Common.Utilities; -using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using System.IO; using System.Linq; using ApplicationId = LibHac.Ncm.ApplicationId; -using ContentType = LibHac.Ncm.ContentType; -using Path = System.IO.Path; namespace Ryujinx.HLE.Loaders.Processes.Extensions { - public static class NcaExtensions + static class NcaExtensions { - private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca) { // Extract RomFs and ExeFs from NCA. @@ -57,7 +47,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions nacpData = controlNca.GetNacp(device); } - /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update. + /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update. // Load program 0 control NCA as we are going to need it for display version. (_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); @@ -96,11 +86,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return processResult; } - public static ulong GetProgramIdBase(this Nca nca) - { - return nca.Header.TitleId & ~0x1FFFUL; - } - public static int GetProgramIndex(this Nca nca) { return (int)(nca.Header.TitleId & 0xF); @@ -111,11 +96,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Program; } - public static bool IsMain(this Nca nca) - { - return nca.IsProgram() && !nca.IsPatch(); - } - public static bool IsPatch(this Nca nca) { int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); @@ -128,56 +108,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nca.Header.ContentType == NcaContentType.Control; } - public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath) - { - updatePath = "(unknown)"; - - // Load Update NCAs. - Nca updatePatchNca = null; - Nca updateControlNca = null; - - // Clear the program index part. - ulong titleIdBase = mainNca.GetProgramIdBase(); - - // Load update information if exists. - string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json"); - if (File.Exists(titleUpdateMetadataPath)) - { - updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; - if (File.Exists(updatePath)) - { - var updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read); - - IFileSystem updatePartitionFileSystem; - - if (Path.GetExtension(updatePath).ToLower() == ".xci") - { - updatePartitionFileSystem = new Xci(fileSystem.KeySet, updateFile.AsStorage()).OpenPartition(XciPartitionType.Secure); - } - else - { - PartitionFileSystem pfsTemp = new(); - pfsTemp.Initialize(updateFile.AsStorage()).ThrowIfFailure(); - updatePartitionFileSystem = pfsTemp; - } - - foreach ((ulong updateTitleId, ContentCollection content) in updatePartitionFileSystem.GetUpdateData(fileSystem, checkLevel)) - { - if ((updateTitleId & ~0x1FFFUL) != titleIdBase) - { - continue; - } - - updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex); - updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex); - break; - } - } - } - - return (updatePatchNca, updateControlNca); - } - public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null) { IFileSystem exeFs = null; @@ -242,31 +172,5 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return nacpData; } - - public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType) - { - string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt"; - using var cnmtFile = new UniqueRef(); - - try - { - Result result = cnmtNca.OpenFileSystem(0, checkLevel) - .OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read); - - if (result.IsSuccess()) - { - return new Cnmt(cnmtFile.Release().AsStream()); - } - } - catch (HorizonResultException ex) - { - if (!ResultFs.PathNotFound.Includes(ex.ResultValue)) - { - Logger.Warning?.Print(LogClass.Application, $"Failed get cnmt for '{cnmtNca.Header.TitleId:x16}' from nca: {ex.Message}"); - } - } - - return null; - } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs index 5f45cd459..50f7d5853 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs @@ -1,87 +1,26 @@ using LibHac.Common; -using LibHac.Common.Keys; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; -using LibHac.Ncm; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; -using LibHac.Tools.Ncm; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; -using Ryujinx.HLE.FileSystem; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; -using ContentType = LibHac.Ncm.ContentType; namespace Ryujinx.HLE.Loaders.Processes.Extensions { public static class PartitionFileSystemExtensions { private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public static Dictionary GetApplicationData(this IFileSystem partitionFileSystem, - VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel) - { - fileSystem.ImportTickets(partitionFileSystem); - - var programs = new Dictionary(); - - foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca")) - { - Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Application); - - if (cnmt == null) - { - continue; - } - - ContentCollection content = new(partitionFileSystem, cnmt); - - if (content.Type != ContentMetaType.Application) - { - continue; - } - - programs.TryAdd(content.ApplicationId, content); - } - - return programs; - } - - public static Dictionary GetUpdateData(this IFileSystem partitionFileSystem, - VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel) - { - fileSystem.ImportTickets(partitionFileSystem); - - var programs = new Dictionary(); - - foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca")) - { - Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Patch); - - if (cnmt == null) - { - continue; - } - - ContentCollection content = new(partitionFileSystem, cnmt); - - if (content.Type != ContentMetaType.Patch) - { - continue; - } - - programs.TryAdd(content.ApplicationId, content); - } - - return programs; - } - - internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, ulong titleId, out string errorMessage) + internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, out string errorMessage) where TMetaData : PartitionFileSystemMetaCore, new() where TFormat : IPartitionFileSystemFormat where THeader : unmanaged, IPartitionFileSystemHeader @@ -96,21 +35,30 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions try { - Dictionary applications = partitionFileSystem.GetApplicationData(device.FileSystem, device.System.FsIntegrityCheckLevel); + device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem); - if (titleId == 0) + // TODO: To support multi-games container, this should use CNMT NCA instead. + foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) { - foreach ((ulong _, ContentCollection content) in applications) + Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); + + if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index) { - mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index); - controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index); - break; + continue; + } + + if (nca.IsPatch()) + { + patchNca = nca; + } + else if (nca.IsProgram()) + { + mainNca = nca; + } + else if (nca.IsControl()) + { + controlNca = nca; } - } - else if (applications.TryGetValue(titleId, out ContentCollection content)) - { - mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index); - controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index); } ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure(); @@ -131,7 +79,54 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (false, ProcessResult.Failed); } - (Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _); + // Load Update NCAs. + Nca updatePatchNca = null; + Nca updateControlNca = null; + + if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) + { + // Clear the program index part. + titleIdBase &= ~0xFUL; + + // Load update information if exists. + string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); + if (File.Exists(titleUpdateMetadataPath)) + { + string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; + if (File.Exists(updatePath)) + { + PartitionFileSystem updatePartitionFileSystem = new(); + updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure(); + + device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem); + + // TODO: This should use CNMT NCA instead. + foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca")) + { + Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath); + + if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index) + { + continue; + } + + if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16")) + { + break; + } + + if (nca.IsProgram()) + { + updatePatchNca = nca; + } + else if (nca.IsControl()) + { + updateControlNca = nca; + } + } + } + } + } if (updatePatchNca != null) { @@ -173,18 +168,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions return (true, mainNca.Load(device, patchNca, controlNca)); } - errorMessage = $"Unable to load: Could not find Main NCA for title \"{titleId:X16}\""; + errorMessage = "Unable to load: Could not find Main NCA"; return (false, ProcessResult.Failed); } - public static Nca GetNca(this IFileSystem fileSystem, KeySet keySet, string path) + public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path) { using var ncaFile = new UniqueRef(); fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - return new Nca(keySet, ncaFile.Release().AsStorage()); + return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage()); } } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 6b4a64be8..220b868db 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes _processesByPid = new ConcurrentDictionary(); } - public bool LoadXci(string path, ulong titleId) + public bool LoadXci(string path) { FileStream stream = new(path, FileMode.Open, FileAccess.Read); Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage()); @@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, titleId, out string errorMessage); + (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage); if (!success) { @@ -66,13 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - public bool LoadNsp(string path, ulong titleId) + public bool LoadNsp(string path) { FileStream file = new(path, FileMode.Open, FileAccess.Read); PartitionFileSystem partitionFileSystem = new(); partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure(); - (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, titleId, out string errorMessage); + (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage); if (processResult.ProcessId == 0) { diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs index 110bb0928..c229b1742 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs @@ -42,14 +42,15 @@ namespace Ryujinx.HLE.Loaders.Processes foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca")) { - Nca nca = partitionFileSystem.GetNca(device.FileSystem.KeySet, fileEntry.FullPath); + Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath); - if (!nca.IsProgram()) + if (!nca.IsProgram() && nca.IsPatch()) { continue; } - ulong currentMainProgramId = nca.GetProgramIdBase(); + ulong currentProgramId = nca.Header.TitleId; + ulong currentMainProgramId = currentProgramId & ~0xFFFul; if (applicationId == 0 && currentMainProgramId != 0) { @@ -66,7 +67,7 @@ namespace Ryujinx.HLE.Loaders.Processes break; } - hasIndex[nca.GetProgramIndex()] = true; + hasIndex[(int)(currentProgramId & 0xF)] = true; } if (programCount == 0) diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 3516049c9..ae063a47d 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -72,9 +72,9 @@ namespace Ryujinx.HLE return Processes.LoadUnpackedNca(exeFsDir, romFsFile); } - public bool LoadXci(string xciFile, ulong titleId = 0) + public bool LoadXci(string xciFile) { - return Processes.LoadXci(xciFile, titleId); + return Processes.LoadXci(xciFile); } public bool LoadNca(string ncaFile) @@ -82,9 +82,9 @@ namespace Ryujinx.HLE return Processes.LoadNca(ncaFile); } - public bool LoadNsp(string nspFile, ulong titleId = 0) + public bool LoadNsp(string nspFile) { - return Processes.LoadNsp(nspFile, titleId); + return Processes.LoadNsp(nspFile); } public bool LoadProgram(string fileName) diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index 7495ccb56..65ab01eeb 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -9,11 +9,9 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.Common.Helper; using System; using System.IO; -using System.Text.Json.Serialization; namespace Ryujinx.Ui.App.Common { @@ -21,10 +19,10 @@ namespace Ryujinx.Ui.App.Common { public bool Favorite { get; set; } public byte[] Icon { get; set; } - public string Name { get; set; } = "Unknown"; - public ulong Id { get; set; } - public string Developer { get; set; } = "Unknown"; - public string Version { get; set; } = "0"; + public string TitleName { get; set; } + public string TitleId { get; set; } + public string Developer { get; set; } + public string Version { get; set; } public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } @@ -38,11 +36,7 @@ namespace Ryujinx.Ui.App.Common public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); - [JsonIgnore] public string IdString => Id.ToString("x16"); - - [JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL; - - public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath) + public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath) { using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); @@ -111,7 +105,7 @@ namespace Ryujinx.Ui.App.Common return string.Empty; } - (Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _); + (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _); if (updatePatchNca != null) { diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 976129717..46f29851c 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -14,18 +14,17 @@ using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Loaders.Npdm; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration.System; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; -using ContentType = LibHac.Ncm.ContentType; using Path = System.IO.Path; using TimeSpan = System.TimeSpan; @@ -43,16 +42,15 @@ namespace Ryujinx.Ui.App.Common private readonly byte[] _nsoIcon; private readonly VirtualFileSystem _virtualFileSystem; - private readonly IntegrityCheckLevel _checkLevel; private Language _desiredTitleLanguage; private CancellationTokenSource _cancellationToken; private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel) + public ApplicationLibrary(VirtualFileSystem virtualFileSystem) { _virtualFileSystem = virtualFileSystem; - _checkLevel = checkLevel; _nspIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_NSP.png"); _xciIcon = GetResourceBytes("Ryujinx.Ui.Common.Resources.Icon_XCI.png"); @@ -71,390 +69,6 @@ namespace Ryujinx.Ui.App.Common return resourceByteArray; } - private ApplicationData GetApplicationFromExeFs(PartitionFileSystem pfs, string filePath) - { - ApplicationData data = new() - { - Icon = _nspIcon, - }; - - using UniqueRef npdmFile = new(); - - try - { - Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); - - if (ResultFs.PathNotFound.Includes(result)) - { - Npdm npdm = new(npdmFile.Get.AsStream()); - - data.Name = npdm.TitleName; - data.Id = npdm.Aci0.TitleId; - } - - return data; - } - catch (Exception exception) - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception.Message}"); - - return null; - } - } - - private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath) - { - bool isExeFs = false; - - // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. - bool hasMainNca = false; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) - { - if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca") - { - using UniqueRef ncaFile = new(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - // Some main NCAs don't have a data partition, so check if the partition exists before opening it - if (nca.Header.ContentType == NcaContentType.Program && - !(nca.SectionExists(NcaSectionType.Data) && - nca.Header.GetFsHeader(dataIndex).IsPatchSection())) - { - hasMainNca = true; - - break; - } - } - else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") - { - isExeFs = true; - } - } - - if (hasMainNca) - { - List applications = GetApplicationsFromPfs(pfs, filePath); - - switch (applications.Count) - { - case 1: - return applications[0]; - case >= 1: - Logger.Warning?.Print(LogClass.Application, $"File '{filePath}' contains more applications than expected: {applications.Count}"); - return applications[0]; - default: - return null; - } - } - - if (isExeFs) - { - return GetApplicationFromExeFs(pfs, filePath); - } - - return null; - } - - private List GetApplicationsFromPfs(IFileSystem pfs, string filePath) - { - var applications = new List(); - string extension = Path.GetExtension(filePath).ToLower(); - - foreach ((ulong titleId, ContentCollection content) in pfs.GetApplicationData(_virtualFileSystem, _checkLevel)) - { - ApplicationData applicationData = new() - { - Id = titleId, - }; - - try - { - Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program); - Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control); - - BlitStruct controlHolder = new(1); - - IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); - - // Check if there is an update available. - if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs)) - { - // Replace the original ControlFs by the updated one. - controlFs = updatedControlFs; - } - - ReadControlData(controlFs, controlHolder.ByteSpan); - - GetApplicationInformation(ref controlHolder.Value, ref applicationData); - - // Read the icon from the ControlFS and store it as a byte array - try - { - using UniqueRef icon = new(); - - controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = new(); - - icon.Get.AsStream().CopyTo(stream); - applicationData.Icon = stream.ToArray(); - } - catch (HorizonResultException) - { - foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) - { - if (entry.Name == "control.nacp") - { - continue; - } - - using var icon = new UniqueRef(); - - controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using MemoryStream stream = new(); - - icon.Get.AsStream().CopyTo(stream); - applicationData.Icon = stream.ToArray(); - - if (applicationData.Icon != null) - { - break; - } - } - - applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon; - } - - applicationData.ControlHolder = controlHolder; - - applications.Add(applicationData); - } - catch (MissingKeyException exception) - { - applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon; - - Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); - } - catch (InvalidDataException) - { - applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon; - - Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}"); - } - catch (Exception exception) - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}"); - } - } - - return applications; - } - - private bool TryGetApplicationsFromFile(string applicationPath, out List applications) - { - applications = new List(); - - long fileSizeBytes = new FileInfo(applicationPath).Length; - - double fileSize = fileSizeBytes * 0.000000000931; - - BlitStruct controlHolder = new(1); - - try - { - string extension = Path.GetExtension(applicationPath).ToLower(); - - using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); - - switch (extension) - { - case ".xci": - { - Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); - - applications = GetApplicationsFromPfs(xci.OpenPartition(XciPartitionType.Secure), applicationPath); - - if (applications.Count == 0) - { - return false; - } - - break; - } - case ".nsp": - case ".pfs0": - var pfs = new PartitionFileSystem(); - pfs.Initialize(file.AsStorage()).ThrowIfFailure(); - - ApplicationData result = GetApplicationFromNsp(pfs, applicationPath); - - if (result == null) - { - return false; - } - - applications.Add(result); - - break; - case ".nro": - { - BinaryReader reader = new(file); - ApplicationData application = new(); - - byte[] Read(long position, int size) - { - file.Seek(position, SeekOrigin.Begin); - - return reader.ReadBytes(size); - } - - try - { - file.Seek(24, SeekOrigin.Begin); - - int assetOffset = reader.ReadInt32(); - - if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") - { - byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); - - long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); - long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); - - ulong nacpOffset = reader.ReadUInt64(); - ulong nacpSize = reader.ReadUInt64(); - - // Reads and stores game icon as byte array - if (iconSize > 0) - { - application.Icon = Read(assetOffset + iconOffset, (int)iconSize); - } - else - { - application.Icon = _nroIcon; - } - - // Read the NACP data - Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); - - GetApplicationInformation(ref controlHolder.Value, ref application); - } - else - { - application.Icon = _nroIcon; - application.Name = Path.GetFileNameWithoutExtension(applicationPath); - } - - application.ControlHolder = controlHolder; - applications.Add(application); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - return false; - } - - break; - } - case ".nca": - { - try - { - ApplicationData application = new(); - - Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); - - if (!nca.IsProgram() || nca.IsPatch()) - { - return false; - } - - application.Icon = _ncaIcon; - application.Name = Path.GetFileNameWithoutExtension(applicationPath); - application.ControlHolder = controlHolder; - - applications.Add(application); - } - catch (InvalidDataException) - { - Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - return false; - } - - break; - } - // If its an NSO we just set defaults - case ".nso": - { - ApplicationData application = new() - { - Icon = _nsoIcon, - Name = Path.GetFileNameWithoutExtension(applicationPath), - }; - - applications.Add(application); - break; - } - } - } - catch (IOException exception) - { - Logger.Warning?.Print(LogClass.Application, exception.Message); - - return false; - } - - foreach (var data in applications) - { - ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata => - { - appMetadata.Title = data.Name; - - // Only do the migration if time_played has a value and timespan_played hasn't been updated yet. - if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero) - { - appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); - appMetadata.TimePlayedOld = default; - } - - // Only do the migration if last_played has a value and last_played_utc doesn't exist yet. - if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue) - { - // Migrate from string-based last_played to DateTime-based last_played_utc. - if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) - { - appMetadata.LastPlayed = lastPlayedOldParsed; - - // Migration successful: deleting last_played from the metadata file. - appMetadata.LastPlayedOld = default; - } - - } - }); - - data.Favorite = appMetadata.Favorite; - data.TimePlayed = appMetadata.TimePlayed; - data.LastPlayed = appMetadata.LastPlayed; - data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(); - data.FileSize = new FileInfo(applicationPath).Length; - data.Path = applicationPath; - } - - return true; - } - public void CancelLoading() { _cancellationToken?.Cancel(); @@ -478,7 +92,7 @@ namespace Ryujinx.Ui.App.Common _cancellationToken = new CancellationTokenSource(); // Builds the applications list with paths to found applications - List applicationPaths = new(); + List applications = new(); try { @@ -522,7 +136,7 @@ namespace Ryujinx.Ui.App.Common if (!fileInfo.Attributes.HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso") { var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName; - applicationPaths.Add(fullPath); + applications.Add(fullPath); numApplicationsFound++; } } @@ -534,34 +148,327 @@ namespace Ryujinx.Ui.App.Common } // Loops through applications list, creating a struct and then firing an event containing the struct for each application - foreach (string applicationPath in applicationPaths) + foreach (string applicationPath in applications) { if (_cancellationToken.Token.IsCancellationRequested) { return; } - if (TryGetApplicationsFromFile(applicationPath, out List applications)) + long fileSize = new FileInfo(applicationPath).Length; + string titleName = "Unknown"; + string titleId = "0000000000000000"; + string developer = "Unknown"; + string version = "0"; + byte[] applicationIcon = null; + + BlitStruct controlHolder = new(1); + + try { - foreach (var application in applications) + string extension = Path.GetExtension(applicationPath).ToLower(); + + using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read); + + if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") { - OnApplicationAdded(new ApplicationAddedEventArgs + try { - AppData = application, - }); - } + IFileSystem pfs; - if (applications.Count > 1) + bool isExeFs = false; + + if (extension == ".xci") + { + Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); + + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + var pfsTemp = new PartitionFileSystem(); + pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); + pfs = pfsTemp; + + // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. + bool hasMainNca = false; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") + { + using UniqueRef ncaFile = new(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + // Some main NCAs don't have a data partition, so check if the partition exists before opening it + if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) + { + hasMainNca = true; + + break; + } + } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } + + if (!hasMainNca && !isExeFs) + { + numApplicationsFound--; + + continue; + } + } + + if (isExeFs) + { + applicationIcon = _nspIcon; + + using UniqueRef npdmFile = new(); + + Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); + + if (ResultFs.PathNotFound.Includes(result)) + { + Npdm npdm = new(npdmFile.Get.AsStream()); + + titleName = npdm.TitleName; + titleId = npdm.Aci0.TitleId.ToString("x16"); + } + } + else + { + GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); + + // Check if there is an update available. + if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs)) + { + // Replace the original ControlFs by the updated one. + controlFs = updatedControlFs; + } + + ReadControlData(controlFs, controlHolder.ByteSpan); + + GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version); + + // Read the icon from the ControlFS and store it as a byte array + try + { + using UniqueRef icon = new(); + + controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } + + using var icon = new UniqueRef(); + + controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using MemoryStream stream = new(); + + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + + if (applicationIcon != null) + { + break; + } + } + + applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; + } + } + } + catch (MissingKeyException exception) + { + applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}"); + + numApplicationsFound--; + + continue; + } + } + else if (extension == ".nro") { - numApplicationsFound += applications.Count - 1; + BinaryReader reader = new(file); + + byte[] Read(long position, int size) + { + file.Seek(position, SeekOrigin.Begin); + + return reader.ReadBytes(size); + } + + try + { + file.Seek(24, SeekOrigin.Begin); + + int assetOffset = reader.ReadInt32(); + + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + { + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + + ulong nacpOffset = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); + + // Reads and stores game icon as byte array + if (iconSize > 0) + { + applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); + } + else + { + applicationIcon = _nroIcon; + } + + // Read the NACP data + Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); + + GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version); + } + else + { + applicationIcon = _nroIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + numApplicationsFound--; + + continue; + } + } + else if (extension == ".nca") + { + try + { + Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) + { + numApplicationsFound--; + + continue; + } + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + numApplicationsFound--; + + continue; + } + + applicationIcon = _ncaIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + // If its an NSO we just set defaults + else if (extension == ".nso") + { + applicationIcon = _nsoIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + + numApplicationsFound--; + + continue; + } + + ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata => + { + appMetadata.Title = titleName; + + // Only do the migration if time_played has a value and timespan_played hasn't been updated yet. + if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero) + { + appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld); + appMetadata.TimePlayedOld = default; } - numApplicationsLoaded += applications.Count; - } - else + // Only do the migration if last_played has a value and last_played_utc doesn't exist yet. + if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue) + { + // Migrate from string-based last_played to DateTime-based last_played_utc. + if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) + { + appMetadata.LastPlayed = lastPlayedOldParsed; + + // Migration successful: deleting last_played from the metadata file. + appMetadata.LastPlayedOld = default; + } + + } + }); + + ApplicationData data = new() { - numApplicationsFound--; - } + Favorite = appMetadata.Favorite, + Icon = applicationIcon, + TitleName = titleName, + TitleId = titleId, + Developer = developer, + Version = version, + TimePlayed = appMetadata.TimePlayed, + LastPlayed = appMetadata.LastPlayed, + FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(), + FileSize = fileSize, + Path = applicationPath, + ControlHolder = controlHolder, + }; + + numApplicationsLoaded++; + + OnApplicationAdded(new ApplicationAddedEventArgs + { + AppData = data, + }); OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs { @@ -593,6 +500,15 @@ namespace Ryujinx.Ui.App.Common ApplicationCountUpdated?.Invoke(null, e); } + private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId) + { + (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0); + + // Return the ControlFS + controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + titleId = controlNca?.Header.TitleId.ToString("x16"); + } + public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); @@ -630,29 +546,10 @@ namespace Ryujinx.Ui.App.Common return appMetadata; } - public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong titleId) + public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage) { byte[] applicationIcon = null; - if (titleId == 0) - { - if (Directory.Exists(applicationPath)) - { - return _ncaIcon; - } - - return Path.GetExtension(applicationPath).ToLower() switch - { - ".nsp" => _nspIcon, - ".pfs0" => _nspIcon, - ".xci" => _xciIcon, - ".nso" => _nsoIcon, - ".nro" => _nroIcon, - ".nca" => _ncaIcon, - _ => _ncaIcon, - }; - } - try { // Look for icon only if applicationPath is not a directory @@ -698,16 +595,7 @@ namespace Ryujinx.Ui.App.Common else { // Store the ControlFS in variable called controlFs - Dictionary programs = pfs.GetApplicationData(_virtualFileSystem, _checkLevel); - IFileSystem controlFs = null; - - if (programs.ContainsKey(titleId)) - { - if (programs[titleId].GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control) is { } controlNca) - { - controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); - } - } + GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _); // Read the icon from the ControlFS and store it as a byte array try @@ -734,11 +622,16 @@ namespace Ryujinx.Ui.App.Common controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - using MemoryStream stream = new(); - icon.Get.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); + using (MemoryStream stream = new()) + { + icon.Get.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + } - break; + if (applicationIcon != null) + { + break; + } } applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon; @@ -821,41 +714,41 @@ namespace Ryujinx.Ui.App.Common return applicationIcon ?? _ncaIcon; } - private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data) + private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version) { _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage) { - data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); - data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); + titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); + publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); } else { - data.Name = null; - data.Developer = null; + titleName = null; + publisher = null; } - if (string.IsNullOrWhiteSpace(data.Name)) + if (string.IsNullOrWhiteSpace(titleName)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.NameString.IsEmpty()) { - data.Name = controlTitle.NameString.ToString(); + titleName = controlTitle.NameString.ToString(); break; } } } - if (string.IsNullOrWhiteSpace(data.Developer)) + if (string.IsNullOrWhiteSpace(publisher)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.PublisherString.IsEmpty()) { - data.Developer = controlTitle.PublisherString.ToString(); + publisher = controlTitle.PublisherString.ToString(); break; } @@ -864,21 +757,25 @@ namespace Ryujinx.Ui.App.Common if (controlData.PresenceGroupId != 0) { - data.Id = controlData.PresenceGroupId; + titleId = controlData.PresenceGroupId.ToString("x16"); } else if (controlData.SaveDataOwnerId != 0) { - data.Id = controlData.SaveDataOwnerId; + titleId = controlData.SaveDataOwnerId.ToString(); } else if (controlData.AddOnContentBaseId != 0) { - data.Id = (controlData.AddOnContentBaseId - 0x1000); + titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); + } + else + { + titleId = "0000000000000000"; } - data.Version = controlData.DisplayVersionString.ToString(); + version = controlData.DisplayVersionString.ToString(); } - private bool IsUpdateApplied(Nca mainNca, out IFileSystem updatedControlFs) + private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs) { updatedControlFs = null; @@ -886,11 +783,11 @@ namespace Ryujinx.Ui.App.Common try { - (Nca patchNca, Nca controlNca) = mainNca.GetUpdateData(_virtualFileSystem, _checkLevel, 0, out updatePath); + (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); if (patchNca != null && controlNca != null) { - updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); return true; } @@ -906,5 +803,120 @@ namespace Ryujinx.Ui.App.Common return false; } + + public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex) + { + Nca mainNca = null; + Nca patchNca = null; + Nca controlNca = null; + + fileSystem.ImportTickets(pfs); + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); + + int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); + + if (ncaProgramIndex != programIndex) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.Program) + { + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + patchNca = nca; + } + else + { + mainNca = nca; + } + } + else if (nca.Header.ContentType == NcaContentType.Control) + { + controlNca = nca; + } + } + + return (mainNca, patchNca, controlNca); + } + + public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex) + { + Nca patchNca = null; + Nca controlNca = null; + + fileSystem.ImportTickets(pfs); + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); + + int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); + + if (ncaProgramIndex != programIndex) + { + continue; + } + + if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) + { + break; + } + + if (nca.Header.ContentType == NcaContentType.Program) + { + patchNca = nca; + } + else if (nca.Header.ContentType == NcaContentType.Control) + { + controlNca = nca; + } + } + + return (patchNca, controlNca); + } + + public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath) + { + updatePath = null; + + if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) + { + // Clear the program index part. + titleIdBase &= ~0xFUL; + + // Load update information if exists. + string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); + + if (File.Exists(titleUpdateMetadataPath)) + { + updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; + + if (File.Exists(updatePath)) + { + FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); + + return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); + } + } + } + + return (null, null); + } } } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 14062481a..afb6a9925 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -7,7 +7,6 @@ using Ryujinx.Common.SystemInterop; using Ryujinx.Modules; using Ryujinx.SDL2.Common; using Ryujinx.Ui; -using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -333,12 +332,7 @@ namespace Ryujinx if (CommandLineState.LaunchPathArg != null) { - ApplicationData applicationData = new() - { - Path = CommandLineState.LaunchPathArg, - }; - - mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg); + mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg); } if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index 884f6687e..8b0b35e6c 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -39,7 +39,6 @@ using Silk.NET.Vulkan; using SPB.Graphics.Vulkan; using System; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Reflection; using System.Threading; @@ -71,7 +70,7 @@ namespace Ryujinx.Ui private bool _gameLoaded; private bool _ending; - private ApplicationData _currentApplicationData = null; + private string _currentEmulatedGamePath = null; private string _lastScannedAmiiboId = ""; private bool _lastScannedAmiiboShowAll = false; @@ -182,12 +181,8 @@ namespace Ryujinx.Ui _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile); _userChannelPersistence = new UserChannelPersistence(); - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - // Instantiate GUI objects. - _applicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel); + _applicationLibrary = new ApplicationLibrary(_virtualFileSystem); _uiHandler = new GtkHostUiHandler(this); _deviceExitStatus = new AutoResetEvent(false); @@ -789,7 +784,7 @@ namespace Ryujinx.Ui } } - private bool LoadApplication(string path, ulong titleId, bool isFirmwareTitle) + private bool LoadApplication(string path, bool isFirmwareTitle) { SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion(); @@ -863,7 +858,7 @@ namespace Ryujinx.Ui case ".xci": Logger.Info?.Print(LogClass.Application, "Loading as XCI."); - return _emulationContext.LoadXci(path, titleId); + return _emulationContext.LoadXci(path); case ".nca": Logger.Info?.Print(LogClass.Application, "Loading as NCA."); @@ -872,7 +867,7 @@ namespace Ryujinx.Ui case ".pfs0": Logger.Info?.Print(LogClass.Application, "Loading as NSP."); - return _emulationContext.LoadNsp(path, titleId); + return _emulationContext.LoadNsp(path); default: Logger.Info?.Print(LogClass.Application, "Loading as Homebrew."); try @@ -893,7 +888,7 @@ namespace Ryujinx.Ui return false; } - public void RunApplication(ApplicationData application, bool startFullscreen = false) + public void RunApplication(string path, bool startFullscreen = false) { if (_gameLoaded) { @@ -915,14 +910,14 @@ namespace Ryujinx.Ui bool isFirmwareTitle = false; - if (application.Path.StartsWith("@SystemContent")) + if (path.StartsWith("@SystemContent")) { - application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path); + path = VirtualFileSystem.SwitchPathToSystemPath(path); isFirmwareTitle = true; } - if (!LoadApplication(application.Path, application.Id, isFirmwareTitle)) + if (!LoadApplication(path, isFirmwareTitle)) { _emulationContext.Dispose(); SwitchToGameTable(); @@ -932,7 +927,7 @@ namespace Ryujinx.Ui SetupProgressUiHandlers(); - _currentApplicationData = application; + _currentEmulatedGamePath = path; _deviceExitStatus.Reset(); @@ -1173,7 +1168,7 @@ namespace Ryujinx.Ui _tableStore.AppendValues( args.AppData.Favorite, new Gdk.Pixbuf(args.AppData.Icon, 75, 75), - $"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}", + $"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}", args.AppData.Developer, args.AppData.Version, args.AppData.TimePlayedString, @@ -1261,22 +1256,9 @@ namespace Ryujinx.Ui { _gameTableSelection.GetSelected(out TreeIter treeIter); - ApplicationData application = new() - { - Favorite = (bool)_tableStore.GetValue(treeIter, 0), - Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0], - Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber), - Developer = (string)_tableStore.GetValue(treeIter, 3), - Version = (string)_tableStore.GetValue(treeIter, 4), - TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)), - LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)), - FileExtension = (string)_tableStore.GetValue(treeIter, 7), - FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)), - Path = (string)_tableStore.GetValue(treeIter, 9), - ControlHolder = (BlitStruct)_tableStore.GetValue(treeIter, 10), - }; + string path = (string)_tableStore.GetValue(treeIter, 9); - RunApplication(application); + RunApplication(path); } private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args) @@ -1334,22 +1316,13 @@ namespace Ryujinx.Ui return; } - ApplicationData application = new() - { - Favorite = (bool)_tableStore.GetValue(treeIter, 0), - Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0], - Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber), - Developer = (string)_tableStore.GetValue(treeIter, 3), - Version = (string)_tableStore.GetValue(treeIter, 4), - TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)), - LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)), - FileExtension = (string)_tableStore.GetValue(treeIter, 7), - FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)), - Path = (string)_tableStore.GetValue(treeIter, 9), - ControlHolder = (BlitStruct)_tableStore.GetValue(treeIter, 10), - }; + string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString(); + string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0]; + string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); - _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, application); + BlitStruct controlData = (BlitStruct)_tableStore.GetValue(treeIter, 10); + + _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData); } private void Load_Application_File(object sender, EventArgs args) @@ -1371,12 +1344,7 @@ namespace Ryujinx.Ui if (fileChooser.Run() == (int)ResponseType.Accept) { - ApplicationData applicationData = new() - { - Path = fileChooser.Filename, - }; - - RunApplication(applicationData); + RunApplication(fileChooser.Filename); } } @@ -1386,13 +1354,7 @@ namespace Ryujinx.Ui if (fileChooser.Run() == (int)ResponseType.Accept) { - ApplicationData applicationData = new() - { - Name = System.IO.Path.GetFileNameWithoutExtension(fileChooser.Filename), - Path = fileChooser.Filename, - }; - - RunApplication(applicationData); + RunApplication(fileChooser.Filename); } } @@ -1407,14 +1369,7 @@ namespace Ryujinx.Ui { string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); - ApplicationData applicationData = new() - { - Name = "miiEdit", - Id = 0x0100000000001009ul, - Path = contentPath, - }; - - RunApplication(applicationData); + RunApplication(contentPath); } private void Open_Ryu_Folder(object sender, EventArgs args) @@ -1690,13 +1645,13 @@ namespace Ryujinx.Ui { _userChannelPersistence.ShouldRestart = false; - RunApplication(_currentApplicationData); + RunApplication(_currentEmulatedGamePath); } else { // otherwise, clear state. _userChannelPersistence = new UserChannelPersistence(); - _currentApplicationData = null; + _currentEmulatedGamePath = null; _actionMenu.Sensitive = false; _firmwareInstallFile.Sensitive = true; _firmwareInstallDirectory.Sensitive = true; @@ -1758,7 +1713,7 @@ namespace Ryujinx.Ui _emulationContext.Processes.ActiveApplication.ProgramId, _emulationContext.Processes.ActiveApplication.ApplicationControlProperties .Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(), - _currentApplicationData.Path); + _currentEmulatedGamePath); window.Destroyed += CheatWindow_Destroyed; window.Show(); diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs index 6903c9419..5af181b08 100644 --- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs +++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -16,7 +16,6 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Helper; @@ -24,6 +23,7 @@ using Ryujinx.Ui.Windows; using System; using System.Buffers; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Reflection; using System.Threading; @@ -36,13 +36,17 @@ namespace Ryujinx.Ui.Widgets private readonly VirtualFileSystem _virtualFileSystem; private readonly AccountManager _accountManager; private readonly HorizonClient _horizonClient; + private readonly BlitStruct _controlData; - private readonly ApplicationData _title; + private readonly string _titleFilePath; + private readonly string _titleName; + private readonly string _titleIdText; + private readonly ulong _titleId; private MessageDialog _dialog; private bool _cancel; - public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, ApplicationData applicationData) + public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct controlData) { _parent = parent; @@ -51,13 +55,23 @@ namespace Ryujinx.Ui.Widgets _virtualFileSystem = virtualFileSystem; _accountManager = accountManager; _horizonClient = horizonClient; - _title = applicationData; + _titleFilePath = titleFilePath; + _titleName = titleName; + _titleIdText = titleId; + _controlData = controlData; - _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.UserAccountSaveDataSize > 0; - _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.DeviceSaveDataSize > 0; - _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(_title.ControlHolder.ByteSpan) && _title.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; + if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId)) + { + GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id"); - string fileExt = System.IO.Path.GetExtension(_title.Path).ToLower(); + return; + } + + _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0; + _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0; + _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0; + + string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower(); bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci"; _extractRomFsMenuItem.Sensitive = hasNca; @@ -123,7 +137,7 @@ namespace Ryujinx.Ui.Widgets private void OpenSaveDir(in SaveDataFilter saveDataFilter) { - if (!TryFindSaveData(_title.Name, _title.Id, _title.ControlHolder, in saveDataFilter, out ulong saveDataId)) + if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId)) { return; } @@ -176,7 +190,7 @@ namespace Ryujinx.Ui.Widgets { Title = "Ryujinx - NCA Section Extractor", Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"), - SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_title.Path)}...", + SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...", WindowPosition = WindowPosition.Center, }; @@ -188,18 +202,18 @@ namespace Ryujinx.Ui.Widgets } }); - using FileStream file = new(_title.Path, FileMode.Open, FileAccess.Read); + using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read); Nca mainNca = null; Nca patchNca = null; - if ((System.IO.Path.GetExtension(_title.Path).ToLower() == ".nsp") || - (System.IO.Path.GetExtension(_title.Path).ToLower() == ".pfs0") || - (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci")) + if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") || + (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || + (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) { IFileSystem pfs; - if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".xci") + if (System.IO.Path.GetExtension(_titleFilePath) == ".xci") { Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage()); @@ -235,7 +249,7 @@ namespace Ryujinx.Ui.Widgets } } } - else if (System.IO.Path.GetExtension(_title.Path).ToLower() == ".nca") + else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca") { mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); } @@ -252,11 +266,7 @@ namespace Ryujinx.Ui.Widgets return; } - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - - (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _); + (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); if (updatePatchNca != null) { @@ -450,44 +460,44 @@ namespace Ryujinx.Ui.Widgets private void OpenSaveUserDir_Clicked(object sender, EventArgs args) { var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low); - var saveDataFilter = SaveDataFilter.Make(_title.Id, saveType: default, userId, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args) { - var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Device, userId: default, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void OpenSaveBcatDir_Clicked(object sender, EventArgs args) { - var saveDataFilter = SaveDataFilter.Make(_title.Id, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); + var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default); OpenSaveDir(in saveDataFilter); } private void ManageTitleUpdates_Clicked(object sender, EventArgs args) { - new TitleUpdateWindow(_parent, _virtualFileSystem, _title).Show(); + new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show(); } private void ManageDlc_Clicked(object sender, EventArgs args) { - new DlcWindow(_virtualFileSystem, _title.IdString, _title).Show(); + new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show(); } private void ManageCheats_Clicked(object sender, EventArgs args) { - new CheatWindow(_virtualFileSystem, _title.Id, _title.Name, _title.Path).Show(); + new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show(); } private void OpenTitleModDir_Clicked(object sender, EventArgs args) { string modsBasePath = ModLoader.GetModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _title.IdString); + string titleModsPath = ModLoader.GetTitleDir(modsBasePath, _titleIdText); OpenHelper.OpenFolder(titleModsPath); } @@ -495,7 +505,7 @@ namespace Ryujinx.Ui.Widgets private void OpenTitleSdModDir_Clicked(object sender, EventArgs args) { string sdModsBasePath = ModLoader.GetSdModsBasePath(); - string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _title.IdString); + string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, _titleIdText); OpenHelper.OpenFolder(titleModsPath); } @@ -517,7 +527,7 @@ namespace Ryujinx.Ui.Widgets private void OpenPtcDir_Clicked(object sender, EventArgs args) { - string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu"); + string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu"); string mainPath = System.IO.Path.Combine(ptcDir, "0"); string backupPath = System.IO.Path.Combine(ptcDir, "1"); @@ -534,7 +544,7 @@ namespace Ryujinx.Ui.Widgets private void OpenShaderCacheDir_Clicked(object sender, EventArgs args) { - string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader"); + string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"); if (!Directory.Exists(shaderCacheDir)) { @@ -546,10 +556,10 @@ namespace Ryujinx.Ui.Widgets private void PurgePtcCache_Clicked(object sender, EventArgs args) { - DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "0")); - DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "cpu", "1")); + DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0")); + DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1")); - MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_title.Name}\n\nAre you sure you want to proceed?"); + MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_titleName}\n\nAre you sure you want to proceed?"); List cacheFiles = new(); @@ -583,9 +593,9 @@ namespace Ryujinx.Ui.Widgets private void PurgeShaderCache_Clicked(object sender, EventArgs args) { - DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _title.IdString, "cache", "shader")); + DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader")); - using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_title.Name}\n\nAre you sure you want to proceed?"); + using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_titleName}\n\nAre you sure you want to proceed?"); List oldCacheDirectories = new(); List newCacheFiles = new(); @@ -627,11 +637,8 @@ namespace Ryujinx.Ui.Widgets private void CreateShortcut_Clicked(object sender, EventArgs args) { - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - byte[] appIcon = new ApplicationLibrary(_virtualFileSystem, checkLevel).GetApplicationIcon(_title.Path, ConfigurationState.Instance.System.Language, _title.Id); - ShortcutHelper.CreateAppShortcut(_title.Path, _title.Name, _title.IdString, appIcon); + byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language); + ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon); } } } diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.cs b/src/Ryujinx/Ui/Windows/CheatWindow.cs index 9bbae1c6c..1eca732b2 100644 --- a/src/Ryujinx/Ui/Windows/CheatWindow.cs +++ b/src/Ryujinx/Ui/Windows/CheatWindow.cs @@ -1,9 +1,7 @@ using Gtk; -using LibHac.Tools.FsSystem; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.Ui.App.Common; -using Ryujinx.Ui.Common.Configuration; using System; using System.Collections.Generic; using System.IO; @@ -29,13 +27,8 @@ namespace Ryujinx.Ui.Windows private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow")) { builder.Autoconnect(this); - - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; - _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath)}"; + _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}"; string modsBasePath = ModLoader.GetModsBasePath(); string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16")); diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.cs b/src/Ryujinx/Ui/Windows/DlcWindow.cs index dbffc4209..9f7179467 100644 --- a/src/Ryujinx/Ui/Windows/DlcWindow.cs +++ b/src/Ryujinx/Ui/Windows/DlcWindow.cs @@ -9,12 +9,9 @@ using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; -using Ryujinx.Ui.App.Common; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using GUI = Gtk.Builder.ObjectAttribute; @@ -23,7 +20,7 @@ namespace Ryujinx.Ui.Windows public class DlcWindow : Window { private readonly VirtualFileSystem _virtualFileSystem; - private readonly string _applicationId; + private readonly string _titleId; private readonly string _dlcJsonPath; private readonly List _dlcContainerList; @@ -35,16 +32,16 @@ namespace Ryujinx.Ui.Windows [GUI] TreeSelection _dlcTreeSelection; #pragma warning restore CS0649, IDE0044 - public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, ApplicationData title) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, title) { } + public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { } - private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string applicationId, ApplicationData title) : base(builder.GetRawOwnedObject("_dlcWindow")) + private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow")) { builder.Autoconnect(this); - _applicationId = applicationId; + _titleId = titleId; _virtualFileSystem = virtualFileSystem; - _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationId, "dlc.json"); - _baseTitleInfoLabel.Text = $"DLC Available for {title.Name} [{applicationId.ToUpper()}]"; + _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json"); + _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]"; try { @@ -75,12 +72,9 @@ namespace Ryujinx.Ui.Windows }; _dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0); - _dlcTreeView.AppendColumn("ApplicationId", new CellRendererText(), "text", 1); + _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1); _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); - // NOTE: Try to load downloadable contents from PFS first. - AddDlc(title.Path, true); - foreach (DownloadableContentContainer dlcContainer in _dlcContainerList) { if (File.Exists(dlcContainer.ContainerPath)) @@ -95,10 +89,7 @@ namespace Ryujinx.Ui.Windows using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); PartitionFileSystem pfs = new(); - if (pfs.Initialize(containerFile.AsStorage()).IsFailure()) - { - continue; - } + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); _virtualFileSystem.ImportTickets(pfs); @@ -137,57 +128,6 @@ namespace Ryujinx.Ui.Windows return null; } - private void AddDlc(string path, bool ignoreNotFound = false) - { - if (!File.Exists(path)) - { - return; - } - - using FileStream containerFile = File.OpenRead(path); - - PartitionFileSystem pfs = new(); - pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); - - bool containsDlc = false; - - _virtualFileSystem.ImportTickets(pfs); - - TreeIter? parentIter = null; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - using var ncaFile = new UniqueRef(); - - pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path); - - if (nca == null) - { - continue; - } - - if (nca.Header.ContentType == NcaContentType.PublicData) - { - if (nca.GetProgramIdBase() != (ulong.Parse(_applicationId, NumberStyles.HexNumber) & ~0x1FFFUL)) - { - break; - } - - parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", path); - - ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); - containsDlc = true; - } - } - - if (!containsDlc && !ignoreNotFound) - { - GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); - } - } - private void AddButton_Clicked(object sender, EventArgs args) { FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel") @@ -207,7 +147,52 @@ namespace Ryujinx.Ui.Windows { foreach (string containerPath in fileChooser.Filenames) { - AddDlc(containerPath); + if (!File.Exists(containerPath)) + { + return; + } + + using FileStream containerFile = File.OpenRead(containerPath); + + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + bool containsDlc = false; + + _virtualFileSystem.ImportTickets(pfs); + + TreeIter? parentIter = null; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath); + + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId) + { + break; + } + + parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath); + + ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); + containsDlc = true; + } + } + + if (!containsDlc) + { + GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); + } } } diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs index 2f7f14f1f..51918eeab 100644 --- a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs +++ b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs @@ -4,15 +4,12 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Ns; -using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Ui.App.Common; -using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; @@ -27,7 +24,7 @@ namespace Ryujinx.Ui.Windows { private readonly MainWindow _parent; private readonly VirtualFileSystem _virtualFileSystem; - private readonly ApplicationData _title; + private readonly string _titleId; private readonly string _updateJsonPath; private TitleUpdateMetadata _titleUpdateWindowData; @@ -41,17 +38,17 @@ namespace Ryujinx.Ui.Windows [GUI] RadioButton _noUpdateRadioButton; #pragma warning restore CS0649, IDE0044 - public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, applicationData) { } + public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } - private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) + private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) { _parent = parent; builder.Autoconnect(this); - _title = applicationData; + _titleId = titleId; _virtualFileSystem = virtualFileSystem; - _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "updates.json"); + _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json"); _radioButtonToPathDictionary = new Dictionary(); try @@ -67,10 +64,7 @@ namespace Ryujinx.Ui.Windows }; } - _baseTitleInfoLabel.Text = $"Updates Available for {applicationData.Name} [{applicationData.IdString}]"; - - // Try to get updates from PFS first - AddUpdate(_title.Path, true); + _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]"; foreach (string path in _titleUpdateWindowData.Paths) { @@ -90,41 +84,18 @@ namespace Ryujinx.Ui.Windows } } - private void AddUpdate(string path, bool ignoreNotFound = false) + private void AddUpdate(string path) { if (File.Exists(path)) { - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - using FileStream file = new(path, FileMode.Open, FileAccess.Read); - IFileSystem pfs; + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); try { - if (System.IO.Path.GetExtension(path).ToLower() == ".xci") - { - pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); - } - else - { - var pfsTemp = new PartitionFileSystem(); - pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure(); - pfs = pfsTemp; - } - - Dictionary updates = pfs.GetUpdateData(_virtualFileSystem, checkLevel); - - Nca patchNca = null; - Nca controlNca = null; - - if (updates.TryGetValue(_title.Id, out ContentCollection update)) - { - patchNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Program); - controlNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Control); - } + (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); if (controlNca != null && patchNca != null) { @@ -135,14 +106,7 @@ namespace Ryujinx.Ui.Windows controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - string radioLabel = $"Version {controlData.DisplayVersionString.ToString()} - {path}"; - - if (System.IO.Path.GetExtension(path).ToLower() == ".xci") - { - radioLabel = "Bundled: " + radioLabel; - } - - RadioButton radioButton = new(radioLabel); + RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}"); radioButton.JoinGroup(_noUpdateRadioButton); _availableUpdatesBox.Add(radioButton); @@ -153,10 +117,7 @@ namespace Ryujinx.Ui.Windows } else { - if (!ignoreNotFound) - { - GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); - } + GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); } } catch (Exception exception) From e6e58389164fe7cb6894dfd6e8ac1cc7d9ec7d11 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Mon, 13 Nov 2023 18:07:05 -0300 Subject: [PATCH 28/40] Do not set modified flag again if texture was not modified (#5909) * Do not set modified flag again if texture was not modified * Formatting * Fix copy dep regression --- src/Ryujinx.Graphics.Gpu/Image/Texture.cs | 18 ++++++-------- .../Image/TextureGroup.cs | 6 ++--- .../Image/TextureManager.cs | 24 +++++++++++++++---- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index dca6263aa..326272e7c 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -102,9 +102,9 @@ namespace Ryujinx.Graphics.Gpu.Image public bool AlwaysFlushOnOverlap { get; private set; } /// - /// Indicates that the texture was fully unmapped since the modified flag was set, and flushes should be ignored until it is modified again. + /// Indicates that the texture was modified since the last time it was flushed. /// - public bool FlushStale { get; private set; } + public bool ModifiedSinceLastFlush { get; set; } /// /// Increments when the host texture is swapped, or when the texture is removed from all pools. @@ -1417,7 +1417,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void SignalModified() { - FlushStale = false; _scaledSetScore = Math.Max(0, _scaledSetScore - 1); if (_modifiedStale || Group.HasCopyDependencies) @@ -1438,14 +1437,17 @@ namespace Ryujinx.Graphics.Gpu.Image { if (bound) { - FlushStale = false; _scaledSetScore = Math.Max(0, _scaledSetScore - 1); } if (_modifiedStale || Group.HasCopyDependencies || Group.HasFlushBuffer) { _modifiedStale = false; - Group.SignalModifying(this, bound); + + if (bound || ModifiedSinceLastFlush || Group.HasCopyDependencies || Group.HasFlushBuffer) + { + Group.SignalModifying(this, bound); + } } _physicalMemory.TextureCache.Lift(this); @@ -1703,12 +1705,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// The range of memory being unmapped public void Unmapped(MultiRange unmapRange) { - if (unmapRange.Contains(Range)) - { - // If this is a full unmap, prevent flushes until the texture is mapped again. - FlushStale = true; - } - ChangedMapping = true; if (Group.Storage == this) diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index 21d7939ad..d7de8a3cd 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -1660,13 +1660,13 @@ namespace Ryujinx.Graphics.Gpu.Image } // If size is zero, we have nothing to flush. - // If the flush is stale, we should ignore it because the texture was unmapped since the modified - // flag was set, and flushing it is not safe anymore as the GPU might no longer own the memory. - if (size == 0 || Storage.FlushStale) + if (size == 0) { return; } + Storage.ModifiedSinceLastFlush = false; + // There is a small gap here where the action is removed but _actionRegistered is still 1. // In this case it will skip registering the action, but here we are already handling it, // so there shouldn't be any issue as it's the same handler for all actions. diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs index ed181640a..e9f58314f 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -413,21 +413,35 @@ namespace Ryujinx.Graphics.Gpu.Image { bool anyChanged = false; - if (_rtHostDs != _rtDepthStencil?.HostTexture) - { - _rtHostDs = _rtDepthStencil?.HostTexture; + Texture dsTexture = _rtDepthStencil; + ITexture hostDsTexture = null; + if (dsTexture != null) + { + hostDsTexture = dsTexture.HostTexture; + dsTexture.ModifiedSinceLastFlush = true; + } + + if (_rtHostDs != hostDsTexture) + { + _rtHostDs = hostDsTexture; anyChanged = true; } for (int index = 0; index < _rtColors.Length; index++) { - ITexture hostTexture = _rtColors[index]?.HostTexture; + Texture texture = _rtColors[index]; + ITexture hostTexture = null; + + if (texture != null) + { + hostTexture = texture.HostTexture; + texture.ModifiedSinceLastFlush = true; + } if (_rtHostColors[index] != hostTexture) { _rtHostColors[index] = hostTexture; - anyChanged = true; } } From 6bce46621c9952bdc697c1977cb866b48cbfad58 Mon Sep 17 00:00:00 2001 From: shinra-electric <50119606+shinra-electric@users.noreply.github.com> Date: Tue, 14 Nov 2023 21:20:33 +0100 Subject: [PATCH 29/40] Change minimum OS to macOS 12 in Info.plist (#5925) This should prevent the app from opening on macOS 11 and lower, informing the user that their OS is unsupported. --- distribution/macos/Info.plist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distribution/macos/Info.plist b/distribution/macos/Info.plist index 6e068ba2d..51c71eaa1 100644 --- a/distribution/macos/Info.plist +++ b/distribution/macos/Info.plist @@ -43,7 +43,7 @@ LSApplicationCategoryType public.app-category.games LSMinimumSystemVersion - 11.0 + 12.0 UTExportedTypeDeclarations @@ -155,4 +155,4 @@ 200000 - \ No newline at end of file + From 1329c47ea46d3fa152dabe02cff82db84429545a Mon Sep 17 00:00:00 2001 From: gdkchan Date: Tue, 14 Nov 2023 22:24:54 -0300 Subject: [PATCH 30/40] Work around issue apparently caused by 5909 (#5926) --- src/Ryujinx.Graphics.Gpu/Image/Texture.cs | 6 +----- src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs | 5 +++-- src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs | 12 ++++++++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index 326272e7c..04c2b615f 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -1443,11 +1443,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_modifiedStale || Group.HasCopyDependencies || Group.HasFlushBuffer) { _modifiedStale = false; - - if (bound || ModifiedSinceLastFlush || Group.HasCopyDependencies || Group.HasFlushBuffer) - { - Group.SignalModifying(this, bound); - } + Group.SignalModifying(this, bound, bound || ModifiedSinceLastFlush || Group.HasCopyDependencies || Group.HasFlushBuffer); } _physicalMemory.TextureCache.Lift(this); diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index d7de8a3cd..e828cb9f1 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -709,7 +709,8 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// The texture that has been modified /// True if this texture is being bound, false if unbound - public void SignalModifying(Texture texture, bool bound) + /// Indicates if the modified flag should be set + public void SignalModifying(Texture texture, bool bound, bool setModified) { ModifiedSequence = _context.GetModifiedSequence(); @@ -721,7 +722,7 @@ namespace Ryujinx.Graphics.Gpu.Image { TextureGroupHandle group = _handles[baseHandle + i]; - group.SignalModifying(bound, _context); + group.SignalModifying(bound, _context, setModified); } }); } diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs index 84171c7a9..717792e04 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs @@ -304,9 +304,17 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// True if this handle is being bound, false if unbound /// The GPU context to register a sync action on - public void SignalModifying(bool bound, GpuContext context) + /// Indicates if the modified flag should be set + public void SignalModifying(bool bound, GpuContext context, bool setModified) { - SignalModified(context); + if (setModified) + { + SignalModified(context); + } + else + { + RegisterSync(context); + } if (!bound && _syncActionRegistered && NextSyncCopies()) { From 5b3662b793b3a34acc12c45c3c1691b7302d4b1d Mon Sep 17 00:00:00 2001 From: gdkchan Date: Tue, 14 Nov 2023 23:24:42 -0300 Subject: [PATCH 31/40] Disable DMA GPU copy for block linear to linear copies (#5927) * Disable DMA GPU copy for block linear to linear copies * Simplify check * PR feedback --- src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs index e6557780b..199cc423e 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs @@ -279,7 +279,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma bool completeSource = IsTextureCopyComplete(src, srcLinear, srcBpp, srcStride, xCount, yCount); bool completeDest = IsTextureCopyComplete(dst, dstLinear, dstBpp, dstStride, xCount, yCount); - if (completeSource && completeDest) + // Try to set the texture data directly, + // but only if we are doing a complete copy, + // and not for block linear to linear copies, since those are typically accessed from the CPU. + + if (completeSource && completeDest && !(dstLinear && !srcLinear)) { var target = memoryManager.Physical.TextureCache.FindTexture( memoryManager, From 29e192f241136ce910071ff4fdedda5bd1d9b838 Mon Sep 17 00:00:00 2001 From: Zoltan Csizmadia Date: Wed, 15 Nov 2023 10:41:31 -0600 Subject: [PATCH 32/40] Migrate to .NET 8 (#5887) * Change TargetFramework to net8.0 * Disable info messages * Fix warings * Disable additional analyzer messages * Fix typo * Add whitespace * Fix ref vs in warnings * Use explicit [In] on array parameters * No need to guard Remove with Contains * Use 'ArgumentOutOfRangeException.ThrowIf...' instead of explicitly throwing a new exception instance * Bump .NET SDK version * Enable JsonSerializerIsReflectionEnabledByDefault * Use 8.0.100 GA release * Bump System package versions --------- Co-authored-by: Zoltan Csizmadia --- .editorconfig | 23 +++++++++++++++++ .github/workflows/build.yml | 4 +-- .github/workflows/release.yml | 2 +- Directory.Packages.props | 10 ++++---- README.md | 2 +- global.json | 4 +-- src/ARMeilleure/ARMeilleure.csproj | 2 +- .../Ryujinx.Audio.Backends.OpenAL.csproj | 2 +- .../Ryujinx.Audio.Backends.SDL2.csproj | 2 +- .../Ryujinx.Audio.Backends.SoundIo.csproj | 8 +++--- .../Renderer/Utils/SpanIOHelper.cs | 4 +-- src/Ryujinx.Audio/Ryujinx.Audio.csproj | 2 +- src/Ryujinx.Ava/Ryujinx.Ava.csproj | 16 +++++++++--- .../UI/Helpers/Win32NativeInterop.cs | 2 +- src/Ryujinx.Common/Ryujinx.Common.csproj | 2 +- src/Ryujinx.Cpu/Ryujinx.Cpu.csproj | 2 +- .../Ryujinx.Graphics.Device.csproj | 2 +- .../Ryujinx.Graphics.GAL.csproj | 2 +- .../Ryujinx.Graphics.Gpu.csproj | 2 +- .../Synchronization/SynchronizationManager.cs | 25 ++++--------------- .../Ryujinx.Graphics.Host1x.csproj | 2 +- .../Ryujinx.Graphics.Nvdec.FFmpeg.csproj | 2 +- .../Ryujinx.Graphics.Nvdec.Vp9.csproj | 2 +- .../Ryujinx.Graphics.Nvdec.csproj | 2 +- .../Ryujinx.Graphics.OpenGL.csproj | 2 +- .../IntermediateRepresentation/PhiNode.cs | 5 +--- .../Ryujinx.Graphics.Shader.csproj | 2 +- .../Ryujinx.Graphics.Texture.csproj | 2 +- .../Utils/RgbaColor32.cs | 9 ++++--- .../Ryujinx.Graphics.Vic.csproj | 2 +- .../Ryujinx.Graphics.Video.csproj | 2 +- src/Ryujinx.Graphics.Vulkan/PipelineBase.cs | 2 +- .../Ryujinx.Graphics.Vulkan.csproj | 2 +- .../VulkanException.cs | 4 --- .../ServiceNotImplementedException.cs | 2 -- src/Ryujinx.HLE/FileSystem/ContentManager.cs | 5 +--- .../NvHostCtrl/Types/NvHostSyncPt.cs | 5 +--- .../Services/Sockets/Bsd/Types/BsdMsgHdr.cs | 12 ++++----- .../Services/Sockets/Sfdnsres/IResolver.cs | 2 +- .../Sfdnsres/Types/AddrInfoSerialized.cs | 6 ++--- .../SslService/SslManagedSocketConnection.cs | 4 +-- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 2 +- .../Ryujinx.Headless.SDL2.csproj | 8 +++--- .../Ryujinx.Horizon.Common.csproj | 2 +- src/Ryujinx.Horizon/Ryujinx.Horizon.csproj | 2 +- .../Ryujinx.Input.SDL2.csproj | 2 +- src/Ryujinx.Input/Ryujinx.Input.csproj | 2 +- src/Ryujinx.Memory/Range/MultiRange.cs | 10 ++------ src/Ryujinx.Memory/Ryujinx.Memory.csproj | 2 +- .../Ryujinx.SDL2.Common.csproj | 2 +- .../Ryujinx.ShaderTools.csproj | 2 +- .../Ryujinx.Tests.Memory.csproj | 2 +- .../Ryujinx.Tests.Unicorn.csproj | 2 +- src/Ryujinx.Tests/Ryujinx.Tests.csproj | 2 +- .../Ryujinx.Ui.Common.csproj | 2 +- src/Ryujinx/Ryujinx.csproj | 6 ++--- src/Spv.Generator/Spv.Generator.csproj | 2 +- 57 files changed, 121 insertions(+), 123 deletions(-) diff --git a/.editorconfig b/.editorconfig index 9d695c7fb..db08c67e2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -233,6 +233,29 @@ dotnet_naming_style.IPascalCase.required_suffix = dotnet_naming_style.IPascalCase.word_separator = dotnet_naming_style.IPascalCase.capitalization = pascal_case +# TODO: +# .NET 8 migration (new warnings are caused by the NET 8 C# compiler and analyzer) +# The following info messages might need to be fixed in the source code instead of hiding the actual message +# Without the following lines, dotnet format would fail +# Disable "Collection initialization can be simplified" +dotnet_diagnostic.IDE0028.severity = none +dotnet_diagnostic.IDE0300.severity = none +dotnet_diagnostic.IDE0301.severity = none +dotnet_diagnostic.IDE0302.severity = none +dotnet_diagnostic.IDE0305.severity = none +# Disable "'new' expression can be simplified" +dotnet_diagnostic.IDE0090.severity = none +# Disable "Use primary constructor" +dotnet_diagnostic.IDE0290.severity = none +# Disable "Member '' does not access instance data and can be marked as static" +dotnet_diagnostic.CA1822.severity = none +# Disable "Change type of field '' from '' to '' for improved performance" +dotnet_diagnostic.CA1859.severity = none +# Disable "Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array" +dotnet_diagnostic.CA1861.severity = none +# Disable "Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'" +dotnet_diagnostic.CA1862.severity = none + [src/Ryujinx.HLE/HOS/Services/**.cs] # Disable "mark members as static" rule for services dotnet_diagnostic.CA1822.severity = none diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16058d9f8..7d46adc2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: - os: windows-latest OS_NAME: Windows x64 - DOTNET_RUNTIME_IDENTIFIER: win10-x64 + DOTNET_RUNTIME_IDENTIFIER: win-x64 RELEASE_ZIP_OS_NAME: win_x64 fail-fast: false @@ -155,4 +155,4 @@ jobs: with: name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal path: "publish_headless/*.tar.gz" - if: github.event_name == 'pull_request' \ No newline at end of file + if: github.event_name == 'pull_request' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 008561f9c..4dc1d091d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: - os: windows-latest OS_NAME: Windows x64 - DOTNET_RUNTIME_IDENTIFIER: win10-x64 + DOTNET_RUNTIME_IDENTIFIER: win-x64 RELEASE_ZIP_OS_NAME: win_x64 steps: - uses: actions/checkout@v4 diff --git a/Directory.Packages.props b/Directory.Packages.props index 009430f92..3d8aac1a7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + @@ -45,10 +45,10 @@ - - - - + + + + diff --git a/README.md b/README.md index b2a6646f5..b2f95cc1f 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ The latest automatic build for Windows, macOS, and Linux can be found on the [Of If you wish to build the emulator yourself, follow these steps: ### Step 1 -Install the X64 version of [.NET 7.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/7.0). +Install the X64 version of [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0). ### Step 2 Either use `git clone https://github.com/Ryujinx/Ryujinx` on the command line to clone the repository or use Code --> Download zip button to get the files. diff --git a/global.json b/global.json index 39ccef0d0..391ba3c2a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.200", + "version": "8.0.100", "rollForward": "latestFeature" } -} \ No newline at end of file +} diff --git a/src/ARMeilleure/ARMeilleure.csproj b/src/ARMeilleure/ARMeilleure.csproj index fa5551154..550e50c26 100644 --- a/src/ARMeilleure/ARMeilleure.csproj +++ b/src/ARMeilleure/ARMeilleure.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj index 115a37601..3863e4439 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj +++ b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj b/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj index 525f1f5b6..dd18e70a1 100644 --- a/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj +++ b/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj index 9f242dbe2..1d92d9d2e 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj +++ b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj @@ -1,9 +1,9 @@ - net7.0 + net8.0 true - win10-x64;linux-x64;osx-x64 + win-x64;osx-x64;linux-x64 @@ -15,11 +15,11 @@ PreserveNewest libsoundio.dll - + PreserveNewest libsoundio.dylib - + PreserveNewest libsoundio.so diff --git a/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs b/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs index 4771ae4dd..abbb6ea6c 100644 --- a/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs +++ b/src/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs @@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Utils throw new ArgumentOutOfRangeException(nameof(backingMemory), backingMemory.Length, null); } - MemoryMarshal.Write(backingMemory.Span[..size], ref data); + MemoryMarshal.Write(backingMemory.Span[..size], in data); backingMemory = backingMemory[size..]; } @@ -45,7 +45,7 @@ namespace Ryujinx.Audio.Renderer.Utils throw new ArgumentOutOfRangeException(nameof(backingMemory), backingMemory.Length, null); } - MemoryMarshal.Write(backingMemory[..size], ref data); + MemoryMarshal.Write(backingMemory[..size], in data); backingMemory = backingMemory[size..]; } diff --git a/src/Ryujinx.Audio/Ryujinx.Audio.csproj b/src/Ryujinx.Audio/Ryujinx.Audio.csproj index 4a159eb5c..fc20f4ec4 100644 --- a/src/Ryujinx.Audio/Ryujinx.Audio.csproj +++ b/src/Ryujinx.Audio/Ryujinx.Audio.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Ava/Ryujinx.Ava.csproj b/src/Ryujinx.Ava/Ryujinx.Ava.csproj index f0e99f427..6812e57c4 100644 --- a/src/Ryujinx.Ava/Ryujinx.Ava.csproj +++ b/src/Ryujinx.Ava/Ryujinx.Ava.csproj @@ -1,7 +1,7 @@  - net7.0 - win10-x64;osx-x64;linux-x64 + net8.0 + win-x64;osx-x64;linux-x64 Exe true 1.0.0-dirty @@ -25,6 +25,16 @@ partial + + + true + + @@ -40,7 +50,7 @@ - + diff --git a/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs b/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs index ca55d0399..35d16b9e0 100644 --- a/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs +++ b/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs @@ -86,7 +86,7 @@ namespace Ryujinx.Ava.UI.Helpers public static partial IntPtr SetCursor(IntPtr handle); [LibraryImport("user32.dll")] - public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, byte[] pvAndPlane, byte[] pvXorPlane); + public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, [In] byte[] pvAndPlane, [In] byte[] pvXorPlane); [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")] public static partial ushort RegisterClassEx(ref WndClassEx param); diff --git a/src/Ryujinx.Common/Ryujinx.Common.csproj b/src/Ryujinx.Common/Ryujinx.Common.csproj index c02b11e0c..da2f13a21 100644 --- a/src/Ryujinx.Common/Ryujinx.Common.csproj +++ b/src/Ryujinx.Common/Ryujinx.Common.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true $(DefineConstants);$(ExtraDefineConstants) diff --git a/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj b/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj index 7da8da25a..5a6bf5c3d 100644 --- a/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj +++ b/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj index 082dac9c2..ae2821edb 100644 --- a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj +++ b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj b/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj index 189108a39..d88b641a3 100644 --- a/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj +++ b/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj index 5255a6e00..6f1cce6ac 100644 --- a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj +++ b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs index ccec763e3..2d5eede58 100644 --- a/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Synchronization/SynchronizationManager.cs @@ -37,10 +37,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// The incremented value of the syncpoint public uint IncrementSyncpoint(uint id) { - if (id >= MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); return _syncpoints[id].Increment(); } @@ -53,10 +50,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// The value of the syncpoint public uint GetSyncpointValue(uint id) { - if (id >= MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); return _syncpoints[id].Value; } @@ -72,10 +66,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// The created SyncpointWaiterHandle object or null if already past threshold public SyncpointWaiterHandle RegisterCallbackOnSyncpoint(uint id, uint threshold, Action callback) { - if (id >= MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); return _syncpoints[id].RegisterCallback(threshold, callback); } @@ -88,10 +79,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// Thrown when id >= MaxHardwareSyncpoints public void UnregisterCallback(uint id, SyncpointWaiterHandle waiterInformation) { - if (id >= MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); _syncpoints[id].UnregisterCallback(waiterInformation); } @@ -107,10 +95,7 @@ namespace Ryujinx.Graphics.Gpu.Synchronization /// True if timed out public bool WaitOnSyncpoint(uint id, uint threshold, TimeSpan timeout) { - if (id >= MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)MaxHardwareSyncpoints); // TODO: Remove this when GPU channel scheduling will be implemented. if (timeout == Timeout.InfiniteTimeSpan) diff --git a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj index 3cff4061e..22959fad8 100644 --- a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj +++ b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj index bff1e803b..d1a6358c2 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj b/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj index bff1e803b..d1a6358c2 100644 --- a/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj +++ b/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj b/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj index bfba98a73..fd49a7c80 100644 --- a/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj +++ b/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj index 2313cc68f..3d64da99b 100644 --- a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj +++ b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs index 6c95c7bdd..f4c4fef42 100644 --- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs +++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/PhiNode.cs @@ -69,10 +69,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation public Operand GetDest(int index) { - if (index != 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } + ArgumentOutOfRangeException.ThrowIfNotEqual(index, 0); return _dest; } diff --git a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj index ea9a7821b..8ccf5348f 100644 --- a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj +++ b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj b/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj index 70e3453c3..51721490e 100644 --- a/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj +++ b/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs b/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs index de7c9262d..8ca3f89bc 100644 --- a/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs +++ b/src/Ryujinx.Graphics.Texture/Utils/RgbaColor32.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; @@ -102,11 +103,11 @@ namespace Ryujinx.Graphics.Texture.Utils } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaColor32 operator <<(RgbaColor32 x, int shift) + public static RgbaColor32 operator <<(RgbaColor32 x, [ConstantExpected] byte shift) { if (Sse2.IsSupported) { - return new RgbaColor32(Sse2.ShiftLeftLogical(x._color, (byte)shift)); + return new RgbaColor32(Sse2.ShiftLeftLogical(x._color, shift)); } else { @@ -115,11 +116,11 @@ namespace Ryujinx.Graphics.Texture.Utils } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RgbaColor32 operator >>(RgbaColor32 x, int shift) + public static RgbaColor32 operator >>(RgbaColor32 x, [ConstantExpected] byte shift) { if (Sse2.IsSupported) { - return new RgbaColor32(Sse2.ShiftRightLogical(x._color, (byte)shift)); + return new RgbaColor32(Sse2.ShiftRightLogical(x._color, shift)); } else { diff --git a/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj b/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj index 2a7cdd985..cfebcfa2a 100644 --- a/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj +++ b/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj b/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj index 9cf37670e..abff58a53 100644 --- a/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj +++ b/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs index 156b3db16..7346d7891 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs @@ -189,7 +189,7 @@ namespace Ryujinx.Graphics.Vulkan PipelineStageFlags.AllCommandsBit, 0, 1, - new ReadOnlySpan(memoryBarrier), + new ReadOnlySpan(in memoryBarrier), 0, ReadOnlySpan.Empty, 0, diff --git a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj index 8d30457e2..f6a7be91e 100644 --- a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj +++ b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanException.cs b/src/Ryujinx.Graphics.Vulkan/VulkanException.cs index 983f03d4e..2d9dbc348 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanException.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanException.cs @@ -33,9 +33,5 @@ namespace Ryujinx.Graphics.Vulkan public VulkanException(string message, Exception innerException) : base(message, innerException) { } - - protected VulkanException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } } diff --git a/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs b/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs index e43c838a2..9cb1cf2c7 100644 --- a/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs +++ b/src/Ryujinx.HLE/Exceptions/ServiceNotImplementedException.cs @@ -35,8 +35,6 @@ namespace Ryujinx.HLE.Exceptions Request = context.Request; } - protected ServiceNotImplementedException(SerializationInfo info, StreamingContext context) : base(info, context) { } - public override string Message { get diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index 724cb675c..b27eb5ead 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -420,10 +420,7 @@ namespace Ryujinx.HLE.FileSystem if (locationList != null) { - if (locationList.Contains(entry)) - { - locationList.Remove(entry); - } + locationList.Remove(entry); locationList.AddLast(entry); } diff --git a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs index 9c6d025eb..b83c642e5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nv/NvDrvServices/NvHostCtrl/Types/NvHostSyncPt.cs @@ -85,10 +85,7 @@ namespace Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl public void SetSyncpointMinEqualSyncpointMax(uint id) { - if (id >= SynchronizationManager.MaxHardwareSyncpoints) - { - throw new ArgumentOutOfRangeException(nameof(id)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(id, (uint)SynchronizationManager.MaxHardwareSyncpoints); int value = (int)ReadSyncpointValue(id); diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs index 07c97182c..62a7ccb59 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Types/BsdMsgHdr.cs @@ -27,7 +27,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types int controlLength = message.Control == null ? 0 : message.Control.Length; BsdSocketFlags flags = message.Flags; - if (!MemoryMarshal.TryWrite(rawData, ref msgNameLength)) + if (!MemoryMarshal.TryWrite(rawData, in msgNameLength)) { return LinuxError.EFAULT; } @@ -45,7 +45,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types rawData = rawData[msgNameLength..]; } - if (!MemoryMarshal.TryWrite(rawData, ref iovCount)) + if (!MemoryMarshal.TryWrite(rawData, in iovCount)) { return LinuxError.EFAULT; } @@ -58,7 +58,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types { ulong iovLength = (ulong)message.Iov[index].Length; - if (!MemoryMarshal.TryWrite(rawData, ref iovLength)) + if (!MemoryMarshal.TryWrite(rawData, in iovLength)) { return LinuxError.EFAULT; } @@ -78,7 +78,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types } } - if (!MemoryMarshal.TryWrite(rawData, ref controlLength)) + if (!MemoryMarshal.TryWrite(rawData, in controlLength)) { return LinuxError.EFAULT; } @@ -96,14 +96,14 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types rawData = rawData[controlLength..]; } - if (!MemoryMarshal.TryWrite(rawData, ref flags)) + if (!MemoryMarshal.TryWrite(rawData, in flags)) { return LinuxError.EFAULT; } rawData = rawData[sizeof(BsdSocketFlags)..]; - if (!MemoryMarshal.TryWrite(rawData, ref message.Length)) + if (!MemoryMarshal.TryWrite(rawData, in message.Length)) { return LinuxError.EFAULT; } diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs index d0fb6675a..39af90383 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs @@ -654,7 +654,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres } uint sentinel = 0; - MemoryMarshal.Write(data, ref sentinel); + MemoryMarshal.Write(data, in sentinel); data = data[sizeof(uint)..]; return region.Memory.Span.Length - data.Length; diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs index a0613d7bc..b57b0d5ca 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/Types/AddrInfoSerialized.cs @@ -94,7 +94,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types Header.ToNetworkOrder(); - MemoryMarshal.Write(buffer, ref Header); + MemoryMarshal.Write(buffer, in Header); buffer = buffer[Unsafe.SizeOf()..]; @@ -103,7 +103,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types AddrInfo4 socketAddress = SocketAddress.Value; socketAddress.ToNetworkOrder(); - MemoryMarshal.Write(buffer, ref socketAddress); + MemoryMarshal.Write(buffer, in socketAddress); buffer = buffer[Unsafe.SizeOf()..]; } @@ -117,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres.Types Array4 rawIPv4Address = RawIPv4Address.Value; AddrInfo4.RawIpv4AddressNetworkEndianSwap(ref rawIPv4Address); - MemoryMarshal.Write(buffer, ref rawIPv4Address); + MemoryMarshal.Write(buffer, in rawIPv4Address); buffer = buffer[Unsafe.SizeOf>()..]; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs index dab099aab..e3c05df51 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs @@ -161,7 +161,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService } else { - throw exception; + throw; } } finally @@ -206,7 +206,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService } else { - throw exception; + throw; } } finally diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index f3439cc8f..370933ccf 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj index d2585c563..7b13df736 100644 --- a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj +++ b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj @@ -1,8 +1,8 @@  - net7.0 - win10-x64;osx-x64;linux-x64 + net8.0 + win-x64;osx-x64;linux-x64 Exe true 1.0.0-dirty @@ -34,7 +34,7 @@ - + @@ -69,4 +69,4 @@ true partial - \ No newline at end of file + diff --git a/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj b/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj index d04c5a9b6..fa1544c4f 100644 --- a/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj +++ b/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj index 0139c367f..ae40f7b5e 100644 --- a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj +++ b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 diff --git a/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj b/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj index 817a96e2e..1ab79d08a 100644 --- a/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj +++ b/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.Input/Ryujinx.Input.csproj b/src/Ryujinx.Input/Ryujinx.Input.csproj index df462734f..59a9eeb61 100644 --- a/src/Ryujinx.Input/Ryujinx.Input.csproj +++ b/src/Ryujinx.Input/Ryujinx.Input.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx.Memory/Range/MultiRange.cs b/src/Ryujinx.Memory/Range/MultiRange.cs index 7011e528e..5a0b4178a 100644 --- a/src/Ryujinx.Memory/Range/MultiRange.cs +++ b/src/Ryujinx.Memory/Range/MultiRange.cs @@ -52,10 +52,7 @@ namespace Ryujinx.Memory.Range { if (HasSingleRange) { - if (_singleRange.Size - offset < size) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } + ArgumentOutOfRangeException.ThrowIfGreaterThan(size, _singleRange.Size - offset); return new MultiRange(_singleRange.Address + offset, size); } @@ -108,10 +105,7 @@ namespace Ryujinx.Memory.Range { if (HasSingleRange) { - if (index != 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } + ArgumentOutOfRangeException.ThrowIfNotEqual(index, 0); return _singleRange; } diff --git a/src/Ryujinx.Memory/Ryujinx.Memory.csproj b/src/Ryujinx.Memory/Ryujinx.Memory.csproj index 91e46e48e..8310a3e5c 100644 --- a/src/Ryujinx.Memory/Ryujinx.Memory.csproj +++ b/src/Ryujinx.Memory/Ryujinx.Memory.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 true diff --git a/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj b/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj index 955e6d3f1..8e7953045 100644 --- a/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj +++ b/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj b/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj index 74b4ec2f7..ab89fb5c7 100644 --- a/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj +++ b/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 Exe Debug;Release diff --git a/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj b/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj index 4dcb69623..f05060838 100644 --- a/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj +++ b/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 false diff --git a/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj b/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj index d925546fe..befacfb22 100644 --- a/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj +++ b/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true Debug;Release diff --git a/src/Ryujinx.Tests/Ryujinx.Tests.csproj b/src/Ryujinx.Tests/Ryujinx.Tests.csproj index ab331ce58..3be9787a3 100644 --- a/src/Ryujinx.Tests/Ryujinx.Tests.csproj +++ b/src/Ryujinx.Tests/Ryujinx.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 Exe false diff --git a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj index 3da47431f..7aff09ff6 100644 --- a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj +++ b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 true diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 5b5ed4637..9890b761b 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -1,8 +1,8 @@ - net7.0 - win10-x64;osx-x64;linux-x64 + net8.0 + win-x64;osx-x64;linux-x64 Exe true 1.0.0-dirty @@ -25,7 +25,7 @@ - + diff --git a/src/Spv.Generator/Spv.Generator.csproj b/src/Spv.Generator/Spv.Generator.csproj index 082dac9c2..ae2821edb 100644 --- a/src/Spv.Generator/Spv.Generator.csproj +++ b/src/Spv.Generator/Spv.Generator.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 From 388446c255566d67240905bc6efa7af5a71c34b5 Mon Sep 17 00:00:00 2001 From: Mary Guillemard Date: Wed, 15 Nov 2023 18:12:19 +0100 Subject: [PATCH 33/40] infra: Workaround Microsoft.NET.ILLink.Tasks restore failure on Flathub This package seems to be required for triming now but isn't restored by default. This changes the flatpak pusher to publish so we are sure that the package is in the cache. Signed-off-by: Mary Guillemard --- .github/workflows/flatpak.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 4c8ba3e17..c1ae9fe8f 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -49,7 +49,9 @@ jobs: run: python -m pip install PyYAML lxml - name: Restore Nuget packages - run: dotnet restore Ryujinx/${{ env.RYUJINX_PROJECT_FILE }} + # With .NET 8.0.100, Microsoft.NET.ILLink.Tasks isn't restored by default and only seems to appears when publishing. + # So we just publish to grab the dependencies + run: dotnet publish -c Release -r linux-x64 Ryujinx/${{ env.RYUJINX_PROJECT_FILE }} --self-contained - name: Generate nuget_sources.json shell: python From cdc8fed64fc8e51fe626b0a369902932db0ec49c Mon Sep 17 00:00:00 2001 From: Mary Guillemard Date: Wed, 15 Nov 2023 19:08:46 +0100 Subject: [PATCH 34/40] chore: Update OpenTK to 4.8.1 (#5912) OpenTK.OpenAL was renamed to OpenTK.Audio.OpenAL. Signed-off-by: Mary Guillemard --- Directory.Packages.props | 8 ++++---- .../Ryujinx.Audio.Backends.OpenAL.csproj | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3d8aac1a7..44f9b5d40 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,10 +27,10 @@ - - - - + + + + diff --git a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj index 3863e4439..b5fd8f9e7 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj +++ b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj @@ -5,7 +5,7 @@ - + From dcf10561b996cdba111c5a3c3fe128781ab44021 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Wed, 15 Nov 2023 21:36:25 -0300 Subject: [PATCH 35/40] Fix missing texture flush for draw then DMA copy sequence without render target change (#5933) * Unbind render targets before DMA copy * Move DirtyAction to TextureGroupHandle * Fix lost copy dependency bug * XML doc --- .../Engine/Dma/DmaClass.cs | 1 + src/Ryujinx.Graphics.Gpu/Image/Texture.cs | 7 +- .../Image/TextureGroup.cs | 38 +--------- .../Image/TextureGroupHandle.cs | 45 ++++++++---- .../Image/TextureManager.cs | 72 +++++++++++++++++-- 5 files changed, 102 insertions(+), 61 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs index 199cc423e..b1921bd51 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Dma/DmaClass.cs @@ -211,6 +211,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma int xCount = (int)_state.State.LineLengthIn; int yCount = (int)_state.State.LineCount; + _channel.TextureManager.RefreshModifiedTextures(); _3dEngine.CreatePendingSyncs(); _3dEngine.FlushUboDirty(); diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs index 04c2b615f..f1615b388 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -101,11 +101,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// public bool AlwaysFlushOnOverlap { get; private set; } - /// - /// Indicates that the texture was modified since the last time it was flushed. - /// - public bool ModifiedSinceLastFlush { get; set; } - /// /// Increments when the host texture is swapped, or when the texture is removed from all pools. /// @@ -1443,7 +1438,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_modifiedStale || Group.HasCopyDependencies || Group.HasFlushBuffer) { _modifiedStale = false; - Group.SignalModifying(this, bound, bound || ModifiedSinceLastFlush || Group.HasCopyDependencies || Group.HasFlushBuffer); + Group.SignalModifying(this, bound); } _physicalMemory.TextureCache.Lift(this); diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs index e828cb9f1..b93f3832d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -709,8 +709,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// The texture that has been modified /// True if this texture is being bound, false if unbound - /// Indicates if the modified flag should be set - public void SignalModifying(Texture texture, bool bound, bool setModified) + public void SignalModifying(Texture texture, bool bound) { ModifiedSequence = _context.GetModifiedSequence(); @@ -722,7 +721,7 @@ namespace Ryujinx.Graphics.Gpu.Image { TextureGroupHandle group = _handles[baseHandle + i]; - group.SignalModifying(bound, _context, setModified); + group.SignalModifying(bound, _context); } }); } @@ -993,26 +992,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// - /// The action to perform when a memory tracking handle is flipped to dirty. - /// This notifies overlapping textures that the memory needs to be synchronized. - /// - /// The handle that a dirty flag was set on - private void DirtyAction(TextureGroupHandle groupHandle) - { - // Notify all textures that belong to this handle. - - Storage.SignalGroupDirty(); - - lock (groupHandle.Overlaps) - { - foreach (Texture overlap in groupHandle.Overlaps) - { - overlap.SignalGroupDirty(); - } - } - } - /// /// Generate a CpuRegionHandle for a given address and size range in CPU VA. /// @@ -1084,11 +1063,6 @@ namespace Ryujinx.Graphics.Gpu.Image views, result.ToArray()); - foreach (RegionHandle handle in result) - { - handle.RegisterDirtyEvent(() => DirtyAction(groupHandle)); - } - return groupHandle; } @@ -1360,11 +1334,6 @@ namespace Ryujinx.Graphics.Gpu.Image var groupHandle = new TextureGroupHandle(this, 0, Storage.Size, _views, 0, 0, 0, _allOffsets.Length, cpuRegionHandles); - foreach (RegionHandle handle in cpuRegionHandles) - { - handle.RegisterDirtyEvent(() => DirtyAction(groupHandle)); - } - handles = new TextureGroupHandle[] { groupHandle }; } else @@ -1620,6 +1589,7 @@ namespace Ryujinx.Graphics.Gpu.Image if ((ignore == null || !handle.HasDependencyTo(ignore)) && handle.Modified) { handle.Modified = false; + handle.DeferredCopy = null; Storage.SignalModifiedDirty(); lock (handle.Overlaps) @@ -1666,8 +1636,6 @@ namespace Ryujinx.Graphics.Gpu.Image return; } - Storage.ModifiedSinceLastFlush = false; - // There is a small gap here where the action is removed but _actionRegistered is still 1. // In this case it will skip registering the action, but here we are already handling it, // so there shouldn't be any issue as it's the same handler for all actions. diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs index 717792e04..72b743906 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs @@ -152,6 +152,32 @@ namespace Ryujinx.Graphics.Gpu.Image // Linear textures are presumed to be used for readback initially. _flushBalance = FlushBalanceThreshold + FlushBalanceIncrement; } + + foreach (RegionHandle handle in handles) + { + handle.RegisterDirtyEvent(DirtyAction); + } + } + + /// + /// The action to perform when a memory tracking handle is flipped to dirty. + /// This notifies overlapping textures that the memory needs to be synchronized. + /// + private void DirtyAction() + { + // Notify all textures that belong to this handle. + + _group.Storage.SignalGroupDirty(); + + lock (Overlaps) + { + foreach (Texture overlap in Overlaps) + { + overlap.SignalGroupDirty(); + } + } + + DeferredCopy = null; } /// @@ -304,17 +330,9 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// True if this handle is being bound, false if unbound /// The GPU context to register a sync action on - /// Indicates if the modified flag should be set - public void SignalModifying(bool bound, GpuContext context, bool setModified) + public void SignalModifying(bool bound, GpuContext context) { - if (setModified) - { - SignalModified(context); - } - else - { - RegisterSync(context); - } + SignalModified(context); if (!bound && _syncActionRegistered && NextSyncCopies()) { @@ -457,7 +475,6 @@ namespace Ryujinx.Graphics.Gpu.Image public void DeferCopy(TextureGroupHandle copyFrom) { Modified = false; - DeferredCopy = copyFrom; _group.Storage.SignalGroupDirty(); @@ -514,7 +531,7 @@ namespace Ryujinx.Graphics.Gpu.Image { existing.Other.Handle.CreateCopyDependency(this); - if (copyToOther) + if (copyToOther && Modified) { existing.Other.Handle.DeferCopy(this); } @@ -558,10 +575,10 @@ namespace Ryujinx.Graphics.Gpu.Image if (fromHandle != null) { // Only copy if the copy texture is still modified. - // It will be set as unmodified if new data is written from CPU, as the data previously in the texture will flush. + // DeferredCopy will be set to null if new data is written from CPU (see the DirtyAction method). // It will also set as unmodified if a copy is deferred to it. - shouldCopy = fromHandle.Modified; + shouldCopy = true; if (fromHandle._bindCount == 0) { diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs index e9f58314f..fa278bbd9 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -20,8 +20,10 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly Texture[] _rtColors; private readonly ITexture[] _rtHostColors; + private readonly bool[] _rtColorsBound; private Texture _rtDepthStencil; private ITexture _rtHostDs; + private bool _rtDsBound; public int ClipRegionWidth { get; private set; } public int ClipRegionHeight { get; private set; } @@ -51,6 +53,7 @@ namespace Ryujinx.Graphics.Gpu.Image _rtColors = new Texture[Constants.TotalRenderTargets]; _rtHostColors = new ITexture[Constants.TotalRenderTargets]; + _rtColorsBound = new bool[Constants.TotalRenderTargets]; } /// @@ -154,7 +157,14 @@ namespace Ryujinx.Graphics.Gpu.Image if (_rtColors[index] != color) { - _rtColors[index]?.SignalModifying(false); + if (_rtColorsBound[index]) + { + _rtColors[index]?.SignalModifying(false); + } + else + { + _rtColorsBound[index] = true; + } if (color != null) { @@ -180,7 +190,14 @@ namespace Ryujinx.Graphics.Gpu.Image if (_rtDepthStencil != depthStencil) { - _rtDepthStencil?.SignalModifying(false); + if (_rtDsBound) + { + _rtDepthStencil?.SignalModifying(false); + } + else + { + _rtDsBound = true; + } if (depthStencil != null) { @@ -419,7 +436,12 @@ namespace Ryujinx.Graphics.Gpu.Image if (dsTexture != null) { hostDsTexture = dsTexture.HostTexture; - dsTexture.ModifiedSinceLastFlush = true; + + if (!_rtDsBound) + { + dsTexture.SignalModifying(true); + _rtDsBound = true; + } } if (_rtHostDs != hostDsTexture) @@ -436,7 +458,12 @@ namespace Ryujinx.Graphics.Gpu.Image if (texture != null) { hostTexture = texture.HostTexture; - texture.ModifiedSinceLastFlush = true; + + if (!_rtColorsBound[index]) + { + texture.SignalModifying(true); + _rtColorsBound[index] = true; + } } if (_rtHostColors[index] != hostTexture) @@ -466,6 +493,31 @@ namespace Ryujinx.Graphics.Gpu.Image _context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs); } + /// + /// Marks all currently bound render target textures as modified, and also makes them be set as modified again on next use. + /// + public void RefreshModifiedTextures() + { + Texture dsTexture = _rtDepthStencil; + + if (dsTexture != null && _rtDsBound) + { + dsTexture.SignalModifying(false); + _rtDsBound = false; + } + + for (int index = 0; index < _rtColors.Length; index++) + { + Texture texture = _rtColors[index]; + + if (texture != null && _rtColorsBound[index]) + { + texture.SignalModifying(false); + _rtColorsBound[index] = false; + } + } + } + /// /// Forces the texture and sampler pools to be re-loaded from the cache on next use. /// @@ -502,11 +554,19 @@ namespace Ryujinx.Graphics.Gpu.Image for (int i = 0; i < _rtColors.Length; i++) { - _rtColors[i]?.DecrementReferenceCount(); + if (_rtColorsBound[i]) + { + _rtColors[i]?.DecrementReferenceCount(); + } + _rtColors[i] = null; } - _rtDepthStencil?.DecrementReferenceCount(); + if (_rtDsBound) + { + _rtDepthStencil?.DecrementReferenceCount(); + } + _rtDepthStencil = null; } } From d11fe26aa319b352112c434167e5c43f7b6ee636 Mon Sep 17 00:00:00 2001 From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:09:15 -0500 Subject: [PATCH 36/40] Fix macOS Path (#5941) --- src/Ryujinx.Common/Configuration/AppDataManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.Common/Configuration/AppDataManager.cs b/src/Ryujinx.Common/Configuration/AppDataManager.cs index 1dbc1f0ce..2b4a594d3 100644 --- a/src/Ryujinx.Common/Configuration/AppDataManager.cs +++ b/src/Ryujinx.Common/Configuration/AppDataManager.cs @@ -48,7 +48,7 @@ namespace Ryujinx.Common.Configuration string appDataPath; if (OperatingSystem.IsMacOS()) { - appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support"); + appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support"); } else { From 82a638230e4b10c099d8517b8ef7b602f22a6887 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Thu, 16 Nov 2023 17:52:21 -0300 Subject: [PATCH 37/40] Fix JitCache.Unmap called with the same address freeing memory in use (#5937) --- src/ARMeilleure/Translation/Cache/JitCache.cs | 30 +++++-------------- .../Translation/Cache/JitUnwindWindows.cs | 2 +- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/ARMeilleure/Translation/Cache/JitCache.cs b/src/ARMeilleure/Translation/Cache/JitCache.cs index 91a054123..e2b5e2d10 100644 --- a/src/ARMeilleure/Translation/Cache/JitCache.cs +++ b/src/ARMeilleure/Translation/Cache/JitCache.cs @@ -117,12 +117,11 @@ namespace ARMeilleure.Translation.Cache int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64()); - bool result = TryFind(funcOffset, out CacheEntry entry); - Debug.Assert(result); - - _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); - - Remove(funcOffset); + if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset) + { + _cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); + _cacheEntries.RemoveAt(entryIndex); + } } } @@ -181,22 +180,7 @@ namespace ARMeilleure.Translation.Cache _cacheEntries.Insert(index, entry); } - private static void Remove(int offset) - { - int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default)); - - if (index < 0) - { - index = ~index - 1; - } - - if (index >= 0) - { - _cacheEntries.RemoveAt(index); - } - } - - public static bool TryFind(int offset, out CacheEntry entry) + public static bool TryFind(int offset, out CacheEntry entry, out int entryIndex) { lock (_lock) { @@ -210,11 +194,13 @@ namespace ARMeilleure.Translation.Cache if (index >= 0) { entry = _cacheEntries[index]; + entryIndex = index; return true; } } entry = default; + entryIndex = 0; return false; } } diff --git a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs index 91fd19c25..3957a7559 100644 --- a/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs +++ b/src/ARMeilleure/Translation/Cache/JitUnwindWindows.cs @@ -95,7 +95,7 @@ namespace ARMeilleure.Translation.Cache { int offset = (int)((long)controlPc - context.ToInt64()); - if (!JitCache.TryFind(offset, out CacheEntry funcEntry)) + if (!JitCache.TryFind(offset, out CacheEntry funcEntry, out _)) { return null; // Not found. } From aa96dcb1bede3693877e2f1eca3e169d8ee13ef1 Mon Sep 17 00:00:00 2001 From: MutantAura <44103205+MutantAura@users.noreply.github.com> Date: Sat, 18 Nov 2023 20:42:45 +0000 Subject: [PATCH 38/40] misc: Default to Vulkan if available or running on macOS (#5913) * Addition of default backend check. Vulkan is preferred if available or macOS. * import ordering format fix * Update src/Ryujinx/Program.cs Co-authored-by: gdkchan * remove redundant load types --------- Co-authored-by: gdkchan --- .../Configuration/ConfigurationLoadResult.cs | 9 ---- .../Configuration/ConfigurationState.cs | 23 ++++++---- .../Ryujinx.Ui.Common.csproj | 1 + src/Ryujinx/Program.cs | 44 +------------------ 4 files changed, 16 insertions(+), 61 deletions(-) delete mode 100644 src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs deleted file mode 100644 index 71366ba7c..000000000 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationLoadResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ryujinx.Ui.Common.Configuration -{ - public enum ConfigurationLoadResult - { - Success = 0, - NotLoaded = 1, - MigratedFromPreVulkan = 1 << 8, - } -} diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index c79fa56c6..b017d384c 100644 --- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -5,6 +5,7 @@ using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Vulkan; using Ryujinx.Ui.Common.Configuration.System; using Ryujinx.Ui.Common.Configuration.Ui; using Ryujinx.Ui.Common.Helper; @@ -763,7 +764,7 @@ namespace Ryujinx.Ui.Common.Configuration Graphics.ResScaleCustom.Value = 1.0f; Graphics.MaxAnisotropy.Value = -1.0f; Graphics.AspectRatio.Value = AspectRatio.Fixed16x9; - Graphics.GraphicsBackend.Value = OperatingSystem.IsMacOS() ? GraphicsBackend.Vulkan : GraphicsBackend.OpenGl; + Graphics.GraphicsBackend.Value = DefaultGraphicsBackend(); Graphics.PreferredGpu.Value = ""; Graphics.ShadersDumpPath.Value = ""; Logger.EnableDebug.Value = false; @@ -907,7 +908,7 @@ namespace Ryujinx.Ui.Common.Configuration }; } - public ConfigurationLoadResult Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) + public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) { bool configurationFileUpdated = false; @@ -916,12 +917,8 @@ namespace Ryujinx.Ui.Common.Configuration Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); LoadDefault(); - - return ConfigurationLoadResult.NotLoaded; } - ConfigurationLoadResult result = ConfigurationLoadResult.Success; - if (configurationFileFormat.Version < 2) { Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); @@ -1336,8 +1333,6 @@ namespace Ryujinx.Ui.Common.Configuration configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; - result |= ConfigurationLoadResult.MigratedFromPreVulkan; - configurationFileUpdated = true; } @@ -1535,8 +1530,18 @@ namespace Ryujinx.Ui.Common.Configuration Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); } + } - return result; + private static GraphicsBackend DefaultGraphicsBackend() + { + // Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan. + // Checks for if the Vulkan version and featureset is compatible should be performed within VulkanRenderer. + if (OperatingSystem.IsMacOS() || VulkanRenderer.GetPhysicalDevices().Length > 0) + { + return GraphicsBackend.Vulkan; + } + + return GraphicsBackend.OpenGl; } private static void LogValueChange(ReactiveEventArgs eventArgs, string valueName) diff --git a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj index 7aff09ff6..74331fdef 100644 --- a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj +++ b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj @@ -62,6 +62,7 @@ + diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index afb6a9925..597d00f30 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -177,8 +177,6 @@ namespace Ryujinx ? appDataConfigurationPath : null; - bool showVulkanPrompt = false; - if (ConfigurationPath == null) { // No configuration, we load the default values and save it to disk @@ -186,26 +184,17 @@ namespace Ryujinx ConfigurationState.Instance.LoadDefault(); ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath); - - showVulkanPrompt = true; } else { if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat)) { - ConfigurationLoadResult result = ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); - - if ((result & ConfigurationLoadResult.MigratedFromPreVulkan) != 0) - { - showVulkanPrompt = true; - } + ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); } else { ConfigurationState.Instance.LoadDefault(); - showVulkanPrompt = true; - Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}"); } } @@ -216,12 +205,10 @@ namespace Ryujinx if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl") { ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl; - showVulkanPrompt = false; } else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan") { ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan; - showVulkanPrompt = false; } } @@ -343,35 +330,6 @@ namespace Ryujinx }, TaskContinuationOptions.OnlyOnFaulted); } - if (showVulkanPrompt) - { - var buttonTexts = new Dictionary() - { - { 0, "Yes (Vulkan)" }, - { 1, "No (OpenGL)" }, - }; - - ResponseType response = GtkDialog.CreateCustomDialog( - "Ryujinx - Default graphics backend", - "Use Vulkan as default graphics backend?", - "Ryujinx now supports the Vulkan API. " + - "Vulkan greatly improves shader compilation performance, " + - "and fixes some graphical glitches; however, since it is a new feature, " + - "you may experience some issues that did not occur with OpenGL.\n\n" + - "Note that you will also lose any existing shader cache the first time you start a game " + - "on version 1.1.200 onwards, because Vulkan required changes to the shader cache that makes it incompatible with previous versions.\n\n" + - "Would you like to set Vulkan as the default graphics backend? " + - "You can change this at any time on the settings window.", - buttonTexts, - MessageType.Question); - - ConfigurationState.Instance.Graphics.GraphicsBackend.Value = response == 0 - ? GraphicsBackend.Vulkan - : GraphicsBackend.OpenGl; - - ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath); - } - Application.Run(); } From 0b58f462668694db1a035e8be40d2a6d366635e1 Mon Sep 17 00:00:00 2001 From: gdkchan Date: Sun, 19 Nov 2023 15:10:44 -0300 Subject: [PATCH 39/40] Extend bindless elimination to see through Phis with the same results (#5957) * Extend bindless elimination to see through Phis with the same results * Shader cache version bump --- .../Shader/DiskCache/DiskCacheHostStorage.cs | 2 +- .../Optimizations/BindlessElimination.cs | 58 ++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index 0dc4b1a72..403e039a4 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache private const ushort FileFormatVersionMajor = 1; private const ushort FileFormatVersionMinor = 2; private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor; - private const uint CodeGenVersion = 5791; + private const uint CodeGenVersion = 5957; private const string SharedTocFileName = "shared.toc"; private const string SharedDataFileName = "shared.data"; diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs index 19b7999a7..c955f5b5f 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs @@ -55,7 +55,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations continue; } - if (bindlessHandle.AsgOp is not Operation handleCombineOp) + if (!TryGetOperation(bindlessHandle.AsgOp, out Operation handleCombineOp)) { continue; } @@ -199,9 +199,64 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations } } + private static bool TryGetOperation(INode asgOp, out Operation outOperation) + { + if (asgOp is PhiNode phi) + { + // If we have a phi, let's check if all inputs are effectively the same value. + // If so, we can "see through" the phi and pick any of the inputs (since they are all the same). + + Operand firstSrc = phi.GetSource(0); + + for (int index = 1; index < phi.SourcesCount; index++) + { + if (!IsSameOperand(firstSrc, phi.GetSource(index))) + { + outOperation = null; + + return false; + } + } + + asgOp = firstSrc.AsgOp; + } + + if (asgOp is Operation operation) + { + outOperation = operation; + + return true; + } + + outOperation = null; + + return false; + } + + private static bool IsSameOperand(Operand x, Operand y) + { + if (x.Type == y.Type && x.Type == OperandType.LocalVariable) + { + return x.AsgOp is Operation xOp && + y.AsgOp is Operation yOp && + xOp.Inst == Instruction.BitwiseOr && + yOp.Inst == Instruction.BitwiseOr && + AreBothEqualConstantBuffers(xOp.GetSource(0), yOp.GetSource(0)) && + AreBothEqualConstantBuffers(xOp.GetSource(1), yOp.GetSource(1)); + } + + return false; + } + + private static bool AreBothEqualConstantBuffers(Operand x, Operand y) + { + return x.Type == y.Type && x.Value == y.Value && x.Type == OperandType.ConstantBuffer; + } + private static Operand GetSourceForMaskedHandle(Operation asgOp, uint mask) { // Assume it was already checked that the operation is bitwise AND. + Operand src0 = asgOp.GetSource(0); Operand src1 = asgOp.GetSource(1); @@ -210,6 +265,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations // We can't check if the mask matches here as both operands are from a constant buffer. // Be optimistic and assume it matches. Avoid constant buffer 1 as official drivers // uses this one to store compiler constants. + return src0.GetCbufSlot() == 1 ? src1 : src0; } else if (src0.Type == OperandType.ConstantBuffer && src1.Type == OperandType.Constant) From 70d65d3d8e77e66226ebab7f23d9b6e8c271319f Mon Sep 17 00:00:00 2001 From: gdkchan Date: Sun, 19 Nov 2023 15:27:34 -0300 Subject: [PATCH 40/40] Enable copy dependency between RGBA8 and RGBA32 formats (#5943) * Enable copy dependency between RGBA8 and RGBA32 formats * Take packed flag into account for texture formats * Account for widths not being a multiple of each other * Don't try to alias depth textures as color, fix log condition * PR feedback --- src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs | 30 +++++++++++++++++-- .../Image/TextureCompatibility.cs | 20 +++++++++++-- src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs | 2 +- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs index 1b517e63f..0af0725a2 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs @@ -651,9 +651,35 @@ namespace Ryujinx.Graphics.Gpu.Image /// True if the format is valid, false otherwise public static bool TryGetTextureFormat(uint encoded, bool isSrgb, out FormatInfo format) { - encoded |= (isSrgb ? 1u << 19 : 0u); + bool isPacked = (encoded & 0x80000000u) != 0; + if (isPacked) + { + encoded &= ~0x80000000u; + } - return _textureFormats.TryGetValue((TextureFormat)encoded, out format); + encoded |= isSrgb ? 1u << 19 : 0u; + + bool found = _textureFormats.TryGetValue((TextureFormat)encoded, out format); + + if (found && isPacked && !format.Format.IsDepthOrStencil()) + { + // If the packed flag is set, then the components of the pixel are tightly packed into the + // GPU registers on the shader. + // We can get the same behaviour by aliasing the texture as a format with the same amount of + // bytes per pixel, but only a single or the lowest possible number of components. + + format = format.BytesPerPixel switch + { + 1 => new FormatInfo(Format.R8Unorm, 1, 1, 1, 1), + 2 => new FormatInfo(Format.R16Unorm, 1, 1, 2, 1), + 4 => new FormatInfo(Format.R32Float, 1, 1, 4, 1), + 8 => new FormatInfo(Format.R32G32Float, 1, 1, 8, 2), + 16 => new FormatInfo(Format.R32G32B32A32Float, 1, 1, 16, 4), + _ => format, + }; + } + + return found; } /// diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs index 5af0471c0..eef38948d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -2,6 +2,8 @@ using Ryujinx.Common; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Texture; using System; +using System.Diagnostics; +using System.Numerics; namespace Ryujinx.Graphics.Gpu.Image { @@ -339,7 +341,20 @@ namespace Ryujinx.Graphics.Gpu.Image if (lhs.FormatInfo.BytesPerPixel != rhs.FormatInfo.BytesPerPixel && IsIncompatibleFormatAliasingAllowed(lhs.FormatInfo, rhs.FormatInfo)) { - alignedWidthMatches = lhsSize.Width * lhs.FormatInfo.BytesPerPixel == rhsSize.Width * rhs.FormatInfo.BytesPerPixel; + // If the formats are incompatible, but the texture strides match, + // we might allow them to be copy compatible depending on the format. + // The strides are aligned because the format with higher bytes per pixel + // might need a bit of padding at the end due to one width not being a multiple of the other. + + Debug.Assert((1 << BitOperations.Log2((uint)lhs.FormatInfo.BytesPerPixel)) == lhs.FormatInfo.BytesPerPixel); + Debug.Assert((1 << BitOperations.Log2((uint)rhs.FormatInfo.BytesPerPixel)) == rhs.FormatInfo.BytesPerPixel); + + int alignment = Math.Max(lhs.FormatInfo.BytesPerPixel, rhs.FormatInfo.BytesPerPixel); + + int lhsStride = BitUtils.AlignUp(lhsSize.Width * lhs.FormatInfo.BytesPerPixel, alignment); + int rhsStride = BitUtils.AlignUp(rhsSize.Width * rhs.FormatInfo.BytesPerPixel, alignment); + + alignedWidthMatches = lhsStride == rhsStride; } TextureViewCompatibility result = TextureViewCompatibility.Full; @@ -718,7 +733,8 @@ namespace Ryujinx.Graphics.Gpu.Image (lhsFormat, rhsFormat) = (rhsFormat, lhsFormat); } - return lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm; + return (lhsFormat.Format == Format.R8G8B8A8Unorm && rhsFormat.Format == Format.R32G32B32A32Float) || + (lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm); } /// diff --git a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs index 0fdb6cd64..a4035577d 100644 --- a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs +++ b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs @@ -430,7 +430,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (!FormatTable.TryGetTextureFormat(format, srgb, out FormatInfo formatInfo)) { - if (gpuVa != 0 && (int)format > 0) + if (gpuVa != 0 && format != 0) { Logger.Error?.Print(LogClass.Gpu, $"Invalid texture format 0x{format:X} (sRGB: {srgb})."); }