diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index b64fc1d92..d102beaa3 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -22,6 +22,7 @@ env: RYUJINX_BASE_VERSION: "1.2" RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "canary" RYUJINX_TARGET_RELEASE_CHANNEL_OWNER: "GreemDev" + RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO: "Ryujinx" RYUJINX_TARGET_RELEASE_CHANNEL_REPO: "Ryujinx-Canary" RELEASE: 1 @@ -93,6 +94,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash @@ -228,6 +230,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a78718be..183d4c618 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,6 +93,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash @@ -224,6 +225,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash diff --git a/Directory.Packages.props b/Directory.Packages.props index fff045062..c0ace079d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,6 +33,7 @@ + diff --git a/src/ARMeilleure/Translation/Dominance.cs b/src/ARMeilleure/Translation/Dominance.cs index e2185bd85..b62714fdf 100644 --- a/src/ARMeilleure/Translation/Dominance.cs +++ b/src/ARMeilleure/Translation/Dominance.cs @@ -77,7 +77,7 @@ namespace ARMeilleure.Translation { continue; } - + for (int pBlkIndex = 0; pBlkIndex < block.Predecessors.Count; pBlkIndex++) { BasicBlock current = block.Predecessors[pBlkIndex]; diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index 6dd38ed89..8236150fe 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.Linq; using System.Runtime; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -848,18 +849,15 @@ namespace ARMeilleure.Translation.PTC } } - List threads = new(); - for (int i = 0; i < degreeOfParallelism; i++) - { - Thread thread = new(TranslateFuncs) - { - IsBackground = true, - Name = "Ptc.TranslateThread." + i - }; - - threads.Add(thread); - } + List threads = Enumerable.Range(0, degreeOfParallelism) + .Select(idx => + new Thread(TranslateFuncs) + { + IsBackground = true, + Name = "Ptc.TranslateThread." + idx + } + ).ToList(); Stopwatch sw = Stopwatch.StartNew(); diff --git a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs index 69f7d876d..be0e1518c 100644 --- a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs +++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs @@ -3,6 +3,7 @@ namespace Ryujinx.Common.Configuration.Multiplayer public enum MultiplayerMode { Disabled, + LdnRyu, LdnMitm, } } diff --git a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs index 02c6dc97b..a9dbe646a 100644 --- a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs +++ b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs @@ -30,10 +30,10 @@ namespace Ryujinx.Common.Logging.Targets string ILogTarget.Name { get => _target.Name; } public AsyncLogTargetWrapper(ILogTarget target) - : this(target, -1, AsyncLogTargetOverflowAction.Block) + : this(target, -1) { } - public AsyncLogTargetWrapper(ILogTarget target, int queueLimit, AsyncLogTargetOverflowAction overflowAction) + public AsyncLogTargetWrapper(ILogTarget target, int queueLimit = -1, AsyncLogTargetOverflowAction overflowAction = AsyncLogTargetOverflowAction.Block) { _target = target; _messageQueue = new BlockingCollection(queueLimit); diff --git a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs index 8d4ede96c..631df3056 100644 --- a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs +++ b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs @@ -47,7 +47,7 @@ namespace Ryujinx.Common.Logging.Targets } // Clean up old logs, should only keep 3 - FileInfo[] files = logDir.GetFiles("*.log").OrderBy((info => info.CreationTime)).ToArray(); + FileInfo[] files = logDir.GetFiles("*.log").OrderBy(info => info.CreationTime).ToArray(); for (int i = 0; i < files.Length - 2; i++) { try diff --git a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs index 762c73889..fcb2229a7 100644 --- a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs +++ b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs @@ -803,18 +803,6 @@ namespace Ryujinx.Common.Memory public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); } - public struct Array256 : IArray where T : unmanaged - { - T _e0; - Array128 _other; - Array127 _other2; - public readonly int Length => 256; - public ref T this[int index] => ref AsSpan()[index]; - - [Pure] - public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); - } - public struct Array140 : IArray where T : unmanaged { T _e0; @@ -828,6 +816,18 @@ namespace Ryujinx.Common.Memory public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); } + public struct Array256 : IArray where T : unmanaged + { + T _e0; + Array128 _other; + Array127 _other2; + public readonly int Length => 256; + public ref T this[int index] => ref AsSpan()[index]; + + [Pure] + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); + } + public struct Array384 : IArray where T : unmanaged { T _e0; diff --git a/src/Ryujinx.Common/ReactiveObject.cs b/src/Ryujinx.Common/ReactiveObject.cs index 4f27af546..8df1e20fe 100644 --- a/src/Ryujinx.Common/ReactiveObject.cs +++ b/src/Ryujinx.Common/ReactiveObject.cs @@ -1,11 +1,13 @@ +using Ryujinx.Common.Logging; using System; +using System.Globalization; using System.Threading; namespace Ryujinx.Common { public class ReactiveObject { - private readonly ReaderWriterLockSlim _readerWriterLock = new(); + private readonly ReaderWriterLockSlim _rwLock = new(); private bool _isInitialized; private T _value; @@ -15,15 +17,15 @@ namespace Ryujinx.Common { get { - _readerWriterLock.EnterReadLock(); + _rwLock.EnterReadLock(); T value = _value; - _readerWriterLock.ExitReadLock(); + _rwLock.ExitReadLock(); return value; } set { - _readerWriterLock.EnterWriteLock(); + _rwLock.EnterWriteLock(); T oldValue = _value; @@ -32,7 +34,7 @@ namespace Ryujinx.Common _isInitialized = true; _value = value; - _readerWriterLock.ExitWriteLock(); + _rwLock.ExitWriteLock(); if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value)) { @@ -40,12 +42,22 @@ namespace Ryujinx.Common } } } + + public void LogChangesToValue(string valueName, LogClass logClass = LogClass.Configuration) + => Event += (_, e) => ReactiveObjectHelper.LogValueChange(logClass, e, valueName); public static implicit operator T(ReactiveObject obj) => obj.Value; } public static class ReactiveObjectHelper { + public static void LogValueChange(LogClass logClass, ReactiveEventArgs eventArgs, string valueName) + { + string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}"); + + Logger.Info?.Print(logClass, message); + } + public static void Toggle(this ReactiveObject rBoolean) => rBoolean.Value = !rBoolean.Value; } diff --git a/src/Ryujinx.Common/ReleaseInformation.cs b/src/Ryujinx.Common/ReleaseInformation.cs index 523479d82..f4c62155a 100644 --- a/src/Ryujinx.Common/ReleaseInformation.cs +++ b/src/Ryujinx.Common/ReleaseInformation.cs @@ -15,6 +15,7 @@ namespace Ryujinx.Common private const string ConfigFileName = "%%RYUJINX_CONFIG_FILE_NAME%%"; public const string ReleaseChannelOwner = "%%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER%%"; + public const string ReleaseChannelSourceRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO%%"; public const string ReleaseChannelRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_REPO%%"; public static string ConfigName => !ConfigFileName.StartsWith("%%") ? ConfigFileName : "Config.json"; @@ -23,6 +24,7 @@ namespace Ryujinx.Common !BuildGitHash.StartsWith("%%") && !ReleaseChannelName.StartsWith("%%") && !ReleaseChannelOwner.StartsWith("%%") && + !ReleaseChannelSourceRepo.StartsWith("%%") && !ReleaseChannelRepo.StartsWith("%%") && !ConfigFileName.StartsWith("%%"); diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs index 71e02184e..53d1e4f33 100644 --- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs +++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs @@ -1,6 +1,7 @@ using System.Buffers.Binary; using System.Net; using System.Net.NetworkInformation; +using System.Runtime.InteropServices; namespace Ryujinx.Common.Utilities { @@ -65,6 +66,11 @@ namespace Ryujinx.Common.Utilities return (targetProperties, targetAddressInfo); } + public static bool SupportsDynamicDns() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + public static uint ConvertIpv4Address(IPAddress ipAddress) { return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes()); diff --git a/src/Ryujinx.Graphics.GAL/IRenderer.cs b/src/Ryujinx.Graphics.GAL/IRenderer.cs index 9b5e2cc42..c2fdcbe4b 100644 --- a/src/Ryujinx.Graphics.GAL/IRenderer.cs +++ b/src/Ryujinx.Graphics.GAL/IRenderer.cs @@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.GAL IPipeline Pipeline { get; } IWindow Window { get; } - + uint ProgramCount { get; } void BackgroundContextAction(Action action, bool alwaysBackground = false); diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs index 2deee045c..6ead314fd 100644 --- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs +++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs @@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.OpenGL public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info) { ProgramCount++; - + return new Program(shaders, info.FragmentOutputMap); } diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 8d324957a..cc2bc36c2 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -549,7 +549,7 @@ namespace Ryujinx.Graphics.Vulkan public IProgram CreateProgram(ShaderSource[] sources, ShaderInfo info) { ProgramCount++; - + bool isCompute = sources.Length == 1 && sources[0].Stage == ShaderStage.Compute; if (info.State.HasValue || isCompute) diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index 955fee4b5..70fcf278d 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -164,6 +164,21 @@ namespace Ryujinx.HLE /// public MultiplayerMode MultiplayerMode { internal get; set; } + /// + /// Disable P2P mode + /// + public bool MultiplayerDisableP2p { internal get; set; } + + /// + /// Multiplayer Passphrase + /// + public string MultiplayerLdnPassphrase { internal get; set; } + + /// + /// LDN Server + /// + public string MultiplayerLdnServer { internal get; set; } + /// /// An action called when HLE force a refresh of output after docked mode changed. /// @@ -194,7 +209,10 @@ namespace Ryujinx.HLE float audioVolume, bool useHypervisor, string multiplayerLanInterfaceId, - MultiplayerMode multiplayerMode) + MultiplayerMode multiplayerMode, + bool multiplayerDisableP2p, + string multiplayerLdnPassphrase, + string multiplayerLdnServer) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -222,6 +240,9 @@ namespace Ryujinx.HLE UseHypervisor = useHypervisor; MultiplayerLanInterfaceId = multiplayerLanInterfaceId; MultiplayerMode = multiplayerMode; + MultiplayerDisableP2p = multiplayerDisableP2p; + MultiplayerLdnPassphrase = multiplayerLdnPassphrase; + MultiplayerLdnServer = multiplayerLdnServer; } } } diff --git a/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs b/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs index 171a083f3..2e7b8ee76 100644 --- a/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs +++ b/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs @@ -2463,7 +2463,7 @@ namespace Ryujinx.HLE.HOS.Diagnostics.Demangler return ParseIntegerLiteral("unsigned short"); case 'i': _position++; - return ParseIntegerLiteral(""); + return ParseIntegerLiteral(string.Empty); case 'j': _position++; return ParseIntegerLiteral("u"); diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index ee179c929..7cbe1afca 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -116,18 +116,13 @@ namespace Ryujinx.HLE.HOS private readonly Dictionary _appMods; // key is ApplicationId private PatchCache _patches; - private static readonly EnumerationOptions _dirEnumOptions; - - static ModLoader() + private static readonly EnumerationOptions _dirEnumOptions = new() { - _dirEnumOptions = new EnumerationOptions - { - MatchCasing = MatchCasing.CaseInsensitive, - MatchType = MatchType.Simple, - RecurseSubdirectories = false, - ReturnSpecialDirectories = false, - }; - } + MatchCasing = MatchCasing.CaseInsensitive, + MatchType = MatchType.Simple, + RecurseSubdirectories = false, + ReturnSpecialDirectories = false, + }; public ModLoader() { @@ -169,7 +164,7 @@ namespace Ryujinx.HLE.HOS foreach (var modDir in dir.EnumerateDirectories()) { types.Clear(); - Mod mod = new("", null, true); + Mod mod = new(string.Empty, null, true); if (StrEquals(RomfsDir, modDir.Name)) { diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs index 4da5fe42b..c6d6ac944 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x20)] + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 8)] struct NetworkConfig { public IntentId IntentId; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs index 449c923cc..f3ab1edd5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x60)] + [StructLayout(LayoutKind.Sequential, Size = 0x60, Pack = 8)] struct ScanFilter { public NetworkId NetworkId; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs index 5939a1394..f3968aab4 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x44)] + [StructLayout(LayoutKind.Sequential, Size = 0x44, Pack = 2)] struct SecurityConfig { public SecurityMode SecurityMode; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs index dbcaa9eeb..e564a2ec9 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x20)] + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 1)] struct SecurityParameter { public Array16 Data; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs index 3820f936e..7246f6f80 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x30)] + [StructLayout(LayoutKind.Sequential, Size = 0x30, Pack = 1)] struct UserConfig { public Array33 UserName; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs index 78ebcac82..bd00a3139 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs @@ -15,6 +15,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public Array8 LatestUpdates = new(); public bool Connected { get; private set; } + public ProxyConfig Config => _parent.NetworkClient.Config; + public AccessPoint(IUserLocalCommunicationService parent) { _parent = parent; @@ -24,9 +26,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public void Dispose() { - _parent.NetworkClient.DisconnectNetwork(); + if (_parent?.NetworkClient != null) + { + _parent.NetworkClient.DisconnectNetwork(); - _parent.NetworkClient.NetworkChange -= NetworkChanged; + _parent.NetworkClient.NetworkChange -= NetworkChanged; + } } private void NetworkChanged(object sender, NetworkChangeEventArgs e) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs index 7ad6de51d..028ab6cfc 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs @@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { interface INetworkClient : IDisposable { + ProxyConfig Config { get; } bool NeedsRealId { get; } event EventHandler NetworkChange; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 1d4b5485e..9f65aed4b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -9,6 +9,8 @@ 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.LdnMitm; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; @@ -21,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class IUserLocalCommunicationService : IpcService, IDisposable { + public static string DefaultLanPlayHost = "ryuldn.vudjun.com"; + public static short LanPlayPort = 30456; + public INetworkClient NetworkClient { get; private set; } private const int NifmRequestID = 90; @@ -175,19 +180,37 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected) { - (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId); - - if (unicastAddress == null) + ProxyConfig config = _state switch { - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress)); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask)); + NetworkState.AccessPointCreated => _accessPoint.Config, + NetworkState.StationConnected => _station.Config, + + _ => default + }; + + if (config.ProxyIp == 0) + { + (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId); + + if (unicastAddress == null) + { + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress)); + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask)); + } + else + { + Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); + } } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP."); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); + context.ResponseData.Write(config.ProxyIp); + context.ResponseData.Write(config.ProxySubnetMask); } } else @@ -1066,6 +1089,27 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator switch (mode) { + case MultiplayerMode.LdnRyu: + try + { + string ldnServer = context.Device.Configuration.MultiplayerLdnServer; + if (string.IsNullOrEmpty(ldnServer)) + { + ldnServer = DefaultLanPlayHost; + } + if (!IPAddress.TryParse(ldnServer, out IPAddress ipAddress)) + { + ipAddress = Dns.GetHostEntry(ldnServer).AddressList[0]; + } + NetworkClient = new LdnMasterProxyClient(ipAddress.ToString(), LanPlayPort, context.Device.Configuration); + } + catch (Exception ex) + { + Logger.Error?.Print(LogClass.ServiceLdn, "Could not locate LdnRyu server. Defaulting to stubbed wireless."); + Logger.Error?.Print(LogClass.ServiceLdn, ex.Message); + NetworkClient = new LdnDisabledClient(); + } + break; case MultiplayerMode.LdnMitm: NetworkClient = new LdnMitmClient(context.Device.Configuration); break; @@ -1103,7 +1147,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _accessPoint?.Dispose(); _accessPoint = null; - NetworkClient?.Dispose(); + NetworkClient?.DisconnectAndStop(); NetworkClient = null; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs index e3385a1ed..2e8bb8d83 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; @@ -6,12 +7,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class LdnDisabledClient : INetworkClient { + public ProxyConfig Config { get; } public bool NeedsRealId => true; public event EventHandler NetworkChange; public NetworkError Connect(ConnectRequest request) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return NetworkError.None; @@ -19,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public NetworkError ConnectPrivate(ConnectPrivateRequest request) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return NetworkError.None; @@ -26,6 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return true; @@ -33,6 +38,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return true; @@ -49,6 +55,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to scan for networks, but Multiplayer is disabled!"); return Array.Empty(); } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs index 273acdd5e..40697d122 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs @@ -12,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm /// internal class LdnMitmClient : INetworkClient { + public ProxyConfig Config { get; } public bool NeedsRealId => false; public event EventHandler NetworkChange; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs new file mode 100644 index 000000000..a7c435506 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs @@ -0,0 +1,7 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + interface IProxyClient + { + bool SendAsync(byte[] buffer); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs new file mode 100644 index 000000000..4c7814b8e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs @@ -0,0 +1,645 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using TcpClient = NetCoreServer.TcpClient; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class LdnMasterProxyClient : TcpClient, INetworkClient, IProxyClient + { + public bool NeedsRealId => true; + + private static InitializeMessage InitializeMemory = new InitializeMessage(); + + private const int InactiveTimeout = 6000; + private const int FailureTimeout = 4000; + private const int ScanTimeout = 1000; + + private bool _useP2pProxy; + private NetworkError _lastError; + + private readonly ManualResetEvent _connected = new ManualResetEvent(false); + private readonly ManualResetEvent _error = new ManualResetEvent(false); + private readonly ManualResetEvent _scan = new ManualResetEvent(false); + private readonly ManualResetEvent _reject = new ManualResetEvent(false); + private readonly AutoResetEvent _apConnected = new AutoResetEvent(false); + + private readonly RyuLdnProtocol _protocol; + private readonly NetworkTimeout _timeout; + + private readonly List _availableGames = new List(); + private DisconnectReason _disconnectReason; + + private P2pProxyServer _hostedProxy; + private P2pProxyClient _connectedProxy; + + private bool _networkConnected; + + private string _passphrase; + private byte[] _gameVersion = new byte[0x10]; + + private readonly HLEConfiguration _config; + + public event EventHandler NetworkChange; + + public ProxyConfig Config { get; private set; } + + public LdnMasterProxyClient(string address, int port, HLEConfiguration config) : base(address, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + _protocol = new RyuLdnProtocol(); + _timeout = new NetworkTimeout(InactiveTimeout, TimeoutConnection); + + _protocol.Initialize += HandleInitialize; + _protocol.Connected += HandleConnected; + _protocol.Reject += HandleReject; + _protocol.RejectReply += HandleRejectReply; + _protocol.SyncNetwork += HandleSyncNetwork; + _protocol.ProxyConfig += HandleProxyConfig; + _protocol.Disconnected += HandleDisconnected; + + _protocol.ScanReply += HandleScanReply; + _protocol.ScanReplyEnd += HandleScanReplyEnd; + _protocol.ExternalProxy += HandleExternalProxy; + + _protocol.Ping += HandlePing; + _protocol.NetworkError += HandleNetworkError; + + _config = config; + _useP2pProxy = !config.MultiplayerDisableP2p; + } + + private void TimeoutConnection() + { + _connected.Reset(); + + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + } + + private bool EnsureConnected() + { + if (IsConnected) + { + return true; + } + + _error.Reset(); + + ConnectAsync(); + + int index = WaitHandle.WaitAny(new WaitHandle[] { _connected, _error }, FailureTimeout); + + if (IsConnected) + { + SendAsync(_protocol.Encode(PacketId.Initialize, InitializeMemory)); + } + + return index == 0 && IsConnected; + } + + private void UpdatePassphraseIfNeeded() + { + string passphrase = _config.MultiplayerLdnPassphrase ?? ""; + if (passphrase != _passphrase) + { + _passphrase = passphrase; + + SendAsync(_protocol.Encode(PacketId.Passphrase, StringUtils.GetFixedLengthBytes(passphrase, 0x80, Encoding.UTF8))); + } + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}"); + + UpdatePassphraseIfNeeded(); + + _connected.Set(); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}"); + + _passphrase = null; + + _connected.Reset(); + + if (_networkConnected) + { + DisconnectInternal(); + } + } + + public void DisconnectAndStop() + { + _timeout.Dispose(); + + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + + Dispose(); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(buffer, (int)offset, (int)size); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}"); + + _error.Set(); + } + + + + private void HandleInitialize(LdnHeader header, InitializeMessage initialize) + { + InitializeMemory = initialize; + } + + private void HandleExternalProxy(LdnHeader header, ExternalProxyConfig config) + { + int length = config.AddressFamily switch + { + AddressFamily.InterNetwork => 4, + AddressFamily.InterNetworkV6 => 16, + _ => 0 + }; + + if (length == 0) + { + return; // Invalid external proxy. + } + + IPAddress address = new(config.ProxyIp.AsSpan()[..length].ToArray()); + P2pProxyClient proxy = new(address.ToString(), config.ProxyPort); + + _connectedProxy = proxy; + + bool success = proxy.PerformAuth(config); + + if (!success) + { + DisconnectInternal(); + } + } + + private void HandlePing(LdnHeader header, PingMessage ping) + { + if (ping.Requester == 0) // Server requested. + { + // Send the ping message back. + + SendAsync(_protocol.Encode(PacketId.Ping, ping)); + } + } + + private void HandleNetworkError(LdnHeader header, NetworkErrorMessage error) + { + if (error.Error == NetworkError.PortUnreachable) + { + _useP2pProxy = false; + } + else + { + _lastError = error.Error; + } + } + + private NetworkError ConsumeNetworkError() + { + NetworkError result = _lastError; + + _lastError = NetworkError.None; + + return result; + } + + private void HandleSyncNetwork(LdnHeader header, NetworkInfo info) + { + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true)); + } + + private void HandleConnected(LdnHeader header, NetworkInfo info) + { + _networkConnected = true; + _disconnectReason = DisconnectReason.None; + + _apConnected.Set(); + + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true)); + } + + private void HandleDisconnected(LdnHeader header, DisconnectMessage message) + { + DisconnectInternal(); + } + + private void HandleReject(LdnHeader header, RejectRequest reject) + { + // When the client receives a Reject request, we have been rejected and will be disconnected shortly. + _disconnectReason = reject.DisconnectReason; + } + + private void HandleRejectReply(LdnHeader header) + { + _reject.Set(); + } + + private void HandleScanReply(LdnHeader header, NetworkInfo info) + { + _availableGames.Add(info); + } + + private void HandleScanReplyEnd(LdnHeader obj) + { + _scan.Set(); + } + + private void DisconnectInternal() + { + if (_networkConnected) + { + _networkConnected = false; + + _hostedProxy?.Dispose(); + _hostedProxy = null; + + _connectedProxy?.Dispose(); + _connectedProxy = null; + + _apConnected.Reset(); + + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false, _disconnectReason)); + + if (IsConnected) + { + _timeout.RefreshTimeout(); + } + } + } + + public void DisconnectNetwork() + { + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.Disconnect, new DisconnectMessage())); + + DisconnectInternal(); + } + } + + public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId) + { + if (_networkConnected) + { + _reject.Reset(); + + SendAsync(_protocol.Encode(PacketId.Reject, new RejectRequest(disconnectReason, nodeId))); + + int index = WaitHandle.WaitAny(new WaitHandle[] { _reject, _error }, InactiveTimeout); + + if (index == 0) + { + return (ConsumeNetworkError() != NetworkError.None) ? ResultCode.InvalidState : ResultCode.Success; + } + } + + return ResultCode.InvalidState; + } + + public void SetAdvertiseData(byte[] data) + { + // TODO: validate we're the owner (the server will do this anyways tho) + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.SetAdvertiseData, data)); + } + } + + public void SetGameVersion(byte[] versionString) + { + _gameVersion = versionString; + + if (_gameVersion.Length < 0x10) + { + Array.Resize(ref _gameVersion, 0x10); + } + } + + public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy) + { + // TODO: validate we're the owner (the server will do this anyways tho) + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.SetAcceptPolicy, new SetAcceptPolicyRequest + { + StationAcceptPolicy = acceptPolicy + })); + } + } + + private void DisposeProxy() + { + _hostedProxy?.Dispose(); + _hostedProxy = null; + } + + private void ConfigureAccessPoint(ref RyuNetworkConfig request) + { + _gameVersion.AsSpan().CopyTo(request.GameVersion.AsSpan()); + + if (_useP2pProxy) + { + // Before sending the request, attempt to set up a proxy server. + // This can be on a range of private ports, which can be exposed on a range of public + // ports via UPnP. If any of this fails, we just fall back to using the master server. + + int i = 0; + for (; i < P2pProxyServer.PrivatePortRange; i++) + { + _hostedProxy = new P2pProxyServer(this, (ushort)(P2pProxyServer.PrivatePortBase + i), _protocol); + + try + { + _hostedProxy.Start(); + + break; + } + catch (SocketException e) + { + _hostedProxy.Dispose(); + _hostedProxy = null; + + if (e.SocketErrorCode != SocketError.AddressAlreadyInUse) + { + i = P2pProxyServer.PrivatePortRange; // Immediately fail. + } + } + } + + bool openSuccess = i < P2pProxyServer.PrivatePortRange; + + if (openSuccess) + { + Task natPunchResult = _hostedProxy.NatPunch(); + + try + { + if (natPunchResult.Result != 0) + { + // Tell the server that we are hosting the proxy. + request.ExternalProxyPort = natPunchResult.Result; + } + } + catch (Exception) { } + + if (request.ExternalProxyPort == 0) + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Failed to open a port with UPnP for P2P connection. Proxying through the master server instead. Expect higher latency."); + _hostedProxy.Dispose(); + } + else + { + Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}."); + _hostedProxy.Start(); + + (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(); + + unicastAddress.Address.GetAddressBytes().AsSpan().CopyTo(request.PrivateIp.AsSpan()); + request.InternalProxyPort = _hostedProxy.PrivatePort; + request.AddressFamily = unicastAddress.Address.AddressFamily; + } + } + else + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Cannot create a P2P server. Proxying through the master server instead. Expect higher latency."); + } + } + } + + private bool CreateNetworkCommon() + { + bool signalled = _apConnected.WaitOne(FailureTimeout); + + if (!_useP2pProxy && _hostedProxy != null) + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Locally hosted proxy server was not externally reachable. Proxying through the master server instead. Expect higher latency."); + + DisposeProxy(); + } + + if (signalled && _connectedProxy != null) + { + _connectedProxy.EnsureProxyReady(); + + Config = _connectedProxy.ProxyConfig; + } + else + { + DisposeProxy(); + } + + return signalled; + } + + public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) + { + _timeout.DisableTimeout(); + + ConfigureAccessPoint(ref request.RyuNetworkConfig); + + if (!EnsureConnected()) + { + DisposeProxy(); + + return false; + } + + UpdatePassphraseIfNeeded(); + + SendAsync(_protocol.Encode(PacketId.CreateAccessPoint, request, advertiseData)); + + // Send a network change event with dummy data immediately. Necessary to avoid crashes in some games + var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo() + { + Common = new CommonNetworkInfo() + { + MacAddress = InitializeMemory.MacAddress, + Channel = request.NetworkConfig.Channel, + LinkLevel = 3, + NetworkType = 2, + Ssid = new Ssid() + { + Length = 32 + } + }, + Ldn = new LdnNetworkInfo() + { + AdvertiseDataSize = (ushort)advertiseData.Length, + AuthenticationId = 0, + NodeCount = 1, + NodeCountMax = request.NetworkConfig.NodeCountMax, + SecurityMode = (ushort)request.SecurityConfig.SecurityMode + } + }, true); + networkChangeEvent.Info.Ldn.Nodes[0] = new NodeInfo() + { + Ipv4Address = 175243265, + IsConnected = 1, + LocalCommunicationVersion = request.NetworkConfig.LocalCommunicationVersion, + MacAddress = InitializeMemory.MacAddress, + NodeId = 0, + UserName = request.UserConfig.UserName + }; + "12345678123456781234567812345678"u8.ToArray().CopyTo(networkChangeEvent.Info.Common.Ssid.Name.AsSpan()); + NetworkChange?.Invoke(this, networkChangeEvent); + + return CreateNetworkCommon(); + } + + public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) + { + _timeout.DisableTimeout(); + + ConfigureAccessPoint(ref request.RyuNetworkConfig); + + if (!EnsureConnected()) + { + DisposeProxy(); + + return false; + } + + UpdatePassphraseIfNeeded(); + + SendAsync(_protocol.Encode(PacketId.CreateAccessPointPrivate, request, advertiseData)); + + return CreateNetworkCommon(); + } + + public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) + { + if (!_networkConnected) + { + _timeout.RefreshTimeout(); + } + + _availableGames.Clear(); + + int index = -1; + + if (EnsureConnected()) + { + UpdatePassphraseIfNeeded(); + + _scan.Reset(); + + SendAsync(_protocol.Encode(PacketId.Scan, scanFilter)); + + index = WaitHandle.WaitAny(new WaitHandle[] { _scan, _error }, ScanTimeout); + } + + if (index != 0) + { + // An error occurred or timeout. Write 0 games. + return Array.Empty(); + } + + return _availableGames.ToArray(); + } + + private NetworkError ConnectCommon() + { + bool signalled = _apConnected.WaitOne(FailureTimeout); + + NetworkError error = ConsumeNetworkError(); + + if (error != NetworkError.None) + { + return error; + } + + if (signalled && _connectedProxy != null) + { + _connectedProxy.EnsureProxyReady(); + + Config = _connectedProxy.ProxyConfig; + } + + return signalled ? NetworkError.None : NetworkError.ConnectTimeout; + } + + public NetworkError Connect(ConnectRequest request) + { + _timeout.DisableTimeout(); + + if (!EnsureConnected()) + { + return NetworkError.Unknown; + } + + SendAsync(_protocol.Encode(PacketId.Connect, request)); + + var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo() + { + Common = request.NetworkInfo.Common, + Ldn = request.NetworkInfo.Ldn + }, true); + + NetworkChange?.Invoke(this, networkChangeEvent); + + return ConnectCommon(); + } + + public NetworkError ConnectPrivate(ConnectPrivateRequest request) + { + _timeout.DisableTimeout(); + + if (!EnsureConnected()) + { + return NetworkError.Unknown; + } + + SendAsync(_protocol.Encode(PacketId.ConnectPrivate, request)); + + return ConnectCommon(); + } + + private void HandleProxyConfig(LdnHeader header, ProxyConfig config) + { + Config = config; + + SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol)); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs new file mode 100644 index 000000000..5012d5d81 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class NetworkTimeout : IDisposable + { + private readonly int _idleTimeout; + private readonly Action _timeoutCallback; + private CancellationTokenSource _cancel; + + private readonly object _lock = new object(); + + public NetworkTimeout(int idleTimeout, Action timeoutCallback) + { + _idleTimeout = idleTimeout; + _timeoutCallback = timeoutCallback; + } + + private async Task TimeoutTask() + { + CancellationTokenSource cts; + + lock (_lock) + { + cts = _cancel; + } + + if (cts == null) + { + return; + } + + try + { + await Task.Delay(_idleTimeout, cts.Token); + } + catch (TaskCanceledException) + { + return; // Timeout cancelled. + } + + lock (_lock) + { + // Run the timeout callback. If the cancel token source has been replaced, we have _just_ been cancelled. + if (cts == _cancel) + { + _timeoutCallback(); + } + } + } + + public bool RefreshTimeout() + { + lock (_lock) + { + _cancel?.Cancel(); + + _cancel = new CancellationTokenSource(); + + Task.Run(TimeoutTask); + } + + return true; + } + + public void DisableTimeout() + { + lock (_lock) + { + _cancel?.Cancel(); + + _cancel = new CancellationTokenSource(); + } + } + + public void Dispose() + { + DisableTimeout(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs new file mode 100644 index 000000000..bc3a5edf2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + public class EphemeralPortPool + { + private const ushort EphemeralBase = 49152; + + private readonly List _ephemeralPorts = new List(); + + private readonly object _lock = new object(); + + public ushort Get() + { + ushort port = EphemeralBase; + lock (_lock) + { + // Starting at the ephemeral port base, return an ephemeral port that is not in use. + // Returns 0 if the range is exhausted. + + for (int i = 0; i < _ephemeralPorts.Count; i++) + { + ushort existingPort = _ephemeralPorts[i]; + + if (existingPort > port) + { + // The port was free - take it. + _ephemeralPorts.Insert(i, port); + + return port; + } + + port++; + } + + if (port != 0) + { + _ephemeralPorts.Add(port); + } + + return port; + } + } + + public void Return(ushort port) + { + lock (_lock) + { + _ephemeralPorts.Remove(port); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs new file mode 100644 index 000000000..bb390d49a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs @@ -0,0 +1,254 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class LdnProxy : IDisposable + { + public EndPoint LocalEndpoint { get; } + public IPAddress LocalAddress { get; } + + private readonly List _sockets = new List(); + private readonly Dictionary _ephemeralPorts = new Dictionary(); + + private readonly IProxyClient _parent; + private RyuLdnProtocol _protocol; + private readonly uint _subnetMask; + private readonly uint _localIp; + private readonly uint _broadcast; + + public LdnProxy(ProxyConfig config, IProxyClient client, RyuLdnProtocol protocol) + { + _parent = client; + _protocol = protocol; + + _ephemeralPorts[ProtocolType.Udp] = new EphemeralPortPool(); + _ephemeralPorts[ProtocolType.Tcp] = new EphemeralPortPool(); + + byte[] address = BitConverter.GetBytes(config.ProxyIp); + Array.Reverse(address); + LocalAddress = new IPAddress(address); + + _subnetMask = config.ProxySubnetMask; + _localIp = config.ProxyIp; + _broadcast = _localIp | (~_subnetMask); + + RegisterHandlers(protocol); + } + + public bool Supported(AddressFamily domain, SocketType type, ProtocolType protocol) + { + if (protocol == ProtocolType.Tcp) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Tcp proxy networking is untested. Please report this game so that it can be tested."); + } + return domain == AddressFamily.InterNetwork && (protocol == ProtocolType.Tcp || protocol == ProtocolType.Udp); + } + + private void RegisterHandlers(RyuLdnProtocol protocol) + { + protocol.ProxyConnect += HandleConnectionRequest; + protocol.ProxyConnectReply += HandleConnectionResponse; + protocol.ProxyData += HandleData; + protocol.ProxyDisconnect += HandleDisconnect; + + _protocol = protocol; + } + + public void UnregisterHandlers(RyuLdnProtocol protocol) + { + protocol.ProxyConnect -= HandleConnectionRequest; + protocol.ProxyConnectReply -= HandleConnectionResponse; + protocol.ProxyData -= HandleData; + protocol.ProxyDisconnect -= HandleDisconnect; + } + + public ushort GetEphemeralPort(ProtocolType type) + { + return _ephemeralPorts[type].Get(); + } + + public void ReturnEphemeralPort(ProtocolType type, ushort port) + { + _ephemeralPorts[type].Return(port); + } + + public void RegisterSocket(LdnProxySocket socket) + { + lock (_sockets) + { + _sockets.Add(socket); + } + } + + public void UnregisterSocket(LdnProxySocket socket) + { + lock (_sockets) + { + _sockets.Remove(socket); + } + } + + private void ForRoutedSockets(ProxyInfo info, Action action) + { + lock (_sockets) + { + foreach (LdnProxySocket socket in _sockets) + { + // Must match protocol and destination port. + if (socket.ProtocolType != info.Protocol || socket.LocalEndPoint is not IPEndPoint endpoint || endpoint.Port != info.DestPort) + { + continue; + } + + // We can assume packets routed to us have been sent to our destination. + // They will either be sent to us, or broadcast packets. + + action(socket); + } + } + } + + public void HandleConnectionRequest(LdnHeader header, ProxyConnectRequest request) + { + ForRoutedSockets(request.Info, (socket) => + { + socket.HandleConnectRequest(request); + }); + } + + public void HandleConnectionResponse(LdnHeader header, ProxyConnectResponse response) + { + ForRoutedSockets(response.Info, (socket) => + { + socket.HandleConnectResponse(response); + }); + } + + public void HandleData(LdnHeader header, ProxyDataHeader proxyHeader, byte[] data) + { + ProxyDataPacket packet = new ProxyDataPacket() { Header = proxyHeader, Data = data }; + + ForRoutedSockets(proxyHeader.Info, (socket) => + { + socket.IncomingData(packet); + }); + } + + public void HandleDisconnect(LdnHeader header, ProxyDisconnectMessage disconnect) + { + ForRoutedSockets(disconnect.Info, (socket) => + { + socket.HandleDisconnect(disconnect); + }); + } + + private uint GetIpV4(IPEndPoint endpoint) + { + if (endpoint.AddressFamily != AddressFamily.InterNetwork) + { + throw new NotSupportedException(); + } + + byte[] address = endpoint.Address.GetAddressBytes(); + Array.Reverse(address); + + return BitConverter.ToUInt32(address); + } + + private ProxyInfo MakeInfo(IPEndPoint localEp, IPEndPoint remoteEP, ProtocolType type) + { + return new ProxyInfo + { + SourceIpV4 = GetIpV4(localEp), + SourcePort = (ushort)localEp.Port, + + DestIpV4 = GetIpV4(remoteEP), + DestPort = (ushort)remoteEP.Port, + + Protocol = type + }; + } + + public void RequestConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must ask the other side to initialize a connection, so they can accept a socket for us. + + ProxyConnectRequest request = new ProxyConnectRequest + { + Info = MakeInfo(localEp, remoteEp, type) + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyConnect, request)); + } + + public void SignalConnected(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must tell the other side that we have accepted their request for connection. + + ProxyConnectResponse request = new ProxyConnectResponse + { + Info = MakeInfo(localEp, remoteEp, type) + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyConnectReply, request)); + } + + public void EndConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must tell the other side that our connection is dropped. + + ProxyDisconnectMessage request = new ProxyDisconnectMessage + { + Info = MakeInfo(localEp, remoteEp, type), + DisconnectReason = 0 // TODO + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyDisconnect, request)); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We send exactly as much as the user wants us to, currently instantly. + // TODO: handle over "virtual mtu" (we have a max packet size to worry about anyways). fragment if tcp? throw if udp? + + ProxyDataHeader request = new ProxyDataHeader + { + Info = MakeInfo(localEp, remoteEp, type), + DataLength = (uint)buffer.Length + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyData, request, buffer.ToArray())); + + return buffer.Length; + } + + public bool IsBroadcast(uint ip) + { + return ip == _broadcast; + } + + public bool IsMyself(uint ip) + { + return ip == _localIp; + } + + public void Dispose() + { + UnregisterHandlers(_protocol); + + lock (_sockets) + { + foreach (LdnProxySocket socket in _sockets) + { + socket.ProxyDestroyed(); + } + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs new file mode 100644 index 000000000..ed7a9c751 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs @@ -0,0 +1,797 @@ +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + /// + /// This socket is forwarded through a TCP stream that goes through the Ldn server. + /// The Ldn server will then route the packets we send (or need to receive) within the virtual adhoc network. + /// + class LdnProxySocket : ISocketImpl + { + private readonly LdnProxy _proxy; + + private bool _isListening; + private readonly List _listenSockets = new List(); + + private readonly Queue _connectRequests = new Queue(); + + private readonly AutoResetEvent _acceptEvent = new AutoResetEvent(false); + private readonly int _acceptTimeout = -1; + + private readonly Queue _errors = new Queue(); + + private readonly AutoResetEvent _connectEvent = new AutoResetEvent(false); + private ProxyConnectResponse _connectResponse; + + private int _receiveTimeout = -1; + private readonly AutoResetEvent _receiveEvent = new AutoResetEvent(false); + private readonly Queue _receiveQueue = new Queue(); + + // private int _sendTimeout = -1; // Sends are techically instant right now, so not _really_ used. + + private bool _connecting; + private bool _broadcast; + private bool _readShutdown; + // private bool _writeShutdown; + private bool _closed; + + private readonly Dictionary _socketOptions = new Dictionary() + { + { SocketOptionName.Broadcast, 0 }, //TODO: honor this value + { SocketOptionName.DontLinger, 0 }, + { SocketOptionName.Debug, 0 }, + { SocketOptionName.Error, 0 }, + { SocketOptionName.KeepAlive, 0 }, + { SocketOptionName.OutOfBandInline, 0 }, + { SocketOptionName.ReceiveBuffer, 131072 }, + { SocketOptionName.ReceiveTimeout, -1 }, + { SocketOptionName.SendBuffer, 131072 }, + { SocketOptionName.SendTimeout, -1 }, + { SocketOptionName.Type, 0 }, + { SocketOptionName.ReuseAddress, 0 } //TODO: honor this value + }; + + public EndPoint RemoteEndPoint { get; private set; } + + public EndPoint LocalEndPoint { get; private set; } + + public bool Connected { get; private set; } + + public bool IsBound { get; private set; } + + public AddressFamily AddressFamily { get; } + + public SocketType SocketType { get; } + + public ProtocolType ProtocolType { get; } + + public bool Blocking { get; set; } + + public int Available + { + get + { + int result = 0; + + lock (_receiveQueue) + { + foreach (ProxyDataPacket data in _receiveQueue) + { + result += data.Data.Length; + } + } + + return result; + } + } + + public bool Readable + { + get + { + if (_isListening) + { + lock (_connectRequests) + { + return _connectRequests.Count > 0; + } + } + else + { + if (_readShutdown) + { + return true; + } + + lock (_receiveQueue) + { + return _receiveQueue.Count > 0; + } + } + + } + } + public bool Writable => Connected || ProtocolType == ProtocolType.Udp; + public bool Error => false; + + public LdnProxySocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, LdnProxy proxy) + { + AddressFamily = addressFamily; + SocketType = socketType; + ProtocolType = protocolType; + + _proxy = proxy; + _socketOptions[SocketOptionName.Type] = (int)socketType; + + proxy.RegisterSocket(this); + } + + private IPEndPoint EnsureLocalEndpoint(bool replace) + { + if (LocalEndPoint != null) + { + if (replace) + { + _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port); + } + else + { + return (IPEndPoint)LocalEndPoint; + } + } + + IPEndPoint localEp = new IPEndPoint(_proxy.LocalAddress, _proxy.GetEphemeralPort(ProtocolType)); + LocalEndPoint = localEp; + + return localEp; + } + + public LdnProxySocket AsAccepted(IPEndPoint remoteEp) + { + Connected = true; + RemoteEndPoint = remoteEp; + + IPEndPoint localEp = EnsureLocalEndpoint(true); + + _proxy.SignalConnected(localEp, remoteEp, ProtocolType); + + return this; + } + + private void SignalError(WsaError error) + { + lock (_errors) + { + _errors.Enqueue((int)error); + } + } + + private IPEndPoint GetEndpoint(uint ipv4, ushort port) + { + byte[] address = BitConverter.GetBytes(ipv4); + Array.Reverse(address); + + return new IPEndPoint(new IPAddress(address), port); + } + + public void IncomingData(ProxyDataPacket packet) + { + bool isBroadcast = _proxy.IsBroadcast(packet.Header.Info.DestIpV4); + + if (!_closed && (_broadcast || !isBroadcast)) + { + lock (_receiveQueue) + { + _receiveQueue.Enqueue(packet); + } + } + } + + public ISocketImpl Accept() + { + if (!_isListening) + { + throw new InvalidOperationException(); + } + + // Accept a pending request to this socket. + + lock (_connectRequests) + { + if (!Blocking && _connectRequests.Count == 0) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + while (true) + { + _acceptEvent.WaitOne(_acceptTimeout); + + lock (_connectRequests) + { + while (_connectRequests.Count > 0) + { + ProxyConnectRequest request = _connectRequests.Dequeue(); + + if (_connectRequests.Count > 0) + { + _acceptEvent.Set(); // Still more accepts to do. + } + + // Is this request made for us? + IPEndPoint endpoint = GetEndpoint(request.Info.DestIpV4, request.Info.DestPort); + + if (Equals(endpoint, LocalEndPoint)) + { + // Yes - let's accept. + IPEndPoint remoteEndpoint = GetEndpoint(request.Info.SourceIpV4, request.Info.SourcePort); + + LdnProxySocket socket = new LdnProxySocket(AddressFamily, SocketType, ProtocolType, _proxy).AsAccepted(remoteEndpoint); + + lock (_listenSockets) + { + _listenSockets.Add(socket); + } + + return socket; + } + } + } + } + } + + public void Bind(EndPoint localEP) + { + ArgumentNullException.ThrowIfNull(localEP); + + if (LocalEndPoint != null) + { + _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port); + } + var asIPEndpoint = (IPEndPoint)localEP; + if (asIPEndpoint.Port == 0) + { + asIPEndpoint.Port = (ushort)_proxy.GetEphemeralPort(ProtocolType); + } + + LocalEndPoint = (IPEndPoint)localEP; + + IsBound = true; + } + + public void Close() + { + _closed = true; + + _proxy.UnregisterSocket(this); + + if (Connected) + { + Disconnect(false); + } + + lock (_listenSockets) + { + foreach (LdnProxySocket socket in _listenSockets) + { + socket.Close(); + } + } + + _isListening = false; + } + + public void Connect(EndPoint remoteEP) + { + if (_isListening || !IsBound) + { + throw new InvalidOperationException(); + } + + if (remoteEP is not IPEndPoint) + { + throw new NotSupportedException(); + } + + IPEndPoint localEp = EnsureLocalEndpoint(true); + + _connecting = true; + + _proxy.RequestConnection(localEp, (IPEndPoint)remoteEP, ProtocolType); + + if (!Blocking && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + + _connectEvent.WaitOne(); //timeout? + + if (_connectResponse.Info.SourceIpV4 == 0) + { + throw new SocketException((int)WsaError.WSAECONNREFUSED); + } + + _connectResponse = default; + } + + public void HandleConnectResponse(ProxyConnectResponse obj) + { + if (!_connecting) + { + return; + } + + _connecting = false; + + if (_connectResponse.Info.SourceIpV4 != 0) + { + IPEndPoint remoteEp = GetEndpoint(obj.Info.SourceIpV4, obj.Info.SourcePort); + RemoteEndPoint = remoteEp; + + Connected = true; + } + else + { + // Connection failed + + SignalError(WsaError.WSAECONNREFUSED); + } + } + + public void Disconnect(bool reuseSocket) + { + if (Connected) + { + ConnectionEnded(); + + // The other side needs to be notified that connection ended. + _proxy.EndConnection(LocalEndPoint as IPEndPoint, RemoteEndPoint as IPEndPoint, ProtocolType); + } + } + + private void ConnectionEnded() + { + if (Connected) + { + RemoteEndPoint = null; + Connected = false; + } + } + + public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue) + { + if (optionLevel != SocketOptionLevel.Socket) + { + throw new NotImplementedException(); + } + + if (_socketOptions.TryGetValue(optionName, out int result)) + { + byte[] data = BitConverter.GetBytes(result); + Array.Copy(data, 0, optionValue, 0, Math.Min(data.Length, optionValue.Length)); + } + else + { + throw new NotImplementedException(); + } + } + + public void Listen(int backlog) + { + if (!IsBound) + { + throw new SocketException(); + } + + _isListening = true; + } + + public void HandleConnectRequest(ProxyConnectRequest obj) + { + lock (_connectRequests) + { + _connectRequests.Enqueue(obj); + } + + _connectEvent.Set(); + } + + public void HandleDisconnect(ProxyDisconnectMessage message) + { + Disconnect(false); + } + + public int Receive(Span buffer) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, SocketFlags.None, ref dummy); + } + + public int Receive(Span buffer, SocketFlags flags) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, flags, ref dummy); + } + + public int Receive(Span buffer, SocketFlags flags, out SocketError socketError) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, flags, out socketError, ref dummy); + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEp) + { + // We just receive all packets meant for us anyways regardless of EP in the actual implementation. + // The point is mostly to return the endpoint that we got the data from. + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, ref remoteEp); + } + else if (_readShutdown) + { + return 0; + } + else if (!Blocking) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + int timeout = _receiveTimeout; + + _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout); + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, ref remoteEp); + } + else if (_readShutdown) + { + return 0; + } + else + { + throw new SocketException((int)WsaError.WSAETIMEDOUT); + } + } + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp) + { + // We just receive all packets meant for us anyways regardless of EP in the actual implementation. + // The point is mostly to return the endpoint that we got the data from. + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + socketError = SocketError.ConnectionReset; + return -1; + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp); + } + else if (_readShutdown) + { + socketError = SocketError.Success; + return 0; + } + else if (!Blocking) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + int timeout = _receiveTimeout; + + _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout); + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp); + } + else if (_readShutdown) + { + socketError = SocketError.Success; + return 0; + } + else + { + socketError = SocketError.TimedOut; + return -1; + } + } + } + + private int ReceiveFromQueue(Span buffer, SocketFlags flags, ref EndPoint remoteEp) + { + int size = buffer.Length; + + // Assumes we have the receive queue lock, and at least one item in the queue. + ProxyDataPacket packet = _receiveQueue.Peek(); + + remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort); + + bool peek = (flags & SocketFlags.Peek) != 0; + + int read; + + if (packet.Data.Length > size) + { + read = size; + + // Cannot fit in the output buffer. Copy up to what we've got. + packet.Data.AsSpan(0, size).CopyTo(buffer); + + if (ProtocolType == ProtocolType.Udp) + { + // Udp overflows, loses the data, then throws an exception. + + if (!peek) + { + _receiveQueue.Dequeue(); + } + + throw new SocketException((int)WsaError.WSAEMSGSIZE); + } + else if (ProtocolType == ProtocolType.Tcp) + { + // Split the data at the buffer boundary. It will stay on the recieve queue. + + byte[] newData = new byte[packet.Data.Length - size]; + Array.Copy(packet.Data, size, newData, 0, newData.Length); + + packet.Data = newData; + } + } + else + { + read = packet.Data.Length; + + packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer); + + if (!peek) + { + _receiveQueue.Dequeue(); + } + } + + return read; + } + + private int ReceiveFromQueue(Span buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp) + { + int size = buffer.Length; + + // Assumes we have the receive queue lock, and at least one item in the queue. + ProxyDataPacket packet = _receiveQueue.Peek(); + + remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort); + + bool peek = (flags & SocketFlags.Peek) != 0; + + int read; + + if (packet.Data.Length > size) + { + read = size; + + // Cannot fit in the output buffer. Copy up to what we've got. + packet.Data.AsSpan(0, size).CopyTo(buffer); + + if (ProtocolType == ProtocolType.Udp) + { + // Udp overflows, loses the data, then throws an exception. + + if (!peek) + { + _receiveQueue.Dequeue(); + } + + socketError = SocketError.MessageSize; + return -1; + } + else if (ProtocolType == ProtocolType.Tcp) + { + // Split the data at the buffer boundary. It will stay on the recieve queue. + + byte[] newData = new byte[packet.Data.Length - size]; + Array.Copy(packet.Data, size, newData, 0, newData.Length); + + packet.Data = newData; + } + } + else + { + read = packet.Data.Length; + + packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer); + + if (!peek) + { + _receiveQueue.Dequeue(); + } + } + + socketError = SocketError.Success; + + return read; + } + + public int Send(ReadOnlySpan buffer) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, SocketFlags.None, RemoteEndPoint); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, flags, RemoteEndPoint); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, flags, out socketError, RemoteEndPoint); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP) + { + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + IPEndPoint localEp = EnsureLocalEndpoint(false); + + if (remoteEP is not IPEndPoint) + { + throw new NotSupportedException(); + } + + return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError, EndPoint remoteEP) + { + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + socketError = SocketError.ConnectionReset; + return -1; + } + + IPEndPoint localEp = EnsureLocalEndpoint(false); + + if (remoteEP is not IPEndPoint) + { + // throw new NotSupportedException(); + socketError = SocketError.OperationNotSupported; + return -1; + } + + socketError = SocketError.Success; + + return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType); + } + + public bool Poll(int microSeconds, SelectMode mode) + { + return mode switch + { + SelectMode.SelectRead => Readable, + SelectMode.SelectWrite => Writable, + SelectMode.SelectError => Error, + _ => false + }; + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue) + { + if (optionLevel != SocketOptionLevel.Socket) + { + throw new NotImplementedException(); + } + + switch (optionName) + { + case SocketOptionName.SendTimeout: + //_sendTimeout = optionValue; + break; + case SocketOptionName.ReceiveTimeout: + _receiveTimeout = optionValue; + break; + case SocketOptionName.Broadcast: + _broadcast = optionValue != 0; + break; + } + + lock (_socketOptions) + { + _socketOptions[optionName] = optionValue; + } + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue) + { + // Just linger uses this for now in BSD, which we ignore. + } + + public void Shutdown(SocketShutdown how) + { + switch (how) + { + case SocketShutdown.Both: + _readShutdown = true; + // _writeShutdown = true; + break; + case SocketShutdown.Receive: + _readShutdown = true; + break; + case SocketShutdown.Send: + // _writeShutdown = true; + break; + } + } + + public void ProxyDestroyed() + { + // Do nothing, for now. Will likely be more useful with TCP. + } + + public void Dispose() + { + + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs new file mode 100644 index 000000000..7da1aa998 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs @@ -0,0 +1,93 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using System.Net.Sockets; +using System.Threading; +using TcpClient = NetCoreServer.TcpClient; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxyClient : TcpClient, IProxyClient + { + private const int FailureTimeout = 4000; + + public ProxyConfig ProxyConfig { get; private set; } + + private readonly RyuLdnProtocol _protocol; + + private readonly ManualResetEvent _connected = new ManualResetEvent(false); + private readonly ManualResetEvent _ready = new ManualResetEvent(false); + private readonly AutoResetEvent _error = new AutoResetEvent(false); + + public P2pProxyClient(string address, int port) : base(address, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + _protocol = new RyuLdnProtocol(); + + _protocol.ProxyConfig += HandleProxyConfig; + + ConnectAsync(); + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client connected a new session with Id {Id}"); + + _connected.Set(); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client disconnected a session with Id {Id}"); + + SocketHelpers.UnregisterProxy(); + + _connected.Reset(); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(buffer, (int)offset, (int)size); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client caught an error with code {error}"); + + _error.Set(); + } + + private void HandleProxyConfig(LdnHeader header, ProxyConfig config) + { + ProxyConfig = config; + + SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol)); + + _ready.Set(); + } + + public bool EnsureProxyReady() + { + return _ready.WaitOne(FailureTimeout); + } + + public bool PerformAuth(ExternalProxyConfig config) + { + bool signalled = _connected.WaitOne(FailureTimeout); + + if (!signalled) + { + return false; + } + + SendAsync(_protocol.Encode(PacketId.ExternalProxy, config)); + + return true; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs new file mode 100644 index 000000000..598fb654f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs @@ -0,0 +1,388 @@ +using NetCoreServer; +using Open.Nat; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxyServer : TcpServer, IDisposable + { + public const ushort PrivatePortBase = 39990; + public const int PrivatePortRange = 10; + + private const ushort PublicPortBase = 39990; + private const int PublicPortRange = 10; + + private const ushort PortLeaseLength = 60; + private const ushort PortLeaseRenew = 50; + + private const ushort AuthWaitSeconds = 1; + + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + public ushort PrivatePort { get; } + + private ushort _publicPort; + + private bool _disposed; + private readonly CancellationTokenSource _disposedCancellation = new CancellationTokenSource(); + + private NatDevice _natDevice; + private Mapping _portMapping; + + private readonly List _players = new List(); + + private readonly List _waitingTokens = new List(); + private readonly AutoResetEvent _tokenEvent = new AutoResetEvent(false); + + private uint _broadcastAddress; + + private readonly LdnMasterProxyClient _master; + private readonly RyuLdnProtocol _masterProtocol; + private readonly RyuLdnProtocol _protocol; + + public P2pProxyServer(LdnMasterProxyClient master, ushort port, RyuLdnProtocol masterProtocol) : base(IPAddress.Any, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + PrivatePort = port; + + _master = master; + _masterProtocol = masterProtocol; + + _masterProtocol.ExternalProxyState += HandleStateChange; + _masterProtocol.ExternalProxyToken += HandleToken; + + _protocol = new RyuLdnProtocol(); + } + + private void HandleToken(LdnHeader header, ExternalProxyToken token) + { + _lock.EnterWriteLock(); + + _waitingTokens.Add(token); + + _lock.ExitWriteLock(); + + _tokenEvent.Set(); + } + + private void HandleStateChange(LdnHeader header, ExternalProxyConnectionState state) + { + if (!state.Connected) + { + _lock.EnterWriteLock(); + + _waitingTokens.RemoveAll(token => token.VirtualIp == state.IpAddress); + + _players.RemoveAll(player => + { + if (player.VirtualIpAddress == state.IpAddress) + { + player.DisconnectAndStop(); + + return true; + } + + return false; + }); + + _lock.ExitWriteLock(); + } + } + + public void Configure(ProxyConfig config) + { + _broadcastAddress = config.ProxyIp | (~config.ProxySubnetMask); + } + + public async Task NatPunch() + { + NatDiscoverer discoverer = new NatDiscoverer(); + CancellationTokenSource cts = new CancellationTokenSource(1000); + + NatDevice device; + + try + { + device = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, cts); + } + catch (NatDeviceNotFoundException) + { + return 0; + } + + _publicPort = PublicPortBase; + + for (int i = 0; i < PublicPortRange; i++) + { + try + { + _portMapping = new Mapping(Protocol.Tcp, PrivatePort, _publicPort, PortLeaseLength, "Ryujinx Local Multiplayer"); + + await device.CreatePortMapAsync(_portMapping); + + break; + } + catch (MappingException) + { + _publicPort++; + } + catch (Exception) + { + return 0; + } + + if (i == PublicPortRange - 1) + { + _publicPort = 0; + } + } + + if (_publicPort != 0) + { + _ = Task.Delay(PortLeaseRenew * 1000, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease)); + } + + _natDevice = device; + + return _publicPort; + } + + // Proxy handlers + + private void RouteMessage(P2pProxySession sender, ref ProxyInfo info, Action action) + { + if (info.SourceIpV4 == 0) + { + // If they sent from a connection bound on 0.0.0.0, make others see it as them. + info.SourceIpV4 = sender.VirtualIpAddress; + } + else if (info.SourceIpV4 != sender.VirtualIpAddress) + { + // Can't pretend to be somebody else. + return; + } + + uint destIp = info.DestIpV4; + + if (destIp == 0xc0a800ff) + { + destIp = _broadcastAddress; + } + + bool isBroadcast = destIp == _broadcastAddress; + + _lock.EnterReadLock(); + + if (isBroadcast) + { + _players.ForEach(player => + { + action(player); + }); + } + else + { + P2pProxySession target = _players.FirstOrDefault(player => player.VirtualIpAddress == destIp); + + if (target != null) + { + action(target); + } + } + + _lock.ExitReadLock(); + } + + public void HandleProxyDisconnect(P2pProxySession sender, LdnHeader header, ProxyDisconnectMessage message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyDisconnect, message)); + }); + } + + public void HandleProxyData(P2pProxySession sender, LdnHeader header, ProxyDataHeader message, byte[] data) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyData, message, data)); + }); + } + + public void HandleProxyConnectReply(P2pProxySession sender, LdnHeader header, ProxyConnectResponse message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnectReply, message)); + }); + } + + public void HandleProxyConnect(P2pProxySession sender, LdnHeader header, ProxyConnectRequest message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnect, message)); + }); + } + + // End proxy handlers + + private async Task RefreshLease() + { + if (_disposed || _natDevice == null) + { + return; + } + + try + { + await _natDevice.CreatePortMapAsync(_portMapping); + } + catch (Exception) + { + + } + + _ = Task.Delay(PortLeaseRenew, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease)); + } + + public bool TryRegisterUser(P2pProxySession session, ExternalProxyConfig config) + { + _lock.EnterWriteLock(); + + // Attempt to find matching configuration. If we don't find one, wait for a bit and try again. + // Woken by new tokens coming in from the master server. + + IPAddress address = (session.Socket.RemoteEndPoint as IPEndPoint).Address; + byte[] addressBytes = ProxyHelpers.AddressTo16Byte(address); + + long time; + long endTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency * AuthWaitSeconds; + + do + { + for (int i = 0; i < _waitingTokens.Count; i++) + { + ExternalProxyToken waitToken = _waitingTokens[i]; + + // Allow any client that has a private IP to connect. (indicated by the server as all 0 in the token) + + bool isPrivate = waitToken.PhysicalIp.AsSpan().SequenceEqual(new byte[16]); + bool ipEqual = isPrivate || waitToken.AddressFamily == address.AddressFamily && waitToken.PhysicalIp.AsSpan().SequenceEqual(addressBytes); + + if (ipEqual && waitToken.Token.AsSpan().SequenceEqual(config.Token.AsSpan())) + { + // This is a match. + + _waitingTokens.RemoveAt(i); + + session.SetIpv4(waitToken.VirtualIp); + + ProxyConfig pconfig = new ProxyConfig + { + ProxyIp = session.VirtualIpAddress, + ProxySubnetMask = 0xFFFF0000 // TODO: Use from server. + }; + + if (_players.Count == 0) + { + Configure(pconfig); + } + + _players.Add(session); + + session.SendAsync(_protocol.Encode(PacketId.ProxyConfig, pconfig)); + + _lock.ExitWriteLock(); + + return true; + } + } + + // Couldn't find the token. + // It may not have arrived yet, so wait for one to arrive. + + _lock.ExitWriteLock(); + + time = Stopwatch.GetTimestamp(); + int remainingMs = (int)((endTime - time) / (Stopwatch.Frequency / 1000)); + + if (remainingMs < 0) + { + remainingMs = 0; + } + + _tokenEvent.WaitOne(remainingMs); + + _lock.EnterWriteLock(); + + } while (time < endTime); + + _lock.ExitWriteLock(); + + return false; + } + + public void DisconnectProxyClient(P2pProxySession session) + { + _lock.EnterWriteLock(); + + bool removed = _players.Remove(session); + + if (removed) + { + _master.SendAsync(_masterProtocol.Encode(PacketId.ExternalProxyState, new ExternalProxyConnectionState + { + IpAddress = session.VirtualIpAddress, + Connected = false + })); + } + + _lock.ExitWriteLock(); + } + + public new void Dispose() + { + base.Dispose(); + + _disposed = true; + _disposedCancellation.Cancel(); + + try + { + Task delete = _natDevice?.DeletePortMapAsync(new Mapping(Protocol.Tcp, PrivatePort, _publicPort, 60, "Ryujinx Local Multiplayer")); + + // Just absorb any exceptions. + delete?.ContinueWith((task) => { }); + } + catch (Exception) + { + // Fail silently. + } + } + + protected override TcpSession CreateSession() + { + return new P2pProxySession(this); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP server caught an error with code {error}"); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs new file mode 100644 index 000000000..515feeac5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs @@ -0,0 +1,90 @@ +using NetCoreServer; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using System; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxySession : TcpSession + { + public uint VirtualIpAddress { get; private set; } + public RyuLdnProtocol Protocol { get; } + + private readonly P2pProxyServer _parent; + + private bool _masterClosed; + + public P2pProxySession(P2pProxyServer server) : base(server) + { + _parent = server; + + Protocol = new RyuLdnProtocol(); + + Protocol.ProxyDisconnect += HandleProxyDisconnect; + Protocol.ProxyData += HandleProxyData; + Protocol.ProxyConnectReply += HandleProxyConnectReply; + Protocol.ProxyConnect += HandleProxyConnect; + + Protocol.ExternalProxy += HandleAuthentication; + } + + private void HandleAuthentication(LdnHeader header, ExternalProxyConfig token) + { + if (!_parent.TryRegisterUser(this, token)) + { + Disconnect(); + } + } + + public void SetIpv4(uint ip) + { + VirtualIpAddress = ip; + } + + public void DisconnectAndStop() + { + _masterClosed = true; + + Disconnect(); + } + + protected override void OnDisconnected() + { + if (!_masterClosed) + { + _parent.DisconnectProxyClient(this); + } + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + try + { + Protocol.Read(buffer, (int)offset, (int)size); + } + catch (Exception) + { + Disconnect(); + } + } + + private void HandleProxyDisconnect(LdnHeader header, ProxyDisconnectMessage message) + { + _parent.HandleProxyDisconnect(this, header, message); + } + + private void HandleProxyData(LdnHeader header, ProxyDataHeader message, byte[] data) + { + _parent.HandleProxyData(this, header, message, data); + } + + private void HandleProxyConnectReply(LdnHeader header, ProxyConnectResponse data) + { + _parent.HandleProxyConnectReply(this, header, data); + } + + private void HandleProxyConnect(LdnHeader header, ProxyConnectRequest message) + { + _parent.HandleProxyConnect(this, header, message); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs new file mode 100644 index 000000000..42b1ab6a2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs @@ -0,0 +1,24 @@ +using System; +using System.Net; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + static class ProxyHelpers + { + public static byte[] AddressTo16Byte(IPAddress address) + { + byte[] ipBytes = new byte[16]; + byte[] srcBytes = address.GetAddressBytes(); + + Array.Copy(srcBytes, 0, ipBytes, 0, srcBytes.Length); + + return ipBytes; + } + + public static bool SupportsNoDelay() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs new file mode 100644 index 000000000..d0eeaf125 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs @@ -0,0 +1,380 @@ +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class RyuLdnProtocol + { + private const byte CurrentProtocolVersion = 1; + private const int Magic = ('R' << 0) | ('L' << 8) | ('D' << 16) | ('N' << 24); + private const int MaxPacketSize = 131072; + + private readonly int _headerSize = Marshal.SizeOf(); + + private readonly byte[] _buffer = new byte[MaxPacketSize]; + private int _bufferEnd = 0; + + // Client Packets. + public event Action Initialize; + public event Action Passphrase; + public event Action Connected; + public event Action SyncNetwork; + public event Action ScanReply; + public event Action ScanReplyEnd; + public event Action Disconnected; + + // External Proxy Packets. + public event Action ExternalProxy; + public event Action ExternalProxyState; + public event Action ExternalProxyToken; + + // Server Packets. + public event Action CreateAccessPoint; + public event Action CreateAccessPointPrivate; + public event Action Reject; + public event Action RejectReply; + public event Action SetAcceptPolicy; + public event Action SetAdvertiseData; + public event Action Connect; + public event Action ConnectPrivate; + public event Action Scan; + + // Proxy Packets. + public event Action ProxyConfig; + public event Action ProxyConnect; + public event Action ProxyConnectReply; + public event Action ProxyData; + public event Action ProxyDisconnect; + + // Lifecycle Packets. + public event Action NetworkError; + public event Action Ping; + + public RyuLdnProtocol() { } + + public void Reset() + { + _bufferEnd = 0; + } + + public void Read(byte[] data, int offset, int size) + { + int index = 0; + + while (index < size) + { + if (_bufferEnd < _headerSize) + { + // Assemble the header first. + + int copyable = Math.Min(size - index, Math.Min(size, _headerSize - _bufferEnd)); + + Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable); + + index += copyable; + _bufferEnd += copyable; + } + + if (_bufferEnd >= _headerSize) + { + // The header is available. Make sure we received all the data (size specified in the header) + + LdnHeader ldnHeader = MemoryMarshal.Cast(_buffer)[0]; + + if (ldnHeader.Magic != Magic) + { + throw new InvalidOperationException("Invalid magic number in received packet."); + } + + if (ldnHeader.Version != CurrentProtocolVersion) + { + throw new InvalidOperationException($"Protocol version mismatch. Expected ${CurrentProtocolVersion}, was ${ldnHeader.Version}."); + } + + int finalSize = _headerSize + ldnHeader.DataSize; + + if (finalSize >= MaxPacketSize) + { + throw new InvalidOperationException($"Max packet size {MaxPacketSize} exceeded."); + } + + int copyable = Math.Min(size - index, Math.Min(size, finalSize - _bufferEnd)); + + Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable); + + index += copyable; + _bufferEnd += copyable; + + if (finalSize == _bufferEnd) + { + // The full packet has been retrieved. Send it to be decoded. + + byte[] ldnData = new byte[ldnHeader.DataSize]; + + Array.Copy(_buffer, _headerSize, ldnData, 0, ldnData.Length); + + DecodeAndHandle(ldnHeader, ldnData); + + Reset(); + } + } + } + } + + private (T, byte[]) ParseWithData(byte[] data) where T : struct + { + T str = default; + int size = Marshal.SizeOf(str); + + byte[] remainder = new byte[data.Length - size]; + + if (remainder.Length > 0) + { + Array.Copy(data, size, remainder, 0, remainder.Length); + } + + return (MemoryMarshal.Read(data), remainder); + } + + private void DecodeAndHandle(LdnHeader header, byte[] data) + { + switch ((PacketId)header.Type) + { + // Client Packets. + case PacketId.Initialize: + { + Initialize?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Passphrase: + { + Passphrase?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Connected: + { + Connected?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.SyncNetwork: + { + SyncNetwork?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ScanReply: + { + ScanReply?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + case PacketId.ScanReplyEnd: + { + ScanReplyEnd?.Invoke(header); + + break; + } + case PacketId.Disconnect: + { + Disconnected?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // External Proxy Packets. + case PacketId.ExternalProxy: + { + ExternalProxy?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ExternalProxyState: + { + ExternalProxyState?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ExternalProxyToken: + { + ExternalProxyToken?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Server Packets. + case PacketId.CreateAccessPoint: + { + (CreateAccessPointRequest packet, byte[] extraData) = ParseWithData(data); + CreateAccessPoint?.Invoke(header, packet, extraData); + break; + } + case PacketId.CreateAccessPointPrivate: + { + (CreateAccessPointPrivateRequest packet, byte[] extraData) = ParseWithData(data); + CreateAccessPointPrivate?.Invoke(header, packet, extraData); + break; + } + case PacketId.Reject: + { + Reject?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.RejectReply: + { + RejectReply?.Invoke(header); + + break; + } + case PacketId.SetAcceptPolicy: + { + SetAcceptPolicy?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.SetAdvertiseData: + { + SetAdvertiseData?.Invoke(header, data); + + break; + } + case PacketId.Connect: + { + Connect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ConnectPrivate: + { + ConnectPrivate?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Scan: + { + Scan?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Proxy Packets + case PacketId.ProxyConfig: + { + ProxyConfig?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyConnect: + { + ProxyConnect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyConnectReply: + { + ProxyConnectReply?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyData: + { + (ProxyDataHeader packet, byte[] extraData) = ParseWithData(data); + + ProxyData?.Invoke(header, packet, extraData); + + break; + } + case PacketId.ProxyDisconnect: + { + ProxyDisconnect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Lifecycle Packets. + case PacketId.Ping: + { + Ping?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.NetworkError: + { + NetworkError?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + default: + break; + } + } + + private static LdnHeader GetHeader(PacketId type, int dataSize) + { + return new LdnHeader() + { + Magic = Magic, + Version = CurrentProtocolVersion, + Type = (byte)type, + DataSize = dataSize + }; + } + + public byte[] Encode(PacketId type) + { + LdnHeader header = GetHeader(type, 0); + + return SpanHelpers.AsSpan(ref header).ToArray(); + } + + public byte[] Encode(PacketId type, byte[] data) + { + LdnHeader header = GetHeader(type, data.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + data.Length); + Array.Copy(data, 0, result, Marshal.SizeOf(), data.Length); + + return result; + } + + public byte[] Encode(PacketId type, T packet) where T : unmanaged + { + byte[] packetData = SpanHelpers.AsSpan(ref packet).ToArray(); + + LdnHeader header = GetHeader(type, packetData.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + packetData.Length); + Array.Copy(packetData, 0, result, Marshal.SizeOf(), packetData.Length); + + return result; + } + + public byte[] Encode(PacketId type, T packet, byte[] data) where T : unmanaged + { + byte[] packetData = SpanHelpers.AsSpan(ref packet).ToArray(); + + LdnHeader header = GetHeader(type, packetData.Length + data.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + packetData.Length + data.Length); + Array.Copy(packetData, 0, result, Marshal.SizeOf(), packetData.Length); + Array.Copy(data, 0, result, Marshal.SizeOf() + packetData.Length, data.Length); + + return result; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs new file mode 100644 index 000000000..448d33f29 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x4)] + struct DisconnectMessage + { + public uint DisconnectIP; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs new file mode 100644 index 000000000..9cbb80242 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs @@ -0,0 +1,19 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Sent by the server to point a client towards an external server being used as a proxy. + /// The client then forwards this to the external proxy after connecting, to verify the connection worked. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x26, Pack = 1)] + struct ExternalProxyConfig + { + public Array16 ProxyIp; + public AddressFamily AddressFamily; + public ushort ProxyPort; + public Array16 Token; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs new file mode 100644 index 000000000..ecf4e14f7 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Indicates a change in connection state for the given client. + /// Is sent to notify the master server when connection is first established. + /// Can be sent by the external proxy to the master server to notify it of a proxy disconnect. + /// Can be sent by the master server to notify the external proxy of a user leaving a room. + /// Both will result in a force kick. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 4)] + struct ExternalProxyConnectionState + { + public uint IpAddress; + public bool Connected; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs new file mode 100644 index 000000000..0a8980c37 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs @@ -0,0 +1,20 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Sent by the master server to an external proxy to tell them someone is going to connect. + /// This drives authentication, and lets the proxy know what virtual IP to give to each joiner, + /// as these are managed by the master server. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x28)] + struct ExternalProxyToken + { + public uint VirtualIp; + public Array16 Token; + public Array16 PhysicalIp; + public AddressFamily AddressFamily; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs new file mode 100644 index 000000000..36ddc65fe --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs @@ -0,0 +1,20 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// This message is first sent by the client to identify themselves. + /// If the server has a token+mac combo that matches the submission, then they are returned their new ID and mac address. (the mac is also reassigned to the new id) + /// Otherwise, they are returned a random mac address. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x16)] + struct InitializeMessage + { + // All 0 if we don't have an ID yet. + public Array16 Id; + + // All 0 if we don't have a mac yet. + public Array6 MacAddress; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs new file mode 100644 index 000000000..f41f15ab4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0xA)] + struct LdnHeader + { + public uint Magic; + public byte Type; + public byte Version; + public int DataSize; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs new file mode 100644 index 000000000..b8ef5fbc1 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs @@ -0,0 +1,36 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + enum PacketId + { + Initialize, + Passphrase, + + CreateAccessPoint, + CreateAccessPointPrivate, + ExternalProxy, + ExternalProxyToken, + ExternalProxyState, + SyncNetwork, + Reject, + RejectReply, + Scan, + ScanReply, + ScanReplyEnd, + Connect, + ConnectPrivate, + Connected, + Disconnect, + + ProxyConfig, + ProxyConnect, + ProxyConnectReply, + ProxyData, + ProxyDisconnect, + + SetAcceptPolicy, + SetAdvertiseData, + + Ping = 254, + NetworkError = 255 + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs new file mode 100644 index 000000000..0deba0b07 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs @@ -0,0 +1,11 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x80)] + struct PassphraseMessage + { + public Array128 Passphrase; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs new file mode 100644 index 000000000..135e39caa --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x2)] + struct PingMessage + { + public byte Requester; + public byte Id; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs new file mode 100644 index 000000000..ffce77791 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct ProxyConnectRequest + { + public ProxyInfo Info; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs new file mode 100644 index 000000000..de2e430fb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct ProxyConnectResponse + { + public ProxyInfo Info; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs new file mode 100644 index 000000000..e46a40692 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Represents data sent over a transport layer. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x14)] + struct ProxyDataHeader + { + public ProxyInfo Info; + public uint DataLength; // Followed by the data with the specified byte length. + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs new file mode 100644 index 000000000..eb3648413 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + class ProxyDataPacket + { + public ProxyDataHeader Header; + public byte[] Data; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs new file mode 100644 index 000000000..2154ae109 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x14)] + struct ProxyDisconnectMessage + { + public ProxyInfo Info; + public int DisconnectReason; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs new file mode 100644 index 000000000..d9338f244 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs @@ -0,0 +1,20 @@ +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Information included in all proxied communication. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 1)] + struct ProxyInfo + { + public uint SourceIpV4; + public ushort SourcePort; + + public uint DestIpV4; + public ushort DestPort; + + public ProtocolType Protocol; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs new file mode 100644 index 000000000..1c2ce1f8b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs @@ -0,0 +1,18 @@ +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8)] + struct RejectRequest + { + public uint NodeId; + public DisconnectReason DisconnectReason; + + public RejectRequest(DisconnectReason disconnectReason, uint nodeId) + { + DisconnectReason = disconnectReason; + NodeId = nodeId; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs new file mode 100644 index 000000000..f3bd72023 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs @@ -0,0 +1,23 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x28, Pack = 1)] + struct RyuNetworkConfig + { + public Array16 GameVersion; + + // PrivateIp is included for external proxies for the case where a client attempts to join from + // their own LAN. UPnP forwarding can fail when connecting devices on the same network over the public IP, + // so if their public IP is identical, the internal address should be sent instead. + + // The fields below are 0 if not hosting a p2p proxy. + + public Array16 PrivateIp; + public AddressFamily AddressFamily; + public ushort ExternalProxyPort; + public ushort InternalProxyPort; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs new file mode 100644 index 000000000..c4a969901 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs @@ -0,0 +1,11 @@ +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x1, Pack = 1)] + struct SetAcceptPolicyRequest + { + public AcceptPolicy StationAcceptPolicy; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index e39c01978..fa43f789e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -14,6 +14,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool Connected { get; private set; } + public ProxyConfig Config => _parent.NetworkClient.Config; + public Station(IUserLocalCommunicationService parent) { _parent = parent; @@ -48,9 +50,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public void Dispose() { - _parent.NetworkClient.DisconnectNetwork(); + if (_parent.NetworkClient != null) + { + _parent.NetworkClient.DisconnectNetwork(); - _parent.NetworkClient.NetworkChange -= NetworkChanged; + _parent.NetworkClient.NetworkChange -= NetworkChanged; + } } private ResultCode NetworkErrorToResult(NetworkError error) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs index ac0ff7d94..0972c21c0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types @@ -14,5 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types public UserConfig UserConfig; public NetworkConfig NetworkConfig; public AddressList AddressList; + + public RyuNetworkConfig RyuNetworkConfig; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs index f67f0aac9..d2dc5b698 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types @@ -6,11 +7,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types /// /// Advertise data is appended separately (remaining data in the buffer). /// - [StructLayout(LayoutKind.Sequential, Size = 0x94, CharSet = CharSet.Ansi)] + [StructLayout(LayoutKind.Sequential, Size = 0xBC, Pack = 1)] struct CreateAccessPointRequest { public SecurityConfig SecurityConfig; public UserConfig UserConfig; public NetworkConfig NetworkConfig; + + public RyuNetworkConfig RyuNetworkConfig; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs new file mode 100644 index 000000000..c89c08bbe --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8)] + struct ProxyConfig + { + public uint ProxyIp; + public uint ProxySubnetMask; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs index 21d48288e..3a40a4ac5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs @@ -95,7 +95,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd } } - ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol) + ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol, context.Device.Configuration.MultiplayerLanInterfaceId) { Blocking = !creationFlags.HasFlag(BsdSocketCreationFlags.NonBlocking), }; diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs index c9b811cf5..981fe0a8f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types; using System; using System.Collections.Generic; @@ -21,21 +22,21 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; } - public nint Handle => Socket.Handle; + public nint Handle => IntPtr.Zero; public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint; public IPEndPoint LocalEndPoint => Socket.LocalEndPoint as IPEndPoint; - public Socket Socket { get; } + public ISocketImpl Socket { get; } - public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType) + public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, string lanInterfaceId) { - Socket = new Socket(addressFamily, socketType, protocolType); + Socket = SocketHelpers.CreateSocket(addressFamily, socketType, protocolType, lanInterfaceId); Refcount = 1; } - private ManagedSocket(Socket socket) + private ManagedSocket(ISocketImpl socket) { Socket = socket; Refcount = 1; @@ -185,6 +186,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl } } + bool hasEmittedBlockingWarning = false; + public LinuxError Receive(out int receiveSize, Span buffer, BsdSocketFlags flags) { LinuxError result; @@ -199,6 +202,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl shouldBlockAfterOperation = true; } + if (Blocking && !hasEmittedBlockingWarning) + { + Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors."); + hasEmittedBlockingWarning = true; + } + receiveSize = Socket.Receive(buffer, ConvertBsdSocketFlags(flags)); result = LinuxError.SUCCESS; @@ -236,6 +245,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl shouldBlockAfterOperation = true; } + if (Blocking && !hasEmittedBlockingWarning) + { + Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors."); + hasEmittedBlockingWarning = true; + } + if (!Socket.IsBound) { receiveSize = -1; @@ -313,7 +328,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported GetSockOpt Option: {option} Level: {level}"); optionValue.Clear(); - return LinuxError.SUCCESS; + return LinuxError.EOPNOTSUPP; } byte[] tempOptionValue = new byte[optionValue.Length]; @@ -347,7 +362,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl { Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported SetSockOpt Option: {option} Level: {level}"); - return LinuxError.SUCCESS; + return LinuxError.EOPNOTSUPP; } int value = optionValue.Length >= 4 ? MemoryMarshal.Read(optionValue) : MemoryMarshal.Read(optionValue); @@ -493,7 +508,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl try { - int receiveSize = Socket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); + int receiveSize = (Socket as DefaultSocket).BaseSocket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); if (receiveSize > 0) { @@ -531,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl try { - int sendSize = Socket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); + int sendSize = (Socket as DefaultSocket).BaseSocket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); if (sendSize > 0) { diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs index d0db44086..e870e8aea 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types; using System.Collections.Generic; using System.Net.Sockets; @@ -26,45 +27,46 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public LinuxError Poll(List events, int timeoutMilliseconds, out int updatedCount) { - List readEvents = new(); - List writeEvents = new(); - List errorEvents = new(); + List readEvents = new(); + List writeEvents = new(); + List errorEvents = new(); updatedCount = 0; foreach (PollEvent evnt in events) { - ManagedSocket socket = (ManagedSocket)evnt.FileDescriptor; - - bool isValidEvent = evnt.Data.InputEvents == 0; - - errorEvents.Add(socket.Socket); - - if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + if (evnt.FileDescriptor is ManagedSocket ms) { - readEvents.Add(socket.Socket); + bool isValidEvent = evnt.Data.InputEvents == 0; - isValidEvent = true; - } + errorEvents.Add(ms.Socket); - if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0) - { - readEvents.Add(socket.Socket); + if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + { + readEvents.Add(ms.Socket); - isValidEvent = true; - } + isValidEvent = true; + } - if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0) - { - writeEvents.Add(socket.Socket); + if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0) + { + readEvents.Add(ms.Socket); - isValidEvent = true; - } + isValidEvent = true; + } - if (!isValidEvent) - { - Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}"); - return LinuxError.EINVAL; + if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0) + { + writeEvents.Add(ms.Socket); + + isValidEvent = true; + } + + if (!isValidEvent) + { + Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}"); + return LinuxError.EINVAL; + } } } @@ -72,7 +74,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl { int actualTimeoutMicroseconds = timeoutMilliseconds == -1 ? -1 : timeoutMilliseconds * 1000; - Socket.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds); + SocketHelpers.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds); } catch (SocketException exception) { @@ -81,34 +83,37 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl foreach (PollEvent evnt in events) { - Socket socket = ((ManagedSocket)evnt.FileDescriptor).Socket; - - PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents; - - if (errorEvents.Contains(socket)) + if (evnt.FileDescriptor is ManagedSocket ms) { - outputEvents |= PollEventTypeMask.Error; + ISocketImpl socket = ms.Socket; - if (!socket.Connected || !socket.IsBound) + PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents; + + if (errorEvents.Contains(ms.Socket)) { - outputEvents |= PollEventTypeMask.Disconnected; - } - } + outputEvents |= PollEventTypeMask.Error; - if (readEvents.Contains(socket)) - { - if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + if (!socket.Connected || !socket.IsBound) + { + outputEvents |= PollEventTypeMask.Disconnected; + } + } + + if (readEvents.Contains(ms.Socket)) { - outputEvents |= PollEventTypeMask.Input; + if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + { + outputEvents |= PollEventTypeMask.Input; + } } - } - if (writeEvents.Contains(socket)) - { - outputEvents |= PollEventTypeMask.Output; - } + if (writeEvents.Contains(ms.Socket)) + { + outputEvents |= PollEventTypeMask.Output; + } - evnt.Data.OutputEvents = outputEvents; + evnt.Data.OutputEvents = outputEvents; + } } updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count; @@ -118,53 +123,55 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public LinuxError Select(List events, int timeout, out int updatedCount) { - List readEvents = new(); - List writeEvents = new(); - List errorEvents = new(); + List readEvents = new(); + List writeEvents = new(); + List errorEvents = new(); updatedCount = 0; foreach (PollEvent pollEvent in events) { - ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor; - - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input)) + if (pollEvent.FileDescriptor is ManagedSocket ms) { - readEvents.Add(socket.Socket); - } + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input)) + { + readEvents.Add(ms.Socket); + } - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output)) - { - writeEvents.Add(socket.Socket); - } + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output)) + { + writeEvents.Add(ms.Socket); + } - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error)) - { - errorEvents.Add(socket.Socket); + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error)) + { + errorEvents.Add(ms.Socket); + } } } - Socket.Select(readEvents, writeEvents, errorEvents, timeout); + SocketHelpers.Select(readEvents, writeEvents, errorEvents, timeout); updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count; foreach (PollEvent pollEvent in events) { - ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor; - - if (readEvents.Contains(socket.Socket)) + if (pollEvent.FileDescriptor is ManagedSocket ms) { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Input; - } + if (readEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Input; + } - if (writeEvents.Contains(socket.Socket)) - { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Output; - } + if (writeEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Output; + } - if (errorEvents.Contains(socket.Socket)) - { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Error; + if (errorEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Error; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs new file mode 100644 index 000000000..f1040e799 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs @@ -0,0 +1,178 @@ +using Ryujinx.Common.Utilities; +using System; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + class DefaultSocket : ISocketImpl + { + public Socket BaseSocket { get; } + + public EndPoint RemoteEndPoint => BaseSocket.RemoteEndPoint; + + public EndPoint LocalEndPoint => BaseSocket.LocalEndPoint; + + public bool Connected => BaseSocket.Connected; + + public bool IsBound => BaseSocket.IsBound; + + public AddressFamily AddressFamily => BaseSocket.AddressFamily; + + public SocketType SocketType => BaseSocket.SocketType; + + public ProtocolType ProtocolType => BaseSocket.ProtocolType; + + public bool Blocking { get => BaseSocket.Blocking; set => BaseSocket.Blocking = value; } + + public int Available => BaseSocket.Available; + + private readonly string _lanInterfaceId; + + public DefaultSocket(Socket baseSocket, string lanInterfaceId) + { + _lanInterfaceId = lanInterfaceId; + + BaseSocket = baseSocket; + } + + public DefaultSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId) + { + _lanInterfaceId = lanInterfaceId; + + BaseSocket = new Socket(domain, type, protocol); + } + + private void EnsureNetworkInterfaceBound() + { + if (_lanInterfaceId != "0" && !BaseSocket.IsBound) + { + (_, UnicastIPAddressInformation ipInfo) = NetworkHelpers.GetLocalInterface(_lanInterfaceId); + + BaseSocket.Bind(new IPEndPoint(ipInfo.Address, 0)); + } + } + + public ISocketImpl Accept() + { + return new DefaultSocket(BaseSocket.Accept(), _lanInterfaceId); + } + + public void Bind(EndPoint localEP) + { + // NOTE: The guest is able to receive on 0.0.0.0 without it being limited to the chosen network interface. + // This is because it must get loopback traffic as well. This could allow other network traffic to leak in. + + BaseSocket.Bind(localEP); + } + + public void Close() + { + BaseSocket.Close(); + } + + public void Connect(EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + BaseSocket.Connect(remoteEP); + } + + public void Disconnect(bool reuseSocket) + { + BaseSocket.Disconnect(reuseSocket); + } + + public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue) + { + BaseSocket.GetSocketOption(optionLevel, optionName, optionValue); + } + + public void Listen(int backlog) + { + BaseSocket.Listen(backlog); + } + + public int Receive(Span buffer) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer); + } + + public int Receive(Span buffer, SocketFlags flags) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer, flags); + } + + public int Receive(Span buffer, SocketFlags flags, out SocketError socketError) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer, flags, out socketError); + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.ReceiveFrom(buffer, flags, ref remoteEP); + } + + public int Send(ReadOnlySpan buffer) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer, flags); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer, flags, out socketError); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.SendTo(buffer, flags, remoteEP); + } + + public bool Poll(int microSeconds, SelectMode mode) + { + return BaseSocket.Poll(microSeconds, mode); + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue) + { + BaseSocket.SetSocketOption(optionLevel, optionName, optionValue); + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue) + { + BaseSocket.SetSocketOption(optionLevel, optionName, optionValue); + } + + public void Shutdown(SocketShutdown how) + { + BaseSocket.Shutdown(how); + } + + public void Dispose() + { + BaseSocket.Dispose(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs new file mode 100644 index 000000000..b7055f08b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs @@ -0,0 +1,47 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + interface ISocketImpl : IDisposable + { + EndPoint RemoteEndPoint { get; } + EndPoint LocalEndPoint { get; } + bool Connected { get; } + bool IsBound { get; } + + AddressFamily AddressFamily { get; } + SocketType SocketType { get; } + ProtocolType ProtocolType { get; } + + bool Blocking { get; set; } + int Available { get; } + + int Receive(Span buffer); + int Receive(Span buffer, SocketFlags flags); + int Receive(Span buffer, SocketFlags flags, out SocketError socketError); + int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEP); + + int Send(ReadOnlySpan buffer); + int Send(ReadOnlySpan buffer, SocketFlags flags); + int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError); + int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP); + + bool Poll(int microSeconds, SelectMode mode); + + ISocketImpl Accept(); + + void Bind(EndPoint localEP); + void Connect(EndPoint remoteEP); + void Listen(int backlog); + + void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue); + void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue); + void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue); + + void Shutdown(SocketShutdown how); + void Disconnect(bool reuseSocket); + void Close(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs new file mode 100644 index 000000000..485a7f86b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs @@ -0,0 +1,74 @@ +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + static class SocketHelpers + { + private static LdnProxy _proxy; + + public static void Select(List readEvents, List writeEvents, List errorEvents, int timeout) + { + var readDefault = readEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + var writeDefault = writeEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + var errorDefault = errorEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + + if (readDefault.Count != 0 || writeDefault.Count != 0 || errorDefault.Count != 0) + { + Socket.Select(readDefault, writeDefault, errorDefault, timeout); + } + + void FilterSockets(List removeFrom, List selectedSockets, Func ldnCheck) + { + removeFrom.RemoveAll(socket => + { + switch (socket) + { + case DefaultSocket dsocket: + return !selectedSockets.Contains(dsocket.BaseSocket); + case LdnProxySocket psocket: + return !ldnCheck(psocket); + default: + throw new NotImplementedException(); + } + }); + }; + + FilterSockets(readEvents, readDefault, (socket) => socket.Readable); + FilterSockets(writeEvents, writeDefault, (socket) => socket.Writable); + FilterSockets(errorEvents, errorDefault, (socket) => socket.Error); + } + + public static void RegisterProxy(LdnProxy proxy) + { + if (_proxy != null) + { + UnregisterProxy(); + } + + _proxy = proxy; + } + + public static void UnregisterProxy() + { + _proxy?.Dispose(); + _proxy = null; + } + + public static ISocketImpl CreateSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId) + { + if (_proxy != null) + { + if (_proxy.Supported(domain, type, protocol)) + { + return new LdnProxySocket(domain, type, protocol, _proxy); + } + } + + return new DefaultSocket(domain, type, protocol, lanInterfaceId); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs index 39af90383..5b2de13f0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs @@ -292,7 +292,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres { string host = MemoryHelper.ReadAsciiString(context.Memory, inputBufferPosition, (int)inputBufferSize); - if (!context.Device.Configuration.EnableInternetAccess) + if (host != "localhost" && !context.Device.Configuration.EnableInternetAccess) { Logger.Info?.Print(LogClass.ServiceSfdnsres, $"Guest network access disabled, DNS Blocked: {host}"); diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs index 8cc761baf..dc33dd6a5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs @@ -1,5 +1,6 @@ using Ryujinx.HLE.HOS.Services.Sockets.Bsd; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Ssl.Types; using System; using System.IO; @@ -116,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService public ResultCode Handshake(string hostName) { StartSslOperation(); - _stream = new SslStream(new NetworkStream(((ManagedSocket)Socket).Socket, false), false, null, null); + _stream = new SslStream(new NetworkStream(((DefaultSocket)((ManagedSocket)Socket).Socket).BaseSocket, false), false, null, null); hostName = RetrieveHostName(hostName); _stream.AuthenticateAsClient(hostName, null, TranslateSslVersion(_sslVersion), false); EndSslOperation(); diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index 10561a5a1..e187b2360 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -85,8 +85,8 @@ namespace Ryujinx.HLE.Loaders.Processes } // TODO: LibHac npdm currently doesn't support version field. - string version = ProgramId > 0x0100000000007FFF - ? DisplayVersion + string version = ProgramId > 0x0100000000007FFF + ? DisplayVersion : device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?"; Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]"); diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index a7bb3cd7f..5f7f6db69 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs index ce52f84d9..8c4854a11 100644 --- a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs +++ b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs @@ -117,8 +117,9 @@ namespace Ryujinx.Headless.SDL2.OpenGL GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) - : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { _glLogLevel = glLogLevel; } diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index 2f86f3ebf..8078ca5e4 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -225,6 +225,9 @@ namespace Ryujinx.Headless.SDL2 [Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")] public bool IgnoreMissingServices { get; set; } + + [Option("ignore-controller-applet", Required = false, Default = false, HelpText = "Enable ignoring the controller applet when your game loses connection to your controller.")] + public bool IgnoreControllerApplet { get; set; } // Values diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index b6ccb2ac4..e3bbd1e51 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -444,8 +444,7 @@ namespace Ryujinx.Headless.SDL2 { Logger.AddTarget(new AsyncLogTargetWrapper( new FileLogTarget("file", logFile), - 1000, - AsyncLogTargetOverflowAction.Block + 1000 )); } else @@ -506,8 +505,8 @@ namespace Ryujinx.Headless.SDL2 private static WindowBase CreateWindow(Options options) { return options.GraphicsBackend == GraphicsBackend.Vulkan - ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode) - : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode); + ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet) + : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet); } private static IRenderer CreateRenderer(Options options, WindowBase window) @@ -578,7 +577,10 @@ namespace Ryujinx.Headless.SDL2 options.AudioVolume, options.UseHypervisor ?? true, options.MultiplayerLanInterfaceId, - Common.Configuration.Multiplayer.MultiplayerMode.Disabled); + Common.Configuration.Multiplayer.MultiplayerMode.Disabled, + false, + "", + ""); return new Switch(configuration); } diff --git a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs index fb73ca335..b88e0fe83 100644 --- a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs +++ b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs @@ -17,8 +17,9 @@ namespace Ryujinx.Headless.SDL2.Vulkan GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) - : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { _glLogLevel = glLogLevel; } diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs index bd47dfd5d..6d681e100 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -86,13 +86,15 @@ namespace Ryujinx.Headless.SDL2 private readonly AspectRatio _aspectRatio; private readonly bool _enableMouse; + private readonly bool _ignoreControllerApplet; public WindowBase( InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) { MouseDriver = new SDL2MouseDriver(hideCursorMode); _inputManager = inputManager; @@ -108,6 +110,7 @@ namespace Ryujinx.Headless.SDL2 _gpuDoneEvent = new ManualResetEvent(false); _aspectRatio = aspectRatio; _enableMouse = enableMouse; + _ignoreControllerApplet = ignoreControllerApplet; HostUITheme = new HeadlessHostUiTheme(); SDL2Driver.Instance.Initialize(); @@ -484,6 +487,8 @@ namespace Ryujinx.Headless.SDL2 public bool DisplayMessageDialog(ControllerAppletUIArgs args) { + if (_ignoreControllerApplet) return false; + string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}"; string message = $"Application requests {playerCount} {"player".ToQuantity(args.PlayerCountMin + args.PlayerCountMax, ShowQuantityAs.None)} with:\n\n" diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs index 4c1c1a043..7aa0dccaa 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationData.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs @@ -27,6 +27,8 @@ namespace Ryujinx.UI.App.Common public ulong Id { get; set; } public string Developer { get; set; } = "Unknown"; public string Version { get; set; } = "0"; + public int PlayerCount { get; set; } + public int GameCount { get; set; } public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 044eccbea..174db51ad 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -12,6 +12,7 @@ using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; @@ -27,10 +28,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; +using System.Threading.Tasks; using ContentType = LibHac.Ncm.ContentType; using MissingKeyException = LibHac.Common.Keys.MissingKeyException; using Path = System.IO.Path; @@ -41,8 +44,10 @@ namespace Ryujinx.UI.App.Common { public class ApplicationLibrary { + public static string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com"; public Language DesiredLanguage { get; set; } public event EventHandler ApplicationCountUpdated; + public event EventHandler LdnGameDataReceived; public readonly IObservableCache Applications; public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates; @@ -62,6 +67,7 @@ namespace Ryujinx.UI.App.Common private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc); private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly LdnGameDataSerializerContext _ldnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel) { @@ -687,7 +693,7 @@ namespace Ryujinx.UI.App.Common (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0) || (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI) || (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA) || - (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) || + (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) || (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO) ); @@ -719,6 +725,7 @@ 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) { @@ -775,6 +782,46 @@ namespace Ryujinx.UI.App.Common } } + public async Task RefreshLdn() + { + + if (ConfigurationState.Instance.Multiplayer.Mode == MultiplayerMode.LdnRyu) + { + try + { + string ldnWebHost = ConfigurationState.Instance.Multiplayer.LdnServer; + if (string.IsNullOrEmpty(ldnWebHost)) + { + ldnWebHost = DefaultLanPlayWebHost; + } + IEnumerable ldnGameDataArray = Array.Empty(); + using HttpClient httpClient = new HttpClient(); + string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games"); + ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData); + var evt = new LdnGameDataReceivedEventArgs + { + LdnData = ldnGameDataArray + }; + LdnGameDataReceived?.Invoke(null, evt); + } + catch (Exception ex) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to fetch the public games JSON from the API. Player and game count in the game list will be unavailable.\n{ex.Message}"); + LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs() + { + LdnData = Array.Empty() + }); + } + } + else + { + LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs() + { + LdnData = Array.Empty() + }); + } + } + // Replace the currently stored DLC state for the game with the provided DLC state. public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs) { diff --git a/src/Ryujinx.UI.Common/App/LdnGameData.cs b/src/Ryujinx.UI.Common/App/LdnGameData.cs new file mode 100644 index 000000000..6c784c991 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameData.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Ryujinx.UI.App.Common +{ + public struct LdnGameData + { + public string Id { get; set; } + public int PlayerCount { get; set; } + public int MaxPlayerCount { get; set; } + public string GameName { get; set; } + public string TitleId { get; set; } + public string Mode { get; set; } + public string Status { get; set; } + public IEnumerable Players { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs b/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs new file mode 100644 index 000000000..7c7454411 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.UI.App.Common +{ + public class LdnGameDataReceivedEventArgs : EventArgs + { + public IEnumerable LdnData { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs b/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs new file mode 100644 index 000000000..ce8edcdb6 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.App.Common +{ + [JsonSerializable(typeof(IEnumerable))] + internal partial class LdnGameDataSerializerContext : JsonSerializerContext + { + + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index 77c6346f2..80ba1b186 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -392,6 +392,21 @@ namespace Ryujinx.UI.Common.Configuration /// public string MultiplayerLanInterfaceId { get; set; } + /// + /// Disable P2p Toggle + /// + public bool MultiplayerDisableP2p { get; set; } + + /// + /// Local network passphrase, for private networks. + /// + public string MultiplayerLdnPassphrase { get; set; } + + /// + /// Custom LDN Server + /// + public string LdnServer { get; set; } + /// /// Uses Hypervisor over JIT if available /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs new file mode 100644 index 000000000..65dd88106 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs @@ -0,0 +1,718 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.HLE; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Configuration.UI; +using System; +using System.Collections.Generic; + +namespace Ryujinx.UI.Common.Configuration +{ + public partial class ConfigurationState + { + public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) + { + bool configurationFileUpdated = false; + + if (configurationFileFormat.Version is < 0 or > ConfigurationFileFormat.CurrentVersion) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); + + LoadDefault(); + } + + if (configurationFileFormat.Version < 2) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); + + configurationFileFormat.SystemRegion = Region.USA; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 3) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 3."); + + configurationFileFormat.SystemTimeZone = "UTC"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 4) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 4."); + + configurationFileFormat.MaxAnisotropy = -1; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 5) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 5."); + + configurationFileFormat.SystemTimeOffset = 0; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 8) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8."); + + configurationFileFormat.EnablePtc = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 9) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 9."); + + configurationFileFormat.ColumnSort = new ColumnSort + { + SortColumnId = 0, + SortAscending = false, + }; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = Key.F1, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 10) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 10."); + + configurationFileFormat.AudioBackend = AudioBackend.OpenAl; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 11) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11."); + + configurationFileFormat.ResScale = 1; + configurationFileFormat.ResScaleCustom = 1.0f; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 12) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 12."); + + configurationFileFormat.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None; + + configurationFileUpdated = true; + } + + // configurationFileFormat.Version == 13 -> LDN1 + + if (configurationFileFormat.Version < 14) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14."); + + configurationFileFormat.CheckUpdatesOnStart = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 16) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 16."); + + configurationFileFormat.EnableShaderCache = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 17) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 17."); + + configurationFileFormat.StartFullscreen = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 18) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 18."); + + configurationFileFormat.AspectRatio = AspectRatio.Fixed16x9; + + configurationFileUpdated = true; + } + + // configurationFileFormat.Version == 19 -> LDN2 + + if (configurationFileFormat.Version < 20) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20."); + + configurationFileFormat.ShowConfirmExit = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 21) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21."); + + // Initialize network config. + + configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled; + configurationFileFormat.MultiplayerLanInterfaceId = "0"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 22) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22."); + + configurationFileFormat.HideCursor = HideCursorMode.Never; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 24) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24."); + + configurationFileFormat.InputConfig = new List + { + new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = "0", + PlayerIndex = PlayerIndex.Player1, + ControllerType = ControllerType.ProController, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = Key.Up, + DpadDown = Key.Down, + DpadLeft = Key.Left, + DpadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H, + }, + }, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 25) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25."); + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 26) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26."); + + configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 27) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27."); + + configurationFileFormat.EnableMouse = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 28) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = Key.F1, + Screenshot = Key.F8, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 29) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = Key.F1, + Screenshot = Key.F8, + ShowUI = Key.F4, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 30) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30."); + + foreach (InputConfig config in configurationFileFormat.InputConfig) + { + if (config is StandardControllerInputConfig controllerConfig) + { + controllerConfig.Rumble = new RumbleConfigController + { + EnableRumble = false, + StrongRumble = 1f, + WeakRumble = 1f, + }; + } + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 31) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31."); + + configurationFileFormat.BackendThreading = BackendThreading.Auto; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 32) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = Key.F5, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 33) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = Key.F2, + }; + + configurationFileFormat.AudioVolume = 1; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 34) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34."); + + configurationFileFormat.EnableInternetAccess = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 35) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35."); + + foreach (InputConfig config in configurationFileFormat.InputConfig) + { + if (config is StandardControllerInputConfig controllerConfig) + { + controllerConfig.RangeLeft = 1.0f; + controllerConfig.RangeRight = 1.0f; + } + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 36) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36."); + + configurationFileFormat.LoggingEnableTrace = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 37) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37."); + + configurationFileFormat.ShowConsole = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 38) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38."); + + configurationFileFormat.BaseStyle = "Dark"; + configurationFileFormat.GameListViewMode = 0; + configurationFileFormat.ShowNames = true; + configurationFileFormat.GridSize = 2; + configurationFileFormat.LanguageCode = "en_US"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 39) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = Key.Unbound, + ResScaleDown = Key.Unbound, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 40) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40."); + + configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 41) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, + ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, + VolumeUp = Key.Unbound, + VolumeDown = Key.Unbound, + }; + } + + if (configurationFileFormat.Version < 42) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42."); + + configurationFileFormat.EnableMacroHLE = true; + } + + if (configurationFileFormat.Version < 43) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43."); + + configurationFileFormat.UseHypervisor = true; + } + + if (configurationFileFormat.Version < 44) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44."); + + configurationFileFormat.AntiAliasing = AntiAliasing.None; + configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear; + configurationFileFormat.ScalingFilterLevel = 80; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 45) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45."); + + configurationFileFormat.ShownFileTypes = new ShownFileTypes + { + NSP = true, + PFS0 = true, + XCI = true, + NCA = true, + NRO = true, + NSO = true, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 46) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46."); + + configurationFileFormat.MultiplayerLanInterfaceId = "0"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 47) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47."); + + configurationFileFormat.WindowStartup = new WindowStartup + { + WindowPositionX = 0, + WindowPositionY = 0, + WindowSizeHeight = 760, + WindowSizeWidth = 1280, + WindowMaximized = false, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 48) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48."); + + configurationFileFormat.EnableColorSpacePassthrough = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 49) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49."); + + if (OperatingSystem.IsMacOS()) + { + AppDataManager.FixMacOSConfigurationFolders(); + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 50) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 50."); + + configurationFileFormat.EnableHardwareAcceleration = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 51) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51."); + + configurationFileFormat.RememberWindowState = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 52) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); + + configurationFileFormat.AutoloadDirs = []; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 53) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53."); + + configurationFileFormat.EnableLowPowerPtc = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 54) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 54."); + + configurationFileFormat.DramSize = MemoryConfiguration.MemoryConfiguration4GiB; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 55) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 55."); + + configurationFileFormat.IgnoreApplet = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 56) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 56."); + + configurationFileFormat.ShowTitleBar = !OperatingSystem.IsWindows(); + + configurationFileUpdated = true; + } + + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; + Graphics.ResScale.Value = configurationFileFormat.ResScale; + Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; + Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; + Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; + Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; + Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; + Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; + Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; + Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing; + Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter; + Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel; + Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug; + Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub; + Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo; + Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn; + Logger.EnableError.Value = configurationFileFormat.LoggingEnableError; + Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace; + Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest; + Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog; + Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses; + Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel; + System.Language.Value = configurationFileFormat.SystemLanguage; + System.Region.Value = configurationFileFormat.SystemRegion; + System.TimeZone.Value = configurationFileFormat.SystemTimeZone; + System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset; + System.EnableDockedMode.Value = configurationFileFormat.DockedMode; + EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; + CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; + ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; + IgnoreApplet.Value = configurationFileFormat.IgnoreApplet; + RememberWindowState.Value = configurationFileFormat.RememberWindowState; + ShowTitleBar.Value = configurationFileFormat.ShowTitleBar; + EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; + HideCursor.Value = configurationFileFormat.HideCursor; + Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; + Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; + Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; + Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; + Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough; + System.EnablePtc.Value = configurationFileFormat.EnablePtc; + System.EnableLowPowerPtc.Value = configurationFileFormat.EnableLowPowerPtc; + System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess; + System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks; + System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode; + System.AudioBackend.Value = configurationFileFormat.AudioBackend; + System.AudioVolume.Value = configurationFileFormat.AudioVolume; + System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode; + System.DramSize.Value = configurationFileFormat.DramSize; + System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices; + System.UseHypervisor.Value = configurationFileFormat.UseHypervisor; + UI.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn; + UI.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn; + UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn; + UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn; + UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn; + UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn; + UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn; + UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn; + UI.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn; + UI.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn; + UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId; + UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending; + UI.GameDirs.Value = configurationFileFormat.GameDirs; + UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs ?? []; + UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP; + UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0; + UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI; + UI.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA; + UI.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO; + UI.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO; + UI.LanguageCode.Value = configurationFileFormat.LanguageCode; + UI.BaseStyle.Value = configurationFileFormat.BaseStyle; + UI.GameListViewMode.Value = configurationFileFormat.GameListViewMode; + UI.ShowNames.Value = configurationFileFormat.ShowNames; + UI.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder; + UI.GridSize.Value = configurationFileFormat.GridSize; + UI.ApplicationSort.Value = configurationFileFormat.ApplicationSort; + UI.StartFullscreen.Value = configurationFileFormat.StartFullscreen; + UI.ShowConsole.Value = configurationFileFormat.ShowConsole; + UI.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth; + UI.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight; + UI.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX; + UI.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY; + UI.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized; + Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; + Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; + Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; + Hid.InputConfig.Value = configurationFileFormat.InputConfig ?? []; + + Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId; + Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode; + Multiplayer.DisableP2p.Value = configurationFileFormat.MultiplayerDisableP2p; + Multiplayer.LdnPassphrase.Value = configurationFileFormat.MultiplayerLdnPassphrase; + Multiplayer.LdnServer.Value = configurationFileFormat.LdnServer; + + if (configurationFileUpdated) + { + ToFileFormat().SaveConfig(configurationFilePath); + + Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); + } + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs new file mode 100644 index 000000000..9be8f4df7 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs @@ -0,0 +1,700 @@ +using ARMeilleure; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.HLE; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Helper; +using System.Collections.Generic; + +namespace Ryujinx.UI.Common.Configuration +{ + public partial class ConfigurationState + { + /// + /// UI configuration section + /// + public class UISection + { + public class Columns + { + public ReactiveObject FavColumn { get; private set; } + public ReactiveObject IconColumn { get; private set; } + public ReactiveObject AppColumn { get; private set; } + public ReactiveObject DevColumn { get; private set; } + public ReactiveObject VersionColumn { get; private set; } + public ReactiveObject LdnInfoColumn { get; private set; } + public ReactiveObject TimePlayedColumn { get; private set; } + public ReactiveObject LastPlayedColumn { get; private set; } + public ReactiveObject FileExtColumn { get; private set; } + public ReactiveObject FileSizeColumn { get; private set; } + public ReactiveObject PathColumn { get; private set; } + + public Columns() + { + FavColumn = new ReactiveObject(); + IconColumn = new ReactiveObject(); + AppColumn = new ReactiveObject(); + DevColumn = new ReactiveObject(); + VersionColumn = new ReactiveObject(); + LdnInfoColumn = new ReactiveObject(); + TimePlayedColumn = new ReactiveObject(); + LastPlayedColumn = new ReactiveObject(); + FileExtColumn = new ReactiveObject(); + FileSizeColumn = new ReactiveObject(); + PathColumn = new ReactiveObject(); + } + } + + public class ColumnSortSettings + { + public ReactiveObject SortColumnId { get; private set; } + public ReactiveObject SortAscending { get; private set; } + + public ColumnSortSettings() + { + SortColumnId = new ReactiveObject(); + SortAscending = new ReactiveObject(); + } + } + + /// + /// Used to toggle which file types are shown in the UI + /// + public class ShownFileTypeSettings + { + public ReactiveObject NSP { get; private set; } + public ReactiveObject PFS0 { get; private set; } + public ReactiveObject XCI { get; private set; } + public ReactiveObject NCA { get; private set; } + public ReactiveObject NRO { get; private set; } + public ReactiveObject NSO { get; private set; } + + public ShownFileTypeSettings() + { + NSP = new ReactiveObject(); + PFS0 = new ReactiveObject(); + XCI = new ReactiveObject(); + NCA = new ReactiveObject(); + NRO = new ReactiveObject(); + NSO = new ReactiveObject(); + } + } + + // + /// Determines main window start-up position, size and state + /// + public class WindowStartupSettings + { + public ReactiveObject WindowSizeWidth { get; private set; } + public ReactiveObject WindowSizeHeight { get; private set; } + public ReactiveObject WindowPositionX { get; private set; } + public ReactiveObject WindowPositionY { get; private set; } + public ReactiveObject WindowMaximized { get; private set; } + + public WindowStartupSettings() + { + WindowSizeWidth = new ReactiveObject(); + WindowSizeHeight = new ReactiveObject(); + WindowPositionX = new ReactiveObject(); + WindowPositionY = new ReactiveObject(); + WindowMaximized = new ReactiveObject(); + } + } + + /// + /// Used to toggle columns in the GUI + /// + public Columns GuiColumns { get; private set; } + + /// + /// Used to configure column sort settings in the GUI + /// + public ColumnSortSettings ColumnSort { get; private set; } + + /// + /// A list of directories containing games to be used to load games into the games list + /// + public ReactiveObject> GameDirs { get; private set; } + + /// + /// A list of directories containing DLC/updates the user wants to autoload during library refreshes + /// + public ReactiveObject> AutoloadDirs { get; private set; } + + /// + /// A list of file types to be hidden in the games List + /// + public ShownFileTypeSettings ShownFileTypes { get; private set; } + + /// + /// Determines main window start-up position, size and state + /// + public WindowStartupSettings WindowStartup { get; private set; } + + /// + /// Language Code for the UI + /// + public ReactiveObject LanguageCode { get; private set; } + + /// + /// Selects the base style + /// + public ReactiveObject BaseStyle { get; private set; } + + /// + /// Start games in fullscreen mode + /// + public ReactiveObject StartFullscreen { get; private set; } + + /// + /// Hide / Show Console Window + /// + public ReactiveObject ShowConsole { get; private set; } + + /// + /// View Mode of the Game list + /// + public ReactiveObject GameListViewMode { get; private set; } + + /// + /// Show application name in Grid Mode + /// + public ReactiveObject ShowNames { get; private set; } + + /// + /// Sets App Icon Size in Grid Mode + /// + public ReactiveObject GridSize { get; private set; } + + /// + /// Sorts Apps in Grid Mode + /// + public ReactiveObject ApplicationSort { get; private set; } + + /// + /// Sets if Grid is ordered in Ascending Order + /// + public ReactiveObject IsAscendingOrder { get; private set; } + + public UISection() + { + GuiColumns = new Columns(); + ColumnSort = new ColumnSortSettings(); + GameDirs = new ReactiveObject>(); + AutoloadDirs = new ReactiveObject>(); + ShownFileTypes = new ShownFileTypeSettings(); + WindowStartup = new WindowStartupSettings(); + BaseStyle = new ReactiveObject(); + StartFullscreen = new ReactiveObject(); + GameListViewMode = new ReactiveObject(); + ShowNames = new ReactiveObject(); + GridSize = new ReactiveObject(); + ApplicationSort = new ReactiveObject(); + IsAscendingOrder = new ReactiveObject(); + LanguageCode = new ReactiveObject(); + ShowConsole = new ReactiveObject(); + ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue); + } + } + + /// + /// Logger configuration section + /// + public class LoggerSection + { + /// + /// Enables printing debug log messages + /// + public ReactiveObject EnableDebug { get; private set; } + + /// + /// Enables printing stub log messages + /// + public ReactiveObject EnableStub { get; private set; } + + /// + /// Enables printing info log messages + /// + public ReactiveObject EnableInfo { get; private set; } + + /// + /// Enables printing warning log messages + /// + public ReactiveObject EnableWarn { get; private set; } + + /// + /// Enables printing error log messages + /// + public ReactiveObject EnableError { get; private set; } + + /// + /// Enables printing trace log messages + /// + public ReactiveObject EnableTrace { get; private set; } + + /// + /// Enables printing guest log messages + /// + public ReactiveObject EnableGuest { get; private set; } + + /// + /// Enables printing FS access log messages + /// + public ReactiveObject EnableFsAccessLog { get; private set; } + + /// + /// Controls which log messages are written to the log targets + /// + public ReactiveObject FilteredClasses { get; private set; } + + /// + /// Enables or disables logging to a file on disk + /// + public ReactiveObject EnableFileLog { get; private set; } + + /// + /// Controls which OpenGL log messages are recorded in the log + /// + public ReactiveObject GraphicsDebugLevel { get; private set; } + + public LoggerSection() + { + EnableDebug = new ReactiveObject(); + EnableDebug.LogChangesToValue(nameof(EnableDebug)); + EnableStub = new ReactiveObject(); + EnableInfo = new ReactiveObject(); + EnableWarn = new ReactiveObject(); + EnableError = new ReactiveObject(); + EnableTrace = new ReactiveObject(); + EnableGuest = new ReactiveObject(); + EnableFsAccessLog = new ReactiveObject(); + FilteredClasses = new ReactiveObject(); + EnableFileLog = new ReactiveObject(); + EnableFileLog.LogChangesToValue(nameof(EnableFileLog)); + GraphicsDebugLevel = new ReactiveObject(); + } + } + + /// + /// System configuration section + /// + public class SystemSection + { + /// + /// Change System Language + /// + public ReactiveObject Language { get; private set; } + + /// + /// Change System Region + /// + public ReactiveObject Region { get; private set; } + + /// + /// Change System TimeZone + /// + public ReactiveObject TimeZone { get; private set; } + + /// + /// System Time Offset in Seconds + /// + public ReactiveObject SystemTimeOffset { get; private set; } + + /// + /// Enables or disables Docked Mode + /// + public ReactiveObject EnableDockedMode { get; private set; } + + /// + /// Enables or disables persistent profiled translation cache + /// + public ReactiveObject EnablePtc { get; private set; } + + /// + /// Enables or disables low-power persistent profiled translation cache loading + /// + public ReactiveObject EnableLowPowerPtc { get; private set; } + + /// + /// Enables or disables guest Internet access + /// + public ReactiveObject EnableInternetAccess { get; private set; } + + /// + /// Enables integrity checks on Game content files + /// + public ReactiveObject EnableFsIntegrityChecks { get; private set; } + + /// + /// Enables FS access log output to the console. Possible modes are 0-3 + /// + public ReactiveObject FsGlobalAccessLogMode { get; private set; } + + /// + /// The selected audio backend + /// + public ReactiveObject AudioBackend { get; private set; } + + /// + /// The audio backend volume + /// + public ReactiveObject AudioVolume { get; private set; } + + /// + /// The selected memory manager mode + /// + public ReactiveObject MemoryManagerMode { get; private set; } + + /// + /// Defines the amount of RAM available on the emulated system, and how it is distributed + /// + public ReactiveObject DramSize { get; private set; } + + /// + /// Enable or disable ignoring missing services + /// + public ReactiveObject IgnoreMissingServices { get; private set; } + + /// + /// Uses Hypervisor over JIT if available + /// + public ReactiveObject UseHypervisor { get; private set; } + + public SystemSection() + { + Language = new ReactiveObject(); + Language.LogChangesToValue(nameof(Language)); + Region = new ReactiveObject(); + Region.LogChangesToValue(nameof(Region)); + TimeZone = new ReactiveObject(); + TimeZone.LogChangesToValue(nameof(TimeZone)); + SystemTimeOffset = new ReactiveObject(); + SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset)); + EnableDockedMode = new ReactiveObject(); + EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode)); + EnablePtc = new ReactiveObject(); + EnablePtc.LogChangesToValue(nameof(EnablePtc)); + EnableLowPowerPtc = new ReactiveObject(); + EnableLowPowerPtc.LogChangesToValue(nameof(EnableLowPowerPtc)); + EnableLowPowerPtc.Event += (_, evnt) + => Optimizations.LowPower = evnt.NewValue; + EnableInternetAccess = new ReactiveObject(); + EnableInternetAccess.LogChangesToValue(nameof(EnableInternetAccess)); + EnableFsIntegrityChecks = new ReactiveObject(); + EnableFsIntegrityChecks.LogChangesToValue(nameof(EnableFsIntegrityChecks)); + FsGlobalAccessLogMode = new ReactiveObject(); + FsGlobalAccessLogMode.LogChangesToValue(nameof(FsGlobalAccessLogMode)); + AudioBackend = new ReactiveObject(); + AudioBackend.LogChangesToValue(nameof(AudioBackend)); + MemoryManagerMode = new ReactiveObject(); + MemoryManagerMode.LogChangesToValue(nameof(MemoryManagerMode)); + DramSize = new ReactiveObject(); + DramSize.LogChangesToValue(nameof(DramSize)); + IgnoreMissingServices = new ReactiveObject(); + IgnoreMissingServices.LogChangesToValue(nameof(IgnoreMissingServices)); + AudioVolume = new ReactiveObject(); + AudioVolume.LogChangesToValue(nameof(AudioVolume)); + UseHypervisor = new ReactiveObject(); + UseHypervisor.LogChangesToValue(nameof(UseHypervisor)); + } + } + + /// + /// Hid configuration section + /// + public class HidSection + { + /// + /// Enable or disable keyboard support (Independent from controllers binding) + /// + public ReactiveObject EnableKeyboard { get; private set; } + + /// + /// Enable or disable mouse support (Independent from controllers binding) + /// + public ReactiveObject EnableMouse { get; private set; } + + /// + /// Hotkey Keyboard Bindings + /// + public ReactiveObject Hotkeys { get; private set; } + + /// + /// Input device configuration. + /// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed. + /// TODO: Implement a ReactiveList class. + /// + public ReactiveObject> InputConfig { get; private set; } + + public HidSection() + { + EnableKeyboard = new ReactiveObject(); + EnableMouse = new ReactiveObject(); + Hotkeys = new ReactiveObject(); + InputConfig = new ReactiveObject>(); + } + } + + /// + /// Graphics configuration section + /// + public class GraphicsSection + { + /// + /// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime. + /// + public ReactiveObject BackendThreading { get; private set; } + + /// + /// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide. + /// + public ReactiveObject MaxAnisotropy { get; private set; } + + /// + /// Aspect Ratio applied to the renderer window. + /// + public ReactiveObject AspectRatio { get; private set; } + + /// + /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead. + /// + public ReactiveObject ResScale { get; private set; } + + /// + /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1. + /// + public ReactiveObject ResScaleCustom { get; private set; } + + /// + /// Dumps shaders in this local directory + /// + public ReactiveObject ShadersDumpPath { get; private set; } + + /// + /// Enables or disables Vertical Sync + /// + public ReactiveObject EnableVsync { get; private set; } + + /// + /// Enables or disables Shader cache + /// + public ReactiveObject EnableShaderCache { get; private set; } + + /// + /// Enables or disables texture recompression + /// + public ReactiveObject EnableTextureRecompression { get; private set; } + + /// + /// Enables or disables Macro high-level emulation + /// + public ReactiveObject EnableMacroHLE { get; private set; } + + /// + /// Enables or disables color space passthrough, if available. + /// + public ReactiveObject EnableColorSpacePassthrough { get; private set; } + + /// + /// Graphics backend + /// + public ReactiveObject GraphicsBackend { get; private set; } + + /// + /// Applies anti-aliasing to the renderer. + /// + public ReactiveObject AntiAliasing { get; private set; } + + /// + /// Sets the framebuffer upscaling type. + /// + public ReactiveObject ScalingFilter { get; private set; } + + /// + /// Sets the framebuffer upscaling level. + /// + public ReactiveObject ScalingFilterLevel { get; private set; } + + /// + /// Preferred GPU + /// + public ReactiveObject PreferredGpu { get; private set; } + + public GraphicsSection() + { + BackendThreading = new ReactiveObject(); + BackendThreading.LogChangesToValue(nameof(BackendThreading)); + ResScale = new ReactiveObject(); + ResScale.LogChangesToValue(nameof(ResScale)); + ResScaleCustom = new ReactiveObject(); + ResScaleCustom.LogChangesToValue(nameof(ResScaleCustom)); + MaxAnisotropy = new ReactiveObject(); + MaxAnisotropy.LogChangesToValue(nameof(MaxAnisotropy)); + AspectRatio = new ReactiveObject(); + AspectRatio.LogChangesToValue(nameof(AspectRatio)); + ShadersDumpPath = new ReactiveObject(); + EnableVsync = new ReactiveObject(); + EnableVsync.LogChangesToValue(nameof(EnableVsync)); + EnableShaderCache = new ReactiveObject(); + EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache)); + EnableTextureRecompression = new ReactiveObject(); + EnableTextureRecompression.LogChangesToValue(nameof(EnableTextureRecompression)); + GraphicsBackend = new ReactiveObject(); + GraphicsBackend.LogChangesToValue(nameof(GraphicsBackend)); + PreferredGpu = new ReactiveObject(); + PreferredGpu.LogChangesToValue(nameof(PreferredGpu)); + EnableMacroHLE = new ReactiveObject(); + EnableMacroHLE.LogChangesToValue(nameof(EnableMacroHLE)); + EnableColorSpacePassthrough = new ReactiveObject(); + EnableColorSpacePassthrough.LogChangesToValue(nameof(EnableColorSpacePassthrough)); + AntiAliasing = new ReactiveObject(); + AntiAliasing.LogChangesToValue(nameof(AntiAliasing)); + ScalingFilter = new ReactiveObject(); + ScalingFilter.LogChangesToValue(nameof(ScalingFilter)); + ScalingFilterLevel = new ReactiveObject(); + ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel)); + } + } + + /// + /// Multiplayer configuration section + /// + public class MultiplayerSection + { + /// + /// GUID for the network interface used by LAN (or 0 for default) + /// + public ReactiveObject LanInterfaceId { get; private set; } + + /// + /// Multiplayer Mode + /// + public ReactiveObject Mode { get; private set; } + + /// + /// Disable P2P + /// + public ReactiveObject DisableP2p { get; private set; } + + /// + /// LDN PassPhrase + /// + public ReactiveObject LdnPassphrase { get; private set; } + + /// + /// LDN Server + /// + public ReactiveObject LdnServer { get; private set; } + + public MultiplayerSection() + { + LanInterfaceId = new ReactiveObject(); + Mode = new ReactiveObject(); + Mode.LogChangesToValue(nameof(MultiplayerMode)); + DisableP2p = new ReactiveObject(); + DisableP2p.LogChangesToValue(nameof(DisableP2p)); + LdnPassphrase = new ReactiveObject(); + LdnPassphrase.LogChangesToValue(nameof(LdnPassphrase)); + LdnServer = new ReactiveObject(); + LdnServer.LogChangesToValue(nameof(LdnServer)); + } + } + + /// + /// The default configuration instance + /// + public static ConfigurationState Instance { get; private set; } + + /// + /// The UI section + /// + public UISection UI { get; private set; } + + /// + /// The Logger section + /// + public LoggerSection Logger { get; private set; } + + /// + /// The System section + /// + public SystemSection System { get; private set; } + + /// + /// The Graphics section + /// + public GraphicsSection Graphics { get; private set; } + + /// + /// The Hid section + /// + public HidSection Hid { get; private set; } + + /// + /// The Multiplayer section + /// + public MultiplayerSection Multiplayer { get; private set; } + + /// + /// Enables or disables Discord Rich Presence + /// + public ReactiveObject EnableDiscordIntegration { get; private set; } + + /// + /// Checks for updates when Ryujinx starts when enabled + /// + public ReactiveObject CheckUpdatesOnStart { get; private set; } + + /// + /// Show "Confirm Exit" Dialog + /// + public ReactiveObject ShowConfirmExit { get; private set; } + + /// + /// Ignore Applet + /// + public ReactiveObject IgnoreApplet { get; private set; } + + /// + /// Enables or disables save window size, position and state on close. + /// + public ReactiveObject RememberWindowState { get; private set; } + + /// + /// Enables or disables the redesigned title bar + /// + public ReactiveObject ShowTitleBar { get; private set; } + + /// + /// Enables hardware-accelerated rendering for Avalonia + /// + public ReactiveObject EnableHardwareAcceleration { get; private set; } + + /// + /// Hide Cursor on Idle + /// + public ReactiveObject HideCursor { get; private set; } + + private ConfigurationState() + { + UI = new UISection(); + Logger = new LoggerSection(); + System = new SystemSection(); + Graphics = new GraphicsSection(); + Hid = new HidSection(); + Multiplayer = new MultiplayerSection(); + EnableDiscordIntegration = new ReactiveObject(); + CheckUpdatesOnStart = new ReactiveObject(); + ShowConfirmExit = new ReactiveObject(); + IgnoreApplet = new ReactiveObject(); + IgnoreApplet.LogChangesToValue(nameof(IgnoreApplet)); + RememberWindowState = new ReactiveObject(); + ShowTitleBar = new ReactiveObject(); + EnableHardwareAcceleration = new ReactiveObject(); + HideCursor = new ReactiveObject(); + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 50b3569a1..b3012568e 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -1,5 +1,4 @@ using ARMeilleure; -using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; @@ -10,668 +9,22 @@ using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE; using Ryujinx.UI.Common.Configuration.System; using Ryujinx.UI.Common.Configuration.UI; -using Ryujinx.UI.Common.Helper; using System; using System.Collections.Generic; -using System.Globalization; -using System.Text.Json.Nodes; namespace Ryujinx.UI.Common.Configuration { - public class ConfigurationState + public partial class ConfigurationState { - /// - /// UI configuration section - /// - public class UISection + public static void Initialize() { - public class Columns + if (Instance != null) { - public ReactiveObject FavColumn { get; private set; } - public ReactiveObject IconColumn { get; private set; } - public ReactiveObject AppColumn { get; private set; } - public ReactiveObject DevColumn { get; private set; } - public ReactiveObject VersionColumn { get; private set; } - public ReactiveObject TimePlayedColumn { get; private set; } - public ReactiveObject LastPlayedColumn { get; private set; } - public ReactiveObject FileExtColumn { get; private set; } - public ReactiveObject FileSizeColumn { get; private set; } - public ReactiveObject PathColumn { get; private set; } - - public Columns() - { - FavColumn = new ReactiveObject(); - IconColumn = new ReactiveObject(); - AppColumn = new ReactiveObject(); - DevColumn = new ReactiveObject(); - VersionColumn = new ReactiveObject(); - TimePlayedColumn = new ReactiveObject(); - LastPlayedColumn = new ReactiveObject(); - FileExtColumn = new ReactiveObject(); - FileSizeColumn = new ReactiveObject(); - PathColumn = new ReactiveObject(); + throw new InvalidOperationException("Configuration is already initialized"); } - } - public class ColumnSortSettings - { - public ReactiveObject SortColumnId { get; private set; } - public ReactiveObject SortAscending { get; private set; } - - public ColumnSortSettings() - { - SortColumnId = new ReactiveObject(); - SortAscending = new ReactiveObject(); + Instance = new ConfigurationState(); } - } - - /// - /// Used to toggle which file types are shown in the UI - /// - public class ShownFileTypeSettings - { - public ReactiveObject NSP { get; private set; } - public ReactiveObject PFS0 { get; private set; } - public ReactiveObject XCI { get; private set; } - public ReactiveObject NCA { get; private set; } - public ReactiveObject NRO { get; private set; } - public ReactiveObject NSO { get; private set; } - - public ShownFileTypeSettings() - { - NSP = new ReactiveObject(); - PFS0 = new ReactiveObject(); - XCI = new ReactiveObject(); - NCA = new ReactiveObject(); - NRO = new ReactiveObject(); - NSO = new ReactiveObject(); - } - } - - // - /// Determines main window start-up position, size and state - /// - public class WindowStartupSettings - { - public ReactiveObject WindowSizeWidth { get; private set; } - public ReactiveObject WindowSizeHeight { get; private set; } - public ReactiveObject WindowPositionX { get; private set; } - public ReactiveObject WindowPositionY { get; private set; } - public ReactiveObject WindowMaximized { get; private set; } - - public WindowStartupSettings() - { - WindowSizeWidth = new ReactiveObject(); - WindowSizeHeight = new ReactiveObject(); - WindowPositionX = new ReactiveObject(); - WindowPositionY = new ReactiveObject(); - WindowMaximized = new ReactiveObject(); - } - } - - /// - /// Used to toggle columns in the GUI - /// - public Columns GuiColumns { get; private set; } - - /// - /// Used to configure column sort settings in the GUI - /// - public ColumnSortSettings ColumnSort { get; private set; } - - /// - /// A list of directories containing games to be used to load games into the games list - /// - public ReactiveObject> GameDirs { get; private set; } - - /// - /// A list of directories containing DLC/updates the user wants to autoload during library refreshes - /// - public ReactiveObject> AutoloadDirs { get; private set; } - - /// - /// A list of file types to be hidden in the games List - /// - public ShownFileTypeSettings ShownFileTypes { get; private set; } - - /// - /// Determines main window start-up position, size and state - /// - public WindowStartupSettings WindowStartup { get; private set; } - - /// - /// Language Code for the UI - /// - public ReactiveObject LanguageCode { get; private set; } - - /// - /// Selects the base style - /// - public ReactiveObject BaseStyle { get; private set; } - - /// - /// Start games in fullscreen mode - /// - public ReactiveObject StartFullscreen { get; private set; } - - /// - /// Hide / Show Console Window - /// - public ReactiveObject ShowConsole { get; private set; } - - /// - /// View Mode of the Game list - /// - public ReactiveObject GameListViewMode { get; private set; } - - /// - /// Show application name in Grid Mode - /// - public ReactiveObject ShowNames { get; private set; } - - /// - /// Sets App Icon Size in Grid Mode - /// - public ReactiveObject GridSize { get; private set; } - - /// - /// Sorts Apps in Grid Mode - /// - public ReactiveObject ApplicationSort { get; private set; } - - /// - /// Sets if Grid is ordered in Ascending Order - /// - public ReactiveObject IsAscendingOrder { get; private set; } - - public UISection() - { - GuiColumns = new Columns(); - ColumnSort = new ColumnSortSettings(); - GameDirs = new ReactiveObject>(); - AutoloadDirs = new ReactiveObject>(); - ShownFileTypes = new ShownFileTypeSettings(); - WindowStartup = new WindowStartupSettings(); - BaseStyle = new ReactiveObject(); - StartFullscreen = new ReactiveObject(); - GameListViewMode = new ReactiveObject(); - ShowNames = new ReactiveObject(); - GridSize = new ReactiveObject(); - ApplicationSort = new ReactiveObject(); - IsAscendingOrder = new ReactiveObject(); - LanguageCode = new ReactiveObject(); - ShowConsole = new ReactiveObject(); - ShowConsole.Event += static (s, e) => { ConsoleHelper.SetConsoleWindowState(e.NewValue); }; - } - } - - /// - /// Logger configuration section - /// - public class LoggerSection - { - /// - /// Enables printing debug log messages - /// - public ReactiveObject EnableDebug { get; private set; } - - /// - /// Enables printing stub log messages - /// - public ReactiveObject EnableStub { get; private set; } - - /// - /// Enables printing info log messages - /// - public ReactiveObject EnableInfo { get; private set; } - - /// - /// Enables printing warning log messages - /// - public ReactiveObject EnableWarn { get; private set; } - - /// - /// Enables printing error log messages - /// - public ReactiveObject EnableError { get; private set; } - - /// - /// Enables printing trace log messages - /// - public ReactiveObject EnableTrace { get; private set; } - - /// - /// Enables printing guest log messages - /// - public ReactiveObject EnableGuest { get; private set; } - - /// - /// Enables printing FS access log messages - /// - public ReactiveObject EnableFsAccessLog { get; private set; } - - /// - /// Controls which log messages are written to the log targets - /// - public ReactiveObject FilteredClasses { get; private set; } - - /// - /// Enables or disables logging to a file on disk - /// - public ReactiveObject EnableFileLog { get; private set; } - - /// - /// Controls which OpenGL log messages are recorded in the log - /// - public ReactiveObject GraphicsDebugLevel { get; private set; } - - public LoggerSection() - { - EnableDebug = new ReactiveObject(); - EnableStub = new ReactiveObject(); - EnableInfo = new ReactiveObject(); - EnableWarn = new ReactiveObject(); - EnableError = new ReactiveObject(); - EnableTrace = new ReactiveObject(); - EnableGuest = new ReactiveObject(); - EnableFsAccessLog = new ReactiveObject(); - FilteredClasses = new ReactiveObject(); - EnableFileLog = new ReactiveObject(); - EnableFileLog.Event += static (sender, e) => LogValueChange(e, nameof(EnableFileLog)); - GraphicsDebugLevel = new ReactiveObject(); - } - } - - /// - /// System configuration section - /// - public class SystemSection - { - /// - /// Change System Language - /// - public ReactiveObject Language { get; private set; } - - /// - /// Change System Region - /// - public ReactiveObject Region { get; private set; } - - /// - /// Change System TimeZone - /// - public ReactiveObject TimeZone { get; private set; } - - /// - /// System Time Offset in Seconds - /// - public ReactiveObject SystemTimeOffset { get; private set; } - - /// - /// Enables or disables Docked Mode - /// - public ReactiveObject EnableDockedMode { get; private set; } - - /// - /// Enables or disables persistent profiled translation cache - /// - public ReactiveObject EnablePtc { get; private set; } - - /// - /// Enables or disables low-power persistent profiled translation cache loading - /// - public ReactiveObject EnableLowPowerPtc { get; private set; } - - /// - /// Enables or disables guest Internet access - /// - public ReactiveObject EnableInternetAccess { get; private set; } - - /// - /// Enables integrity checks on Game content files - /// - public ReactiveObject EnableFsIntegrityChecks { get; private set; } - - /// - /// Enables FS access log output to the console. Possible modes are 0-3 - /// - public ReactiveObject FsGlobalAccessLogMode { get; private set; } - - /// - /// The selected audio backend - /// - public ReactiveObject AudioBackend { get; private set; } - - /// - /// The audio backend volume - /// - public ReactiveObject AudioVolume { get; private set; } - - /// - /// The selected memory manager mode - /// - public ReactiveObject MemoryManagerMode { get; private set; } - - /// - /// Defines the amount of RAM available on the emulated system, and how it is distributed - /// - public ReactiveObject DramSize { get; private set; } - - /// - /// Enable or disable ignoring missing services - /// - public ReactiveObject IgnoreMissingServices { get; private set; } - - /// - /// Uses Hypervisor over JIT if available - /// - public ReactiveObject UseHypervisor { get; private set; } - - public SystemSection() - { - Language = new ReactiveObject(); - Region = new ReactiveObject(); - TimeZone = new ReactiveObject(); - SystemTimeOffset = new ReactiveObject(); - EnableDockedMode = new ReactiveObject(); - EnableDockedMode.Event += static (sender, e) => LogValueChange(e, nameof(EnableDockedMode)); - EnablePtc = new ReactiveObject(); - EnablePtc.Event += static (sender, e) => LogValueChange(e, nameof(EnablePtc)); - EnableLowPowerPtc = new ReactiveObject(); - EnableLowPowerPtc.Event += static (sender, e) => LogValueChange(e, nameof(EnableLowPowerPtc)); - EnableInternetAccess = new ReactiveObject(); - EnableInternetAccess.Event += static (sender, e) => LogValueChange(e, nameof(EnableInternetAccess)); - EnableFsIntegrityChecks = new ReactiveObject(); - EnableFsIntegrityChecks.Event += static (sender, e) => LogValueChange(e, nameof(EnableFsIntegrityChecks)); - FsGlobalAccessLogMode = new ReactiveObject(); - FsGlobalAccessLogMode.Event += static (sender, e) => LogValueChange(e, nameof(FsGlobalAccessLogMode)); - AudioBackend = new ReactiveObject(); - AudioBackend.Event += static (sender, e) => LogValueChange(e, nameof(AudioBackend)); - MemoryManagerMode = new ReactiveObject(); - MemoryManagerMode.Event += static (sender, e) => LogValueChange(e, nameof(MemoryManagerMode)); - DramSize = new ReactiveObject(); - DramSize.Event += static (sender, e) => LogValueChange(e, nameof(DramSize)); - IgnoreMissingServices = new ReactiveObject(); - IgnoreMissingServices.Event += static (sender, e) => LogValueChange(e, nameof(IgnoreMissingServices)); - AudioVolume = new ReactiveObject(); - AudioVolume.Event += static (sender, e) => LogValueChange(e, nameof(AudioVolume)); - UseHypervisor = new ReactiveObject(); - UseHypervisor.Event += static (sender, e) => LogValueChange(e, nameof(UseHypervisor)); - } - } - - /// - /// Hid configuration section - /// - public class HidSection - { - /// - /// Enable or disable keyboard support (Independent from controllers binding) - /// - public ReactiveObject EnableKeyboard { get; private set; } - - /// - /// Enable or disable mouse support (Independent from controllers binding) - /// - public ReactiveObject EnableMouse { get; private set; } - - /// - /// Hotkey Keyboard Bindings - /// - public ReactiveObject Hotkeys { get; private set; } - - /// - /// Input device configuration. - /// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed. - /// TODO: Implement a ReactiveList class. - /// - public ReactiveObject> InputConfig { get; private set; } - - public HidSection() - { - EnableKeyboard = new ReactiveObject(); - EnableMouse = new ReactiveObject(); - Hotkeys = new ReactiveObject(); - InputConfig = new ReactiveObject>(); - } - } - - /// - /// Graphics configuration section - /// - public class GraphicsSection - { - /// - /// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime. - /// - public ReactiveObject BackendThreading { get; private set; } - - /// - /// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide. - /// - public ReactiveObject MaxAnisotropy { get; private set; } - - /// - /// Aspect Ratio applied to the renderer window. - /// - public ReactiveObject AspectRatio { get; private set; } - - /// - /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead. - /// - public ReactiveObject ResScale { get; private set; } - - /// - /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1. - /// - public ReactiveObject ResScaleCustom { get; private set; } - - /// - /// Dumps shaders in this local directory - /// - public ReactiveObject ShadersDumpPath { get; private set; } - - /// - /// Enables or disables Vertical Sync - /// - public ReactiveObject EnableVsync { get; private set; } - - /// - /// Enables or disables Shader cache - /// - public ReactiveObject EnableShaderCache { get; private set; } - - /// - /// Enables or disables texture recompression - /// - public ReactiveObject EnableTextureRecompression { get; private set; } - - /// - /// Enables or disables Macro high-level emulation - /// - public ReactiveObject EnableMacroHLE { get; private set; } - - /// - /// Enables or disables color space passthrough, if available. - /// - public ReactiveObject EnableColorSpacePassthrough { get; private set; } - - /// - /// Graphics backend - /// - public ReactiveObject GraphicsBackend { get; private set; } - - /// - /// Applies anti-aliasing to the renderer. - /// - public ReactiveObject AntiAliasing { get; private set; } - - /// - /// Sets the framebuffer upscaling type. - /// - public ReactiveObject ScalingFilter { get; private set; } - - /// - /// Sets the framebuffer upscaling level. - /// - public ReactiveObject ScalingFilterLevel { get; private set; } - - /// - /// Preferred GPU - /// - public ReactiveObject PreferredGpu { get; private set; } - - public GraphicsSection() - { - BackendThreading = new ReactiveObject(); - BackendThreading.Event += static (_, e) => LogValueChange(e, nameof(BackendThreading)); - ResScale = new ReactiveObject(); - ResScale.Event += static (_, e) => LogValueChange(e, nameof(ResScale)); - ResScaleCustom = new ReactiveObject(); - ResScaleCustom.Event += static (_, e) => LogValueChange(e, nameof(ResScaleCustom)); - MaxAnisotropy = new ReactiveObject(); - MaxAnisotropy.Event += static (_, e) => LogValueChange(e, nameof(MaxAnisotropy)); - AspectRatio = new ReactiveObject(); - AspectRatio.Event += static (_, e) => LogValueChange(e, nameof(AspectRatio)); - ShadersDumpPath = new ReactiveObject(); - EnableVsync = new ReactiveObject(); - EnableVsync.Event += static (_, e) => LogValueChange(e, nameof(EnableVsync)); - EnableShaderCache = new ReactiveObject(); - EnableShaderCache.Event += static (_, e) => LogValueChange(e, nameof(EnableShaderCache)); - EnableTextureRecompression = new ReactiveObject(); - EnableTextureRecompression.Event += static (_, e) => LogValueChange(e, nameof(EnableTextureRecompression)); - GraphicsBackend = new ReactiveObject(); - GraphicsBackend.Event += static (_, e) => LogValueChange(e, nameof(GraphicsBackend)); - PreferredGpu = new ReactiveObject(); - PreferredGpu.Event += static (_, e) => LogValueChange(e, nameof(PreferredGpu)); - EnableMacroHLE = new ReactiveObject(); - EnableMacroHLE.Event += static (_, e) => LogValueChange(e, nameof(EnableMacroHLE)); - EnableColorSpacePassthrough = new ReactiveObject(); - EnableColorSpacePassthrough.Event += static (_, e) => LogValueChange(e, nameof(EnableColorSpacePassthrough)); - AntiAliasing = new ReactiveObject(); - AntiAliasing.Event += static (_, e) => LogValueChange(e, nameof(AntiAliasing)); - ScalingFilter = new ReactiveObject(); - ScalingFilter.Event += static (_, e) => LogValueChange(e, nameof(ScalingFilter)); - ScalingFilterLevel = new ReactiveObject(); - ScalingFilterLevel.Event += static (_, e) => LogValueChange(e, nameof(ScalingFilterLevel)); - } - } - - /// - /// Multiplayer configuration section - /// - public class MultiplayerSection - { - /// - /// GUID for the network interface used by LAN (or 0 for default) - /// - public ReactiveObject LanInterfaceId { get; private set; } - - /// - /// Multiplayer Mode - /// - public ReactiveObject Mode { get; private set; } - - public MultiplayerSection() - { - LanInterfaceId = new ReactiveObject(); - Mode = new ReactiveObject(); - Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode)); - } - } - - /// - /// The default configuration instance - /// - public static ConfigurationState Instance { get; private set; } - - /// - /// The UI section - /// - public UISection UI { get; private set; } - - /// - /// The Logger section - /// - public LoggerSection Logger { get; private set; } - - /// - /// The System section - /// - public SystemSection System { get; private set; } - - /// - /// The Graphics section - /// - public GraphicsSection Graphics { get; private set; } - - /// - /// The Hid section - /// - public HidSection Hid { get; private set; } - - /// - /// The Multiplayer section - /// - public MultiplayerSection Multiplayer { get; private set; } - - /// - /// Enables or disables Discord Rich Presence - /// - public ReactiveObject EnableDiscordIntegration { get; private set; } - - /// - /// Checks for updates when Ryujinx starts when enabled - /// - public ReactiveObject CheckUpdatesOnStart { get; private set; } - - /// - /// Show "Confirm Exit" Dialog - /// - public ReactiveObject ShowConfirmExit { get; private set; } - - /// - /// Ignore Applet - /// - public ReactiveObject IgnoreApplet { get; private set; } - - /// - /// Enables or disables save window size, position and state on close. - /// - public ReactiveObject RememberWindowState { get; private set; } - - /// - /// Enables or disables the redesigned title bar - /// - public ReactiveObject ShowTitleBar { get; private set; } - - /// - /// Enables hardware-accelerated rendering for Avalonia - /// - public ReactiveObject EnableHardwareAcceleration { get; private set; } - - /// - /// Hide Cursor on Idle - /// - public ReactiveObject HideCursor { get; private set; } - - private ConfigurationState() - { - UI = new UISection(); - Logger = new LoggerSection(); - System = new SystemSection(); - Graphics = new GraphicsSection(); - Hid = new HidSection(); - Multiplayer = new MultiplayerSection(); - EnableDiscordIntegration = new ReactiveObject(); - CheckUpdatesOnStart = new ReactiveObject(); - ShowConfirmExit = new ReactiveObject(); - IgnoreApplet = new ReactiveObject(); - RememberWindowState = new ReactiveObject(); - ShowTitleBar = new ReactiveObject(); - EnableHardwareAcceleration = new ReactiveObject(); - HideCursor = new ReactiveObject(); - } public ConfigurationFileFormat ToFileFormat() { @@ -734,6 +87,7 @@ namespace Ryujinx.UI.Common.Configuration AppColumn = UI.GuiColumns.AppColumn, DevColumn = UI.GuiColumns.DevColumn, VersionColumn = UI.GuiColumns.VersionColumn, + LdnInfoColumn = UI.GuiColumns.LdnInfoColumn, TimePlayedColumn = UI.GuiColumns.TimePlayedColumn, LastPlayedColumn = UI.GuiColumns.LastPlayedColumn, FileExtColumn = UI.GuiColumns.FileExtColumn, @@ -783,6 +137,9 @@ namespace Ryujinx.UI.Common.Configuration PreferredGpu = Graphics.PreferredGpu, MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId, MultiplayerMode = Multiplayer.Mode, + MultiplayerDisableP2p = Multiplayer.DisableP2p, + MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, + LdnServer = Multiplayer.LdnServer, }; return configurationFile; @@ -842,6 +199,9 @@ namespace Ryujinx.UI.Common.Configuration System.UseHypervisor.Value = true; Multiplayer.LanInterfaceId.Value = "0"; Multiplayer.Mode.Value = MultiplayerMode.Disabled; + Multiplayer.DisableP2p.Value = false; + Multiplayer.LdnPassphrase.Value = ""; + Multiplayer.LdnServer.Value = ""; UI.GuiColumns.FavColumn.Value = true; UI.GuiColumns.IconColumn.Value = true; UI.GuiColumns.AppColumn.Value = true; @@ -940,708 +300,9 @@ namespace Ryujinx.UI.Common.Configuration StickButton = Key.H, }, } - ]; } - public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) - { - bool configurationFileUpdated = false; - - if (configurationFileFormat.Version is < 0 or > ConfigurationFileFormat.CurrentVersion) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); - - LoadDefault(); - } - - if (configurationFileFormat.Version < 2) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); - - configurationFileFormat.SystemRegion = Region.USA; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 3) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 3."); - - configurationFileFormat.SystemTimeZone = "UTC"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 4) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 4."); - - configurationFileFormat.MaxAnisotropy = -1; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 5) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 5."); - - configurationFileFormat.SystemTimeOffset = 0; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 8) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8."); - - configurationFileFormat.EnablePtc = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 9) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 9."); - - configurationFileFormat.ColumnSort = new ColumnSort - { - SortColumnId = 0, - SortAscending = false, - }; - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 10) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 10."); - - configurationFileFormat.AudioBackend = AudioBackend.OpenAl; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 11) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11."); - - configurationFileFormat.ResScale = 1; - configurationFileFormat.ResScaleCustom = 1.0f; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 12) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 12."); - - configurationFileFormat.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None; - - configurationFileUpdated = true; - } - - // configurationFileFormat.Version == 13 -> LDN1 - - if (configurationFileFormat.Version < 14) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14."); - - configurationFileFormat.CheckUpdatesOnStart = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 16) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 16."); - - configurationFileFormat.EnableShaderCache = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 17) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 17."); - - configurationFileFormat.StartFullscreen = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 18) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 18."); - - configurationFileFormat.AspectRatio = AspectRatio.Fixed16x9; - - configurationFileUpdated = true; - } - - // configurationFileFormat.Version == 19 -> LDN2 - - if (configurationFileFormat.Version < 20) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20."); - - configurationFileFormat.ShowConfirmExit = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 21) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21."); - - // Initialize network config. - - configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled; - configurationFileFormat.MultiplayerLanInterfaceId = "0"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 22) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22."); - - configurationFileFormat.HideCursor = HideCursorMode.Never; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 24) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24."); - - configurationFileFormat.InputConfig = new List - { - new StandardKeyboardInputConfig - { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.WindowKeyboard, - Id = "0", - PlayerIndex = PlayerIndex.Player1, - ControllerType = ControllerType.ProController, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = Key.Up, - DpadDown = Key.Down, - DpadLeft = Key.Left, - DpadRight = Key.Right, - ButtonMinus = Key.Minus, - ButtonL = Key.E, - ButtonZl = Key.Q, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound, - }, - LeftJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = Key.W, - StickDown = Key.S, - StickLeft = Key.A, - StickRight = Key.D, - StickButton = Key.F, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = Key.Z, - ButtonB = Key.X, - ButtonX = Key.C, - ButtonY = Key.V, - ButtonPlus = Key.Plus, - ButtonR = Key.U, - ButtonZr = Key.O, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound, - }, - RightJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = Key.I, - StickDown = Key.K, - StickLeft = Key.J, - StickRight = Key.L, - StickButton = Key.H, - }, - }, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 25) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25."); - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 26) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26."); - - configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 27) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27."); - - configurationFileFormat.EnableMouse = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 28) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - Screenshot = Key.F8, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 29) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - Screenshot = Key.F8, - ShowUI = Key.F4, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 30) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30."); - - foreach (InputConfig config in configurationFileFormat.InputConfig) - { - if (config is StandardControllerInputConfig controllerConfig) - { - controllerConfig.Rumble = new RumbleConfigController - { - EnableRumble = false, - StrongRumble = 1f, - WeakRumble = 1f, - }; - } - } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 31) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31."); - - configurationFileFormat.BackendThreading = BackendThreading.Auto; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 32) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = Key.F5, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 33) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = Key.F2, - }; - - configurationFileFormat.AudioVolume = 1; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 34) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34."); - - configurationFileFormat.EnableInternetAccess = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 35) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35."); - - foreach (InputConfig config in configurationFileFormat.InputConfig) - { - if (config is StandardControllerInputConfig controllerConfig) - { - controllerConfig.RangeLeft = 1.0f; - controllerConfig.RangeRight = 1.0f; - } - } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 36) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36."); - - configurationFileFormat.LoggingEnableTrace = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 37) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37."); - - configurationFileFormat.ShowConsole = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 38) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38."); - - configurationFileFormat.BaseStyle = "Dark"; - configurationFileFormat.GameListViewMode = 0; - configurationFileFormat.ShowNames = true; - configurationFileFormat.GridSize = 2; - configurationFileFormat.LanguageCode = "en_US"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 39) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, - ResScaleUp = Key.Unbound, - ResScaleDown = Key.Unbound, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 40) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40."); - - configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 41) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, - ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, - ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, - VolumeUp = Key.Unbound, - VolumeDown = Key.Unbound, - }; - } - - if (configurationFileFormat.Version < 42) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42."); - - configurationFileFormat.EnableMacroHLE = true; - } - - if (configurationFileFormat.Version < 43) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43."); - - configurationFileFormat.UseHypervisor = true; - } - - if (configurationFileFormat.Version < 44) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44."); - - configurationFileFormat.AntiAliasing = AntiAliasing.None; - configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear; - configurationFileFormat.ScalingFilterLevel = 80; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 45) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45."); - - configurationFileFormat.ShownFileTypes = new ShownFileTypes - { - NSP = true, - PFS0 = true, - XCI = true, - NCA = true, - NRO = true, - NSO = true, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 46) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46."); - - configurationFileFormat.MultiplayerLanInterfaceId = "0"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 47) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47."); - - configurationFileFormat.WindowStartup = new WindowStartup - { - WindowPositionX = 0, - WindowPositionY = 0, - WindowSizeHeight = 760, - WindowSizeWidth = 1280, - WindowMaximized = false, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 48) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48."); - - configurationFileFormat.EnableColorSpacePassthrough = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 49) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49."); - - if (OperatingSystem.IsMacOS()) - { - AppDataManager.FixMacOSConfigurationFolders(); - } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 50) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 50."); - - configurationFileFormat.EnableHardwareAcceleration = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 51) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51."); - - configurationFileFormat.RememberWindowState = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 52) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); - - configurationFileFormat.AutoloadDirs = []; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 53) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53."); - - configurationFileFormat.EnableLowPowerPtc = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 54) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 54."); - - configurationFileFormat.DramSize = MemoryConfiguration.MemoryConfiguration4GiB; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 55) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 55."); - - configurationFileFormat.IgnoreApplet = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 56) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 56."); - - configurationFileFormat.ShowTitleBar = !OperatingSystem.IsWindows(); - - configurationFileUpdated = true; - } - - Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; - Graphics.ResScale.Value = configurationFileFormat.ResScale; - Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; - Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; - Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; - Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; - Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; - Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; - Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; - Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing; - Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter; - Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel; - Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug; - Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub; - Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo; - Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn; - Logger.EnableError.Value = configurationFileFormat.LoggingEnableError; - Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace; - Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest; - Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog; - Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses; - Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel; - System.Language.Value = configurationFileFormat.SystemLanguage; - System.Region.Value = configurationFileFormat.SystemRegion; - System.TimeZone.Value = configurationFileFormat.SystemTimeZone; - System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset; - System.EnableDockedMode.Value = configurationFileFormat.DockedMode; - EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; - CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; - ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; - IgnoreApplet.Value = configurationFileFormat.IgnoreApplet; - RememberWindowState.Value = configurationFileFormat.RememberWindowState; - ShowTitleBar.Value = configurationFileFormat.ShowTitleBar; - EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; - HideCursor.Value = configurationFileFormat.HideCursor; - Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; - Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; - Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; - Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; - Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough; - System.EnablePtc.Value = configurationFileFormat.EnablePtc; - System.EnableLowPowerPtc.Value = configurationFileFormat.EnableLowPowerPtc; - System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess; - System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks; - System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode; - System.AudioBackend.Value = configurationFileFormat.AudioBackend; - System.AudioVolume.Value = configurationFileFormat.AudioVolume; - System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode; - System.DramSize.Value = configurationFileFormat.DramSize; - System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices; - System.UseHypervisor.Value = configurationFileFormat.UseHypervisor; - UI.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn; - UI.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn; - UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn; - UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn; - UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn; - UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn; - UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn; - UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn; - UI.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn; - UI.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn; - UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId; - UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending; - UI.GameDirs.Value = configurationFileFormat.GameDirs; - UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs ?? []; - UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP; - UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0; - UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI; - UI.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA; - UI.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO; - UI.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO; - UI.LanguageCode.Value = configurationFileFormat.LanguageCode; - UI.BaseStyle.Value = configurationFileFormat.BaseStyle; - UI.GameListViewMode.Value = configurationFileFormat.GameListViewMode; - UI.ShowNames.Value = configurationFileFormat.ShowNames; - UI.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder; - UI.GridSize.Value = configurationFileFormat.GridSize; - UI.ApplicationSort.Value = configurationFileFormat.ApplicationSort; - UI.StartFullscreen.Value = configurationFileFormat.StartFullscreen; - UI.ShowConsole.Value = configurationFileFormat.ShowConsole; - UI.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth; - UI.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight; - UI.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX; - UI.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY; - UI.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized; - Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; - Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; - Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; - Hid.InputConfig.Value = configurationFileFormat.InputConfig ?? []; - - Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId; - Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode; - - if (configurationFileUpdated) - { - ToFileFormat().SaveConfig(configurationFilePath); - - Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); - } - } - private static GraphicsBackend DefaultGraphicsBackend() { // Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan. @@ -1653,24 +314,5 @@ namespace Ryujinx.UI.Common.Configuration return GraphicsBackend.OpenGl; } - - private static void LogValueChange(ReactiveEventArgs eventArgs, string valueName) - { - string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}"); - - Ryujinx.Common.Logging.Logger.Info?.Print(LogClass.Configuration, message); - } - - public static void Initialize() - { - if (Instance != null) - { - throw new InvalidOperationException("Configuration is already initialized"); } - - Instance = new ConfigurationState(); - - Instance.System.EnableLowPowerPtc.Event += (_, evnt) => Optimizations.LowPower = evnt.NewValue; } - } -} diff --git a/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs b/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs index 9cb283593..a7913f142 100644 --- a/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs +++ b/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs @@ -1,4 +1,3 @@ -using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Logging.Targets; @@ -11,103 +10,69 @@ namespace Ryujinx.UI.Common.Configuration { public static void Initialize() { - ConfigurationState.Instance.Logger.EnableDebug.Event += ReloadEnableDebug; - ConfigurationState.Instance.Logger.EnableStub.Event += ReloadEnableStub; - ConfigurationState.Instance.Logger.EnableInfo.Event += ReloadEnableInfo; - ConfigurationState.Instance.Logger.EnableWarn.Event += ReloadEnableWarning; - ConfigurationState.Instance.Logger.EnableError.Event += ReloadEnableError; - ConfigurationState.Instance.Logger.EnableTrace.Event += ReloadEnableTrace; - ConfigurationState.Instance.Logger.EnableGuest.Event += ReloadEnableGuest; - ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += ReloadEnableFsAccessLog; - ConfigurationState.Instance.Logger.FilteredClasses.Event += ReloadFilteredClasses; - ConfigurationState.Instance.Logger.EnableFileLog.Event += ReloadFileLogger; - } - - private static void ReloadEnableDebug(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Debug, e.NewValue); - } - - private static void ReloadEnableStub(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Stub, e.NewValue); - } - - private static void ReloadEnableInfo(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Info, e.NewValue); - } - - private static void ReloadEnableWarning(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Warning, e.NewValue); - } - - private static void ReloadEnableError(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Error, e.NewValue); - } - - private static void ReloadEnableTrace(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Trace, e.NewValue); - } - - private static void ReloadEnableGuest(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Guest, e.NewValue); - } - - private static void ReloadEnableFsAccessLog(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.AccessLog, e.NewValue); - } - - private static void ReloadFilteredClasses(object sender, ReactiveEventArgs e) - { - bool noFilter = e.NewValue.Length == 0; - - foreach (var logClass in Enum.GetValues()) + ConfigurationState.Instance.Logger.EnableDebug.Event += + (_, e) => Logger.SetEnable(LogLevel.Debug, e.NewValue); + ConfigurationState.Instance.Logger.EnableStub.Event += + (_, e) => Logger.SetEnable(LogLevel.Stub, e.NewValue); + ConfigurationState.Instance.Logger.EnableInfo.Event += + (_, e) => Logger.SetEnable(LogLevel.Info, e.NewValue); + ConfigurationState.Instance.Logger.EnableWarn.Event += + (_, e) => Logger.SetEnable(LogLevel.Warning, e.NewValue); + ConfigurationState.Instance.Logger.EnableError.Event += + (_, e) => Logger.SetEnable(LogLevel.Error, e.NewValue); + ConfigurationState.Instance.Logger.EnableTrace.Event += + (_, e) => Logger.SetEnable(LogLevel.Trace, e.NewValue); + ConfigurationState.Instance.Logger.EnableGuest.Event += + (_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue); + ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += + (_, e) => Logger.SetEnable(LogLevel.AccessLog, e.NewValue); + + ConfigurationState.Instance.Logger.FilteredClasses.Event += (_, e) => { - Logger.SetEnable(logClass, noFilter); - } + bool noFilter = e.NewValue.Length == 0; - foreach (var logClass in e.NewValue) - { - Logger.SetEnable(logClass, true); - } - } - - private static void ReloadFileLogger(object sender, ReactiveEventArgs e) - { - if (e.NewValue) - { - string logDir = AppDataManager.LogsDirPath; - FileStream logFile = null; - - if (!string.IsNullOrEmpty(logDir)) + foreach (var logClass in Enum.GetValues()) { - logFile = FileLogTarget.PrepareLogFile(logDir); + Logger.SetEnable(logClass, noFilter); } - if (logFile == null) + foreach (var logClass in e.NewValue) + { + Logger.SetEnable(logClass, true); + } + }; + + ConfigurationState.Instance.Logger.EnableFileLog.Event += (_, e) => + { + if (e.NewValue) + { + string logDir = AppDataManager.LogsDirPath; + FileStream logFile = null; + + if (!string.IsNullOrEmpty(logDir)) + { + logFile = FileLogTarget.PrepareLogFile(logDir); + } + + if (logFile == null) + { + Logger.Error?.Print(LogClass.Application, + "No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable."); + Logger.RemoveTarget("file"); + + return; + } + + Logger.AddTarget(new AsyncLogTargetWrapper( + new FileLogTarget("file", logFile), + 1000 + )); + } + else { - Logger.Error?.Print(LogClass.Application, "No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable."); Logger.RemoveTarget("file"); - - return; } - - Logger.AddTarget(new AsyncLogTargetWrapper( - new FileLogTarget("file", logFile), - 1000, - AsyncLogTargetOverflowAction.Block - )); - } - else - { - Logger.RemoveTarget("file"); - } + }; } } } diff --git a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs index c778ef1f1..c486492e0 100644 --- a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs +++ b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs @@ -7,6 +7,7 @@ namespace Ryujinx.UI.Common.Configuration.UI public bool AppColumn { get; set; } public bool DevColumn { get; set; } public bool VersionColumn { get; set; } + public bool LdnInfoColumn { get; set; } public bool TimePlayedColumn { get; set; } public bool LastPlayedColumn { get; set; } public bool FileExtColumn { get; set; } diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs index 7bfb1c95b..d4b2a4187 100644 --- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs +++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs @@ -1,11 +1,10 @@ using DiscordRPC; using Humanizer; -using LibHac.Bcat; +using Humanizer.Localisation; using Ryujinx.Common; using Ryujinx.HLE.Loaders.Processes; using Ryujinx.UI.App.Common; using Ryujinx.UI.Common.Configuration; -using System.Collections.Generic; using System.Linq; using System.Text; @@ -15,9 +14,13 @@ namespace Ryujinx.UI.Common { public static Timestamps StartedAt { get; set; } - private static readonly string _description = ReleaseInformation.IsValid - ? $"v{ReleaseInformation.Version} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}@{ReleaseInformation.BuildGitHash}" - : "dev build"; + private static string VersionString + => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}"; + + private static readonly string _description = + ReleaseInformation.IsValid + ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}" + : "dev build"; private const string ApplicationId = "1293250299716173864"; @@ -74,13 +77,13 @@ namespace Ryujinx.UI.Common Assets = new Assets { LargeImageKey = _discordGameAssetKeys.Contains(procRes.ProgramIdText) ? procRes.ProgramIdText : "game", - LargeImageText = TruncateToByteLength($"{appMeta.Title} | {procRes.DisplayVersion}"), + LargeImageText = TruncateToByteLength($"{appMeta.Title} (v{procRes.DisplayVersion})"), SmallImageKey = "ryujinx", SmallImageText = TruncateToByteLength(_description) }, Details = TruncateToByteLength($"Playing {appMeta.Title}"), State = appMeta.LastPlayed.HasValue && appMeta.TimePlayed.TotalSeconds > 5 - ? $"Total play time: {appMeta.TimePlayed.Humanize(2, false)}" + ? $"Total play time: {appMeta.TimePlayed.Humanize(2, false, maxUnit: TimeUnit.Hour)}" : "Never played", Timestamps = Timestamps.Now }); @@ -163,6 +166,7 @@ namespace Ryujinx.UI.Common "010036b0034e4000", // Super Mario Party "01006fe013472000", // Mario Party Superstars "0100965017338000", // Super Mario Party Jamboree + "01006d0017f7a000", // Mario & Luigi: Brothership "010067300059a000", // Mario + Rabbids: Kingdom Battle "0100317013770000", // Mario + Rabbids: Sparks of Hope "0100a3900c3e2000", // Paper Mario: The Origami King diff --git a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs index 93b2d6138..be10deca0 100644 --- a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs @@ -4,6 +4,7 @@ using Ryujinx.Common.Logging; using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -23,6 +24,26 @@ namespace Ryujinx.UI.Common.Helper public static partial void SHChangeNotify(uint wEventId, uint uFlags, nint dwItem1, nint dwItem2); public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()) && !ReleaseInformation.IsFlatHubBuild; + + public static bool AreMimeTypesRegistered + { + get + { + if (OperatingSystem.IsLinux()) + { + return AreMimeTypesRegisteredLinux(); + } + + if (OperatingSystem.IsWindows()) + { + return AreMimeTypesRegisteredWindows(); + } + + // TODO: Add macOS support. + + return false; + } + } [SupportedOSPlatform("linux")] private static bool AreMimeTypesRegisteredLinux() => File.Exists(Path.Combine(_mimeDbPath, "packages", "Ryujinx.xml")); @@ -72,6 +93,10 @@ namespace Ryujinx.UI.Common.Helper [SupportedOSPlatform("windows")] private static bool AreMimeTypesRegisteredWindows() { + return _fileExtensions.Aggregate(false, + (current, ext) => current | CheckRegistering(ext) + ); + static bool CheckRegistering(string ext) { RegistryKey key = Registry.CurrentUser.OpenSubKey(@$"Software\Classes\{ext}"); @@ -87,20 +112,20 @@ namespace Ryujinx.UI.Common.Helper return keyValue is not null && (keyValue.Contains("Ryujinx") || keyValue.Contains(AppDomain.CurrentDomain.FriendlyName)); } - - bool registered = false; - - foreach (string ext in _fileExtensions) - { - registered |= CheckRegistering(ext); - } - - return registered; } [SupportedOSPlatform("windows")] private static bool InstallWindowsMimeTypes(bool uninstall = false) { + bool registered = _fileExtensions.Aggregate(false, + (current, ext) => current | RegisterExtension(ext, uninstall) + ); + + // Notify Explorer the file association has been changed. + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, nint.Zero, nint.Zero); + + return registered; + static bool RegisterExtension(string ext, bool uninstall = false) { string keyString = @$"Software\Classes\{ext}"; @@ -127,42 +152,13 @@ namespace Ryujinx.UI.Common.Helper Logger.Debug?.Print(LogClass.Application, $"Adding type association {ext}"); using var openCmd = key.CreateSubKey(@"shell\open\command"); - openCmd.SetValue("", $"\"{Environment.ProcessPath}\" \"%1\""); + openCmd.SetValue(string.Empty, $"\"{Environment.ProcessPath}\" \"%1\""); Logger.Debug?.Print(LogClass.Application, $"Added type association {ext}"); } return true; } - - bool registered = false; - - foreach (string ext in _fileExtensions) - { - registered |= RegisterExtension(ext, uninstall); - } - - // Notify Explorer the file association has been changed. - SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, nint.Zero, nint.Zero); - - return registered; - } - - public static bool AreMimeTypesRegistered() - { - if (OperatingSystem.IsLinux()) - { - return AreMimeTypesRegisteredLinux(); - } - - if (OperatingSystem.IsWindows()) - { - return AreMimeTypesRegisteredWindows(); - } - - // TODO: Add macOS support. - - return false; } public static bool Install() diff --git a/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs b/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs index 05fa82920..95fb3985b 100644 --- a/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs +++ b/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs @@ -43,8 +43,8 @@ namespace Ryujinx.UI.Common.Models { if (obj == null) return false; - else - return this.Path == obj.Path; + + return this.Path == obj.Path; } public override int GetHashCode() diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index dc4f4ff36..7246be4b9 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -207,6 +207,9 @@ namespace Ryujinx.Ava ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState; + ConfigurationState.Instance.Multiplayer.LdnPassphrase.Event += UpdateLdnPassphraseState; + ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState; + ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState; _gpuCancellationTokenSource = new CancellationTokenSource(); _gpuDoneEvent = new ManualResetEvent(false); @@ -491,6 +494,21 @@ namespace Ryujinx.Ava Device.Configuration.MultiplayerMode = e.NewValue; } + private void UpdateLdnPassphraseState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerLdnPassphrase = e.NewValue; + } + + private void UpdateLdnServerState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerLdnServer = e.NewValue; + } + + private void UpdateDisableP2pState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerDisableP2p = e.NewValue; + } + public void ToggleVSync() { Device.EnableDeviceVsync = !Device.EnableDeviceVsync; @@ -863,10 +881,11 @@ namespace Ryujinx.Ava ConfigurationState.Instance.Graphics.AspectRatio, ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, - ConfigurationState.Instance.Multiplayer.LanInterfaceId, - ConfigurationState.Instance.Multiplayer.Mode - ) - ); + ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Multiplayer.DisableP2p, + ConfigurationState.Instance.Multiplayer.LdnPassphrase, + ConfigurationState.Instance.Multiplayer.LdnServer)); } private static IHardwareDeviceDriver InitializeAudio() @@ -1050,7 +1069,7 @@ namespace Ryujinx.Ava string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld]; UpdateShaderCount(); - + if (GraphicsConfig.ResScale != 1) { dockedMode += $" ({GraphicsConfig.ResScale}x)"; diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json index f2720b669..495ab4b4d 100644 --- a/src/Ryujinx/Assets/Locales/ar_SA.json +++ b/src/Ryujinx/Assets/Locales/ar_SA.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "تعيين لون الخلفية", "AvatarClose": "إغلاق", "ControllerSettingsLoadProfileToolTip": "تحميل الملف الشخصي", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "إضافة ملف شخصي", "ControllerSettingsRemoveProfileToolTip": "إزالة الملف الشخصي", "ControllerSettingsSaveProfileToolTip": "حفظ الملف الشخصي", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "حدث خطأ أثناء البحث عن بيانات الحفظ المحددة: {0}", "FolderDialogExtractTitle": "اختر المجلد الذي تريد الاستخراج إليه", "DialogNcaExtractionMessage": "استخراج قسم {0} من {1}...", - "DialogNcaExtractionTitle": "ريوجينكس - مستخرج قسم NCA", + "DialogNcaExtractionTitle": "مستخرج قسم NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "فشل الاستخراج. لم يكن NCA الرئيسي موجودا في الملف المحدد.", "DialogNcaExtractionCheckLogErrorMessage": "فشل الاستخراج. اقرأ ملف التسجيل لمزيد من المعلومات.", "DialogNcaExtractionSuccessMessage": "تم الاستخراج بنجاح.", diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json index 4004ec325..e23f3b619 100644 --- a/src/Ryujinx/Assets/Locales/de_DE.json +++ b/src/Ryujinx/Assets/Locales/de_DE.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Hintergrundfarbe auswählen", "AvatarClose": "Schließen", "ControllerSettingsLoadProfileToolTip": "Lädt ein Profil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Fügt ein Profil hinzu", "ControllerSettingsRemoveProfileToolTip": "Entfernt ein Profil", "ControllerSettingsSaveProfileToolTip": "Speichert ein Profil", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "Es ist ein Fehler beim Suchen der angegebenen Speicherdaten aufgetreten: {0}", "FolderDialogExtractTitle": "Wähle den Ordner, in welchen die Dateien entpackt werden sollen", "DialogNcaExtractionMessage": "Extrahiert {0} abschnitt von {1}...", - "DialogNcaExtractionTitle": "Ryujinx - NCA-Abschnitt-Extraktor", + "DialogNcaExtractionTitle": "NCA-Abschnitt-Extraktor", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Extraktion fehlgeschlagen. Der Hauptheader der NCA war in der ausgewählten Datei nicht vorhanden.", "DialogNcaExtractionCheckLogErrorMessage": "Extraktion fehlgeschlagen. Überprüfe die Logs für weitere Informationen.", "DialogNcaExtractionSuccessMessage": "Extraktion erfolgreich abgeschlossen.", diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json index 56940ffc9..79975b892 100644 --- a/src/Ryujinx/Assets/Locales/el_GR.json +++ b/src/Ryujinx/Assets/Locales/el_GR.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Ορισμός Χρώματος Φόντου", "AvatarClose": "Κλείσιμο", "ControllerSettingsLoadProfileToolTip": "Φόρτωση Προφίλ", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Προσθήκη Προφίλ", "ControllerSettingsRemoveProfileToolTip": "Κατάργηση Προφίλ", "ControllerSettingsSaveProfileToolTip": "Αποθήκευση Προφίλ", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "Σφάλμα κατά την εύρεση των αποθηκευμένων δεδομένων: {0}", "FolderDialogExtractTitle": "Επιλέξτε τον φάκελο στον οποίο θέλετε να εξαγάγετε", "DialogNcaExtractionMessage": "Εξαγωγή ενότητας {0} από {1}...", - "DialogNcaExtractionTitle": "Ryujinx - NCA Εξαγωγέας Τμημάτων", + "DialogNcaExtractionTitle": "NCA Εξαγωγέας Τμημάτων", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Αποτυχία εξαγωγής. Η κύρια NCA δεν υπήρχε στο επιλεγμένο αρχείο.", "DialogNcaExtractionCheckLogErrorMessage": "Αποτυχία εξαγωγής. Διαβάστε το αρχείο καταγραφής για περισσότερες πληροφορίες.", "DialogNcaExtractionSuccessMessage": "Η εξαγωγή ολοκληρώθηκε με επιτυχία.", diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 30c84ab14..fdd2d4df2 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -444,7 +444,7 @@ "DialogMessageFindSaveErrorMessage": "There was an error finding the specified savedata: {0}", "FolderDialogExtractTitle": "Choose the folder to extract into", "DialogNcaExtractionMessage": "Extracting {0} section from {1}...", - "DialogNcaExtractionTitle": "Ryujinx - NCA Section Extractor", + "DialogNcaExtractionTitle": "NCA Section Extractor", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Extraction failure. The main NCA was not present in the selected file.", "DialogNcaExtractionCheckLogErrorMessage": "Extraction failed. Please check the log file for more details.", "DialogNcaExtractionSuccessMessage": "Extraction completed successfully.", @@ -461,7 +461,7 @@ "DialogUpdaterRestartMessage": "Do you want to restart Ryujinx now?", "DialogUpdaterNoInternetMessage": "You are not connected to the Internet!", "DialogUpdaterNoInternetSubMessage": "Please verify that you have a working Internet connection!", - "DialogUpdaterDirtyBuildMessage": "You Cannot update a Dirty build of Ryujinx!", + "DialogUpdaterDirtyBuildMessage": "You cannot update a Dirty build of Ryujinx!", "DialogUpdaterDirtyBuildSubMessage": "Please download Ryujinx at https://github.com/GreemDev/Ryujinx/releases/ if you are looking for a supported version.", "DialogRestartRequiredMessage": "Restart Required", "DialogThemeRestartMessage": "Theme has been saved. A restart is needed to apply the theme.", @@ -848,5 +848,17 @@ "MultiplayerMode": "Mode:", "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", "MultiplayerModeDisabled": "Disabled", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json index bbc86ed0c..e6da1d113 100644 --- a/src/Ryujinx/Assets/Locales/es_ES.json +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Establecer color de fondo", "AvatarClose": "Cerrar", "ControllerSettingsLoadProfileToolTip": "Cargar perfil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Agregar perfil", "ControllerSettingsRemoveProfileToolTip": "Eliminar perfil", "ControllerSettingsSaveProfileToolTip": "Guardar perfil", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "Hubo un error encontrando los datos de guardado especificados: {0}", "FolderDialogExtractTitle": "Elige la carpeta en la que deseas extraer", "DialogNcaExtractionMessage": "Extrayendo {0} sección de {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Extractor de sección NCA", + "DialogNcaExtractionTitle": "Extractor de sección NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Fallo de extracción. El NCA principal no estaba presente en el archivo seleccionado.", "DialogNcaExtractionCheckLogErrorMessage": "Fallo de extracción. Lee el registro para más información.", "DialogNcaExtractionSuccessMessage": "Se completó la extracción con éxito.", diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json index 0c673f719..e52333cea 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -408,6 +408,7 @@ "AvatarClose": "Fermer", "ControllerSettingsLoadProfileToolTip": "Charger un profil", "ControllerSettingsAddProfileToolTip": "Ajouter un profil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsRemoveProfileToolTip": "Supprimer un profil", "ControllerSettingsSaveProfileToolTip": "Enregistrer un profil", "MenuBarFileToolsTakeScreenshot": "Prendre une capture d'écran", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "Une erreur s'est produite lors de la recherche de la sauvegarde spécifiée : {0}", "FolderDialogExtractTitle": "Choisissez le dossier dans lequel extraire", "DialogNcaExtractionMessage": "Extraction de la section {0} depuis {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Extracteur de la section NCA", + "DialogNcaExtractionTitle": "Extracteur de la section NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Échec de l'extraction. Le NCA principal n'était pas présent dans le fichier sélectionné.", "DialogNcaExtractionCheckLogErrorMessage": "Échec de l'extraction. Lisez le fichier journal pour plus d'informations.", "DialogNcaExtractionSuccessMessage": "Extraction terminée avec succès.", diff --git a/src/Ryujinx/Assets/Locales/he_IL.json b/src/Ryujinx/Assets/Locales/he_IL.json index cac3fbf53..d2bd21124 100644 --- a/src/Ryujinx/Assets/Locales/he_IL.json +++ b/src/Ryujinx/Assets/Locales/he_IL.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "הגדר צבע רקע", "AvatarClose": "סגור", "ControllerSettingsLoadProfileToolTip": "טען פרופיל", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "הוסף פרופיל", "ControllerSettingsRemoveProfileToolTip": "הסר פרופיל", "ControllerSettingsSaveProfileToolTip": "שמור פרופיל", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "אירעה שגיאה במציאת שמור המשחק שצויין: {0}", "FolderDialogExtractTitle": "בחרו את התיקייה לחילוץ", "DialogNcaExtractionMessage": "מלחץ {0} ממקטע {1}...", - "DialogNcaExtractionTitle": "ריוג'ינקס - מחלץ מקטע NCA", + "DialogNcaExtractionTitle": "מחלץ מקטע NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "כשל בחילוץ. ה-NCA הראשי לא היה קיים בקובץ שנבחר.", "DialogNcaExtractionCheckLogErrorMessage": "כשל בחילוץ. קרא את קובץ הרישום למידע נוסף.", "DialogNcaExtractionSuccessMessage": "החילוץ הושלם בהצלחה.", diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json index 419a5dd2b..ee2bd3a16 100644 --- a/src/Ryujinx/Assets/Locales/it_IT.json +++ b/src/Ryujinx/Assets/Locales/it_IT.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Imposta colore di sfondo", "AvatarClose": "Chiudi", "ControllerSettingsLoadProfileToolTip": "Carica profilo", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Aggiungi profilo", "ControllerSettingsRemoveProfileToolTip": "Rimuovi profilo", "ControllerSettingsSaveProfileToolTip": "Salva profilo", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "C'è stato un errore durante la ricerca dei dati di salvataggio: {0}", "FolderDialogExtractTitle": "Scegli una cartella in cui estrarre", "DialogNcaExtractionMessage": "Estrazione della sezione {0} da {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Estrazione sezione NCA", + "DialogNcaExtractionTitle": "Estrazione sezione NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "L'estrazione è fallita. L'NCA principale non era presente nel file selezionato.", "DialogNcaExtractionCheckLogErrorMessage": "L'estrazione è fallita. Consulta il file di log per maggiori informazioni.", "DialogNcaExtractionSuccessMessage": "Estrazione completata con successo.", diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json index b2c0a506e..8d15ab678 100644 --- a/src/Ryujinx/Assets/Locales/ja_JP.json +++ b/src/Ryujinx/Assets/Locales/ja_JP.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "背景色を指定", "AvatarClose": "閉じる", "ControllerSettingsLoadProfileToolTip": "プロファイルをロード", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "プロファイルを追加", "ControllerSettingsRemoveProfileToolTip": "プロファイルを削除", "ControllerSettingsSaveProfileToolTip": "プロファイルをセーブ", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "セーブデータ: {0} の検索中にエラーが発生しました", "FolderDialogExtractTitle": "展開フォルダを選択", "DialogNcaExtractionMessage": "{1} から {0} セクションを展開中...", - "DialogNcaExtractionTitle": "Ryujinx - NCA セクション展開", + "DialogNcaExtractionTitle": "NCA セクション展開", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "展開に失敗しました. 選択されたファイルにはメイン NCA が存在しません.", "DialogNcaExtractionCheckLogErrorMessage": "展開に失敗しました. 詳細はログを確認してください.", "DialogNcaExtractionSuccessMessage": "展開が正常終了しました", diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json index bce762d19..2a62c415d 100644 --- a/src/Ryujinx/Assets/Locales/ko_KR.json +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "배경색 설정", "AvatarClose": "닫기", "ControllerSettingsLoadProfileToolTip": "프로필 불러오기", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "프로필 추가", "ControllerSettingsRemoveProfileToolTip": "프로필 제거", "ControllerSettingsSaveProfileToolTip": "프로필 저장", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "지정된 저장 데이터를 찾는 중에 오류 발생: {0}", "FolderDialogExtractTitle": "추출할 폴더 선택", "DialogNcaExtractionMessage": "{1}에서 {0} 섹션을 추출하는 중...", - "DialogNcaExtractionTitle": "Ryujinx - NCA 섹션 추출기", + "DialogNcaExtractionTitle": "NCA 섹션 추출기", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "추출 실패하였습니다. 선택한 파일에 기본 NCA가 없습니다.", "DialogNcaExtractionCheckLogErrorMessage": "추출 실패하였습니다. 자세한 내용은 로그 파일을 읽으세요.", "DialogNcaExtractionSuccessMessage": "추출이 성공적으로 완료되었습니다.", diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json index 5a276a7c5..d6356dc76 100644 --- a/src/Ryujinx/Assets/Locales/pl_PL.json +++ b/src/Ryujinx/Assets/Locales/pl_PL.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Ustaw kolor tła", "AvatarClose": "Zamknij", "ControllerSettingsLoadProfileToolTip": "Wczytaj profil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Dodaj profil", "ControllerSettingsRemoveProfileToolTip": "Usuń profil", "ControllerSettingsSaveProfileToolTip": "Zapisz profil", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "Wystąpił błąd podczas próby znalezienia określonych zapisanych danych: {0}", "FolderDialogExtractTitle": "Wybierz folder, do którego chcesz rozpakować", "DialogNcaExtractionMessage": "Wypakowywanie sekcji {0} z {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Asystent wypakowania sekcji NCA", + "DialogNcaExtractionTitle": "Asystent wypakowania sekcji NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Niepowodzenie podczas wypakowywania. W wybranym pliku nie było głównego NCA.", "DialogNcaExtractionCheckLogErrorMessage": "Niepowodzenie podczas wypakowywania. Przeczytaj plik dziennika, aby uzyskać więcej informacji.", "DialogNcaExtractionSuccessMessage": "Wypakowywanie zakończone pomyślnie.", diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json index 6aeb422ed..42a8e437b 100644 --- a/src/Ryujinx/Assets/Locales/pt_BR.json +++ b/src/Ryujinx/Assets/Locales/pt_BR.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Definir cor de fundo", "AvatarClose": "Fechar", "ControllerSettingsLoadProfileToolTip": "Carregar perfil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Adicionar perfil", "ControllerSettingsRemoveProfileToolTip": "Remover perfil", "ControllerSettingsSaveProfileToolTip": "Salvar perfil", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "Ocorreu um erro ao tentar encontrar o diretório de salvamento: {0}", "FolderDialogExtractTitle": "Escolha o diretório onde os arquivos serão extraídos", "DialogNcaExtractionMessage": "Extraindo seção {0} de {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Extrator de seções NCA", + "DialogNcaExtractionTitle": "Extrator de seções NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Falha na extração. O NCA principal não foi encontrado no arquivo selecionado.", "DialogNcaExtractionCheckLogErrorMessage": "Falha na extração. Leia o arquivo de log para mais informações.", "DialogNcaExtractionSuccessMessage": "Extração concluída com êxito.", diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json index 4980a9a5d..4ef6ff6d9 100644 --- a/src/Ryujinx/Assets/Locales/ru_RU.json +++ b/src/Ryujinx/Assets/Locales/ru_RU.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Установить цвет фона", "AvatarClose": "Закрыть", "ControllerSettingsLoadProfileToolTip": "Загрузить профиль", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Добавить профиль", "ControllerSettingsRemoveProfileToolTip": "Удалить профиль", "ControllerSettingsSaveProfileToolTip": "Сохранить профиль", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "Произошла ошибка при поиске указанных данных сохранения: {0}", "FolderDialogExtractTitle": "Выберите папку для извлечения", "DialogNcaExtractionMessage": "Извлечение {0} раздела из {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Извлечение разделов NCA", + "DialogNcaExtractionTitle": "Извлечение разделов NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Ошибка извлечения. Основной NCA не присутствовал в выбранном файле.", "DialogNcaExtractionCheckLogErrorMessage": "Ошибка извлечения. Прочтите файл журнала для получения дополнительной информации.", "DialogNcaExtractionSuccessMessage": "Извлечение завершено успешно.", diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json index 16c2d9455..91169d9e2 100644 --- a/src/Ryujinx/Assets/Locales/th_TH.json +++ b/src/Ryujinx/Assets/Locales/th_TH.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "ตั้งค่าสีพื้นหลัง", "AvatarClose": "ปิด", "ControllerSettingsLoadProfileToolTip": "โหลด โปรไฟล์", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "เพิ่ม โปรไฟล์", "ControllerSettingsRemoveProfileToolTip": "ลบ โปรไฟล์", "ControllerSettingsSaveProfileToolTip": "บันทึก โปรไฟล์", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "มีข้อผิดพลาดในการค้นหาข้อมูลบันทึกที่ระบุไว้: {0}", "FolderDialogExtractTitle": "เลือกโฟลเดอร์ที่จะแตกไฟล์เข้าไป", "DialogNcaExtractionMessage": "กำลังแตกไฟล์ {0} จากส่วน {1}...", - "DialogNcaExtractionTitle": "Ryujinx - เครื่องมือแตกไฟล์ของ NCA", + "DialogNcaExtractionTitle": "เครื่องมือแตกไฟล์ของ NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "เกิดความล้มเหลวในการแตกไฟล์เนื่องจากไม่พบ NCA หลักในไฟล์ที่เลือก", "DialogNcaExtractionCheckLogErrorMessage": "เกิดความล้มเหลวในการแตกไฟล์ โปรดอ่านไฟล์บันทึกประวัติเพื่อดูข้อมูลเพิ่มเติม", "DialogNcaExtractionSuccessMessage": "การแตกไฟล์เสร็จสมบูรณ์แล้ว", diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json index d26ca18b7..b9ce3e884 100644 --- a/src/Ryujinx/Assets/Locales/tr_TR.json +++ b/src/Ryujinx/Assets/Locales/tr_TR.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Arka Plan Rengi Ayarla", "AvatarClose": "Kapat", "ControllerSettingsLoadProfileToolTip": "Profil Yükle", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Profil Ekle", "ControllerSettingsRemoveProfileToolTip": "Profili Kaldır", "ControllerSettingsSaveProfileToolTip": "Profili Kaydet", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "Belirtilen kayıt verisi bulunmaya çalışırken hata: {0}", "FolderDialogExtractTitle": "İçine ayıklanacak klasörü seç", "DialogNcaExtractionMessage": "{1} den {0} kısmı ayıklanıyor...", - "DialogNcaExtractionTitle": "Ryujinx - NCA Kısmı Ayıklayıcısı", + "DialogNcaExtractionTitle": "NCA Kısmı Ayıklayıcısı", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Ayıklama hatası. Ana NCA seçilen dosyada bulunamadı.", "DialogNcaExtractionCheckLogErrorMessage": "Ayıklama hatası. Ek bilgi için kayıt dosyasını okuyun.", "DialogNcaExtractionSuccessMessage": "Ayıklama başarıyla tamamlandı.", diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json index 0a4f251fe..581d1bca1 100644 --- a/src/Ryujinx/Assets/Locales/uk_UA.json +++ b/src/Ryujinx/Assets/Locales/uk_UA.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "Встановити колір фону", "AvatarClose": "Закрити", "ControllerSettingsLoadProfileToolTip": "Завантажити профіль", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Додати профіль", "ControllerSettingsRemoveProfileToolTip": "Видалити профіль", "ControllerSettingsSaveProfileToolTip": "Зберегти профіль", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "Під час пошуку вказаних даних збереження сталася помилка: {0}", "FolderDialogExtractTitle": "Виберіть папку для видобування", "DialogNcaExtractionMessage": "Видобування розділу {0} з {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Екстрактор розділів NCA", + "DialogNcaExtractionTitle": "Екстрактор розділів NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Помилка видобування. Основний NCA не був присутній у вибраному файлі.", "DialogNcaExtractionCheckLogErrorMessage": "Помилка видобування. Прочитайте файл журналу для отримання додаткової інформації.", "DialogNcaExtractionSuccessMessage": "Видобування успішно завершено.", diff --git a/src/Ryujinx/Assets/Locales/zh_CN.json b/src/Ryujinx/Assets/Locales/zh_CN.json index d1f3c13e3..3426b8a4a 100644 --- a/src/Ryujinx/Assets/Locales/zh_CN.json +++ b/src/Ryujinx/Assets/Locales/zh_CN.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "设置背景色", "AvatarClose": "关闭", "ControllerSettingsLoadProfileToolTip": "加载配置文件", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "新增配置文件", "ControllerSettingsRemoveProfileToolTip": "删除配置文件", "ControllerSettingsSaveProfileToolTip": "保存配置文件", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "查找指定存档时出错:{0}", "FolderDialogExtractTitle": "选择要提取到的文件夹", "DialogNcaExtractionMessage": "提取 {1} 的 {0} 分区...", - "DialogNcaExtractionTitle": "Ryujinx - NCA 分区提取", + "DialogNcaExtractionTitle": "NCA 分区提取", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失败,所选文件中没有 NCA 文件", "DialogNcaExtractionCheckLogErrorMessage": "提取失败,请查看日志文件获取详情", "DialogNcaExtractionSuccessMessage": "提取成功!", diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json index d69e57a9c..fd02254e1 100644 --- a/src/Ryujinx/Assets/Locales/zh_TW.json +++ b/src/Ryujinx/Assets/Locales/zh_TW.json @@ -407,6 +407,7 @@ "AvatarSetBackgroundColor": "設定背景顏色", "AvatarClose": "關閉", "ControllerSettingsLoadProfileToolTip": "載入設定檔", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "新增設定檔", "ControllerSettingsRemoveProfileToolTip": "刪除設定檔", "ControllerSettingsSaveProfileToolTip": "儲存設定檔", @@ -437,7 +438,7 @@ "DialogMessageFindSaveErrorMessage": "尋找指定的存檔時出現錯誤: {0}", "FolderDialogExtractTitle": "選擇要解壓到的資料夾", "DialogNcaExtractionMessage": "從 {1} 提取 {0} 分區...", - "DialogNcaExtractionTitle": "Ryujinx - NCA 分區提取器", + "DialogNcaExtractionTitle": "NCA 分區提取器", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失敗。所選檔案中不存在主 NCA 檔案。", "DialogNcaExtractionCheckLogErrorMessage": "提取失敗。請閱讀日誌檔案了解更多資訊。", "DialogNcaExtractionSuccessMessage": "提取成功。", diff --git a/src/Ryujinx/Common/ApplicationHelper.cs b/src/Ryujinx/Common/ApplicationHelper.cs index 43b69045c..db5961347 100644 --- a/src/Ryujinx/Common/ApplicationHelper.cs +++ b/src/Ryujinx/Common/ApplicationHelper.cs @@ -146,7 +146,7 @@ namespace Ryujinx.Ava.Common var cancellationToken = new CancellationTokenSource(); UpdateWaitWindow waitingDialog = new( - LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle], + App.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, ncaSectionType, Path.GetFileName(titleFilePath)), cancellationToken); @@ -269,7 +269,7 @@ namespace Ryujinx.Ava.Common Dispatcher.UIThread.Post(waitingDialog.Close); NotificationHelper.Show( - LocaleManager.Instance[LocaleKeys.DialogNcaExtractionTitle], + App.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), $"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}", NotificationType.Information); } diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index 3247a55f8..b57caa468 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -99,7 +99,7 @@ namespace Ryujinx.Ava.Common.Locale _ => false }; - public static string FormatDynamicValue(LocaleKeys key, params object[] values) + public static string FormatDynamicValue(LocaleKeys key, params object[] values) => Instance.UpdateAndGetDynamicValue(key, values); public string UpdateAndGetDynamicValue(LocaleKeys key, params object[] values) diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 31db628d3..05fd66b90 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -30,7 +30,6 @@ namespace Ryujinx.Ava { internal partial class Program { - // public static double WindowScaleFactor { get; set; } public static double DesktopScaleFactor { get; set; } = 1.0; public static string Version { get; private set; } @@ -101,10 +100,10 @@ namespace Ryujinx.Ava // Delete backup files after updating. Task.Run(Updater.CleanupUpdate); - Console.Title = $"Ryujinx Console {Version}"; + Console.Title = $"{App.FullAppName} Console {Version}"; // Hook unhandled exception and process exit events. - AppDomain.CurrentDomain.UnhandledException += (sender, e) + AppDomain.CurrentDomain.UnhandledException += (sender, e) => ProcessUnhandledException(sender, e.ExceptionObject as Exception, e.IsTerminating); AppDomain.CurrentDomain.ProcessExit += (_, _) => Exit(); @@ -225,16 +224,14 @@ namespace Ryujinx.Ava private static void PrintSystemInfo() { - Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}"); + Logger.Notice.Print(LogClass.Application, $"{App.FullAppName} Version: {Version}"); SystemInfo.Gather().Print(); var enabledLogLevels = Logger.GetEnabledLevels().ToArray(); - Logger.Notice.Print(LogClass.Application, $"Logs Enabled: { - (enabledLogLevels.Length is 0 + Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogLevels.Length is 0 ? "" - : enabledLogLevels.JoinToString(", ")) - }"); + : enabledLogLevels.JoinToString(", "))}"); Logger.Notice.Print(LogClass.Application, AppDataManager.Mode == AppDataManager.LaunchMode.Custom @@ -246,13 +243,13 @@ namespace Ryujinx.Ava { Logger.Log log = Logger.Error ?? Logger.Notice; string message = $"Unhandled exception caught: {ex}"; - + // ReSharper disable once ConstantConditionalAccessQualifier - if (sender?.GetType()?.AsPrettyString() is {} senderName) + if (sender?.GetType()?.AsPrettyString() is { } senderName) log.Print(LogClass.Application, message, senderName); else log.PrintMsg(LogClass.Application, message); - + if (isTerminating) Exit(); } diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs index 1dbf37255..2ebba7ac0 100644 --- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs @@ -31,7 +31,7 @@ namespace Ryujinx.Ava.UI.Applet public bool DisplayMessageDialog(ControllerAppletUIArgs args) { ManualResetEvent dialogCloseEvent = new(false); - + bool okPressed = false; if (ConfigurationState.Instance.IgnoreApplet) diff --git a/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs b/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs index 5ec7737ed..0cd3f18e5 100644 --- a/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs @@ -24,7 +24,7 @@ namespace Ryujinx.Ava.UI.Applet public AvaloniaDynamicTextInputHandler(MainWindow parent) { _parent = parent; - + if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver) { avaloniaKeyboardDriver.KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed; @@ -121,7 +121,7 @@ namespace Ryujinx.Ava.UI.Applet avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease; avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput; } - + _textChangedSubscription?.Dispose(); _selectionStartChangedSubscription?.Dispose(); _selectionEndtextChangedSubscription?.Dispose(); diff --git a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs index 3a8350893..ee0e884d2 100644 --- a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs +++ b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs @@ -37,8 +37,8 @@ namespace Ryujinx.Ava.UI.Applet public ControllerAppletDialog(MainWindow mainWindow, ControllerAppletUIArgs args) { - PlayerCount = args.PlayerCountMin == args.PlayerCountMax - ? args.PlayerCountMin.ToString() + PlayerCount = args.PlayerCountMin == args.PlayerCountMax + ? args.PlayerCountMin.ToString() : $"{args.PlayerCountMin} - {args.PlayerCountMax}"; SupportsProController = (args.SupportedStyles & ControllerType.ProController) != 0; diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index ea748a3bf..0daa77ac4 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -7,6 +7,7 @@ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:converters="clr-namespace:Avalonia.Data.Converters;assembly=Avalonia.Base" d:DesignHeight="450" d:DesignWidth="800" Focusable="True" @@ -110,6 +111,11 @@ Text="{Binding FileExtension}" TextAlignment="Start" TextWrapping="Wrap" /> + UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem)); - + InitializeComponent(); } @@ -60,13 +60,13 @@ namespace Ryujinx.Ava.UI.Controls LoadProfiles(); } - public void Navigate(Type sourcePageType, object parameter) + public void Navigate(Type sourcePageType, object parameter) => ContentFrame.Navigate(sourcePageType, parameter); public static async Task Show( - AccountManager ownerAccountManager, + AccountManager ownerAccountManager, ContentManager ownerContentManager, - VirtualFileSystem ownerVirtualFileSystem, + VirtualFileSystem ownerVirtualFileSystem, HorizonClient ownerHorizonClient) { var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient); @@ -158,9 +158,9 @@ namespace Ryujinx.Ava.UI.Controls _ = Dispatcher.UIThread.InvokeAsync(async () => await ContentDialogHelper.CreateErrorDialog( LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage])); - + return; - } + } AccountManager.OpenUser(profile.UserId); } diff --git a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs index bd8c1e3a7..a7fe3f0ce 100644 --- a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs +++ b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs @@ -226,11 +226,11 @@ namespace Ryujinx.Ava.UI.Helpers (int)Symbol.Help, primaryButtonResult); - internal static async Task CreateConfirmationDialogExtended( + internal static async Task CreateDeniableConfirmationDialog( string primaryText, string secondaryText, string acceptButtonText, - string noacceptButtonText, + string noAcceptButtonText, string cancelButtonText, string title, UserResult primaryButtonResult = UserResult.Yes) @@ -239,7 +239,7 @@ namespace Ryujinx.Ava.UI.Helpers primaryText, secondaryText, acceptButtonText, - noacceptButtonText, + noAcceptButtonText, cancelButtonText, (int)Symbol.Help, primaryButtonResult); diff --git a/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs b/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs index 94c3ab35d..6196421c8 100644 --- a/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs +++ b/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs @@ -22,9 +22,9 @@ namespace Ryujinx.Ava.UI.Helpers _key = key; } - public string this[string key] => + public string this[string key] => _glyphs.TryGetValue(Enum.Parse(key), out var val) - ? val + ? val : string.Empty; public override object ProvideValue(IServiceProvider serviceProvider) => this[_key]; diff --git a/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs new file mode 100644 index 000000000..8bd8b5f0d --- /dev/null +++ b/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs @@ -0,0 +1,44 @@ +using Avalonia.Data.Converters; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Common.Helper; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class MultiplayerInfoConverter : MarkupExtension, IValueConverter + { + private static readonly MultiplayerInfoConverter _instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is ApplicationData applicationData) + { + if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0) + { + return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}"; + } + else + { + return ""; + } + } + else + { + return ""; + } + } + + 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/UI/Helpers/TimeZoneConverter.cs b/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs index 5edc6482e..0e5525c7b 100644 --- a/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs +++ b/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs @@ -9,12 +9,12 @@ namespace Ryujinx.Ava.UI.Helpers { public static TimeZoneConverter Instance = new(); - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - => value is TimeZone timeZone - ? $"{timeZone.UtcDifference} {timeZone.Location} {timeZone.Abbreviation}" + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is TimeZone timeZone + ? $"{timeZone.UtcDifference} {timeZone.Location} {timeZone.Abbreviation}" : null; - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); } } diff --git a/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs b/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs index f12cf0aa6..40f783c44 100644 --- a/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs @@ -10,7 +10,7 @@ namespace Ryujinx.Ava.UI.Models public string DockedMode { get; } public string FifoStatus { get; } public string GameStatus { get; } - + public uint ShaderCount { get; } public StatusUpdatedEventArgs(bool vSyncEnabled, string volumeStatus, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, uint shaderCount) diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 70f8b2a82..53263847b 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -117,6 +117,8 @@ namespace Ryujinx.Ava.UI.ViewModels public ApplicationData ListSelectedApplication; public ApplicationData GridSelectedApplication; + public IEnumerable LastLdnGameData; + public static readonly Bitmap IconBitmap = new(Assembly.GetAssembly(typeof(ConfigurationState))!.GetManifestResourceStream("Ryujinx.UI.Common.Resources.Logo_Ryujinx.png")!); @@ -173,7 +175,7 @@ namespace Ryujinx.Ava.UI.ViewModels SwitchToGameControl = switchToGameControl; SetMainContent = setMainContent; TopLevel = topLevel; - + #if DEBUG topLevel.AttachDevTools(new KeyGesture(Avalonia.Input.Key.F12, KeyModifiers.Control)); #endif @@ -268,7 +270,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool ShowFirmwareStatus => !ShowLoadProgress; - public bool ShowRightmostSeparator + public bool ShowRightmostSeparator { get => _showRightmostSeparator; set @@ -553,7 +555,7 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(); } } - + public string ShaderCountText { get => _shaderCountText; @@ -802,6 +804,11 @@ namespace Ryujinx.Ava.UI.ViewModels { get => FileAssociationHelper.IsTypeAssociationSupported; } + + public bool AreMimeTypesRegistered + { + get => FileAssociationHelper.AreMimeTypesRegistered; + } public ObservableCollectionExtended Applications { @@ -1016,7 +1023,7 @@ namespace Ryujinx.Ava.UI.ViewModels ? SortExpressionComparer.Ascending(selector) : SortExpressionComparer.Descending(selector); - private IComparer GetComparer() + private IComparer GetComparer() => SortMode switch { #pragma warning disable IDE0055 // Disable formatting @@ -1246,7 +1253,7 @@ namespace Ryujinx.Ava.UI.ViewModels private void InitializeGame() { RendererHostControl.WindowCreated += RendererHost_Created; - + AppHost.StatusUpdatedEvent += Update_StatusBar; AppHost.AppExit += AppHost_AppExit; @@ -1295,9 +1302,9 @@ namespace Ryujinx.Ava.UI.ViewModels GameStatusText = args.GameStatus; VolumeStatusText = args.VolumeStatus; FifoStatusText = args.FifoStatus; - - ShaderCountText = (ShowRightmostSeparator = args.ShaderCount > 0) - ? $"{LocaleManager.Instance[LocaleKeys.CompilingShaders]}: {args.ShaderCount}" + + ShaderCountText = (ShowRightmostSeparator = args.ShaderCount > 0) + ? $"{LocaleManager.Instance[LocaleKeys.CompilingShaders]}: {args.ShaderCount}" : string.Empty; ShowStatusSeparator = true; @@ -1702,7 +1709,7 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId) + public static void UpdateGameMetadata(string titleId) => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame()); public void RefreshFirmwareStatus() diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index f069896f8..2da252d00 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -25,12 +25,13 @@ using System.Collections.ObjectModel; using System.Linq; using System.Net.NetworkInformation; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Threading.Tasks; using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; namespace Ryujinx.Ava.UI.ViewModels { - public class SettingsViewModel : BaseModel + public partial class SettingsViewModel : BaseModel { private readonly VirtualFileSystem _virtualFileSystem; private readonly ContentManager _contentManager; @@ -56,6 +57,8 @@ namespace Ryujinx.Ava.UI.ViewModels public event Action SaveSettingsEvent; private int _networkInterfaceIndex; private int _multiplayerModeIndex; + private string _ldnPassphrase; + private string _LdnServer; public int ResolutionScale { @@ -180,10 +183,24 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsVulkanSelected => GraphicsBackendIndex == 0; public bool UseHypervisor { get; set; } + public bool DisableP2P { get; set; } public string TimeZone { get; set; } public string ShaderDumpPath { get; set; } + public string LdnPassphrase + { + get => _ldnPassphrase; + set + { + _ldnPassphrase = value; + IsInvalidLdnPassphraseVisible = !ValidateLdnPassphrase(value); + + OnPropertyChanged(); + OnPropertyChanged(nameof(IsInvalidLdnPassphraseVisible)); + } + } + public int Language { get; set; } public int Region { get; set; } public int FsGlobalAccessLogMode { get; set; } @@ -276,6 +293,21 @@ namespace Ryujinx.Ava.UI.ViewModels } } + [GeneratedRegex("Ryujinx-[0-9a-f]{8}")] + private static partial Regex LdnPassphraseRegex(); + + public bool IsInvalidLdnPassphraseVisible { get; set; } + + public string LdnServer + { + get => _LdnServer; + set + { + _LdnServer = value; + OnPropertyChanged(); + } + } + public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() { _virtualFileSystem = virtualFileSystem; @@ -393,6 +425,11 @@ namespace Ryujinx.Ava.UI.ViewModels Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(NetworkInterfaceIndex))); } + private bool ValidateLdnPassphrase(string passphrase) + { + return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && LdnPassphraseRegex().IsMatch(passphrase)); + } + public void ValidateAndSetTimeZone(string location) { if (_validTzRegions.Contains(location)) @@ -497,6 +534,9 @@ namespace Ryujinx.Ava.UI.ViewModels OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; + DisableP2P = config.Multiplayer.DisableP2p.Value; + LdnPassphrase = config.Multiplayer.LdnPassphrase.Value; + LdnServer = config.Multiplayer.LdnServer.Value; } public void SaveSettings() @@ -613,6 +653,9 @@ namespace Ryujinx.Ava.UI.ViewModels config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex; + config.Multiplayer.DisableP2p.Value = DisableP2P; + config.Multiplayer.LdnPassphrase.Value = LdnPassphrase; + config.Multiplayer.LdnServer.Value = LdnServer; config.ToFileFormat().SaveConfig(Program.ConfigurationPath); diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs index c900ea532..ee84fbc37 100644 --- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs @@ -26,19 +26,17 @@ namespace Ryujinx.Ava.UI.Views.Input foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) { - if (visual is ToggleButton button and not CheckBox) + switch (visual) { - button.IsCheckedChanged += Button_IsCheckedChanged; - } - - if (visual is CheckBox check) - { - check.IsCheckedChanged += CheckBox_IsCheckedChanged; - } - - if (visual is Slider slider) - { - slider.PropertyChanged += Slider_IsCheckedChanged; + case ToggleButton button and not CheckBox: + button.IsCheckedChanged += Button_IsCheckedChanged; + break; + case CheckBox check: + check.IsCheckedChanged += CheckBox_IsCheckedChanged; + break; + case Slider slider: + slider.PropertyChanged += Slider_ValueChanged; + break; } } } @@ -47,33 +45,28 @@ namespace Ryujinx.Ava.UI.Views.Input { base.OnPointerReleased(e); - if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver) + if (_currentAssigner is { ToggledButton.IsPointerOver: false }) { _currentAssigner.Cancel(); } } - private float _changeSlider = -1.0f; + private float _changeSlider = float.NaN; - private void Slider_IsCheckedChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void Slider_ValueChanged(object sender, AvaloniaPropertyChangedEventArgs e) { if (sender is Slider check) { - if ((bool)check.IsPointerOver && _changeSlider == -1.0f) + _changeSlider = check.IsPointerOver switch { - _changeSlider = (float)check.Value; - - } - else if (!(bool)check.IsPointerOver) - { - _changeSlider = -1.0f; - } + true when float.IsNaN(_changeSlider) => (float)check.Value, + false => float.NaN, + _ => _changeSlider + }; - if (_changeSlider != -1.0f && _changeSlider != (float)check.Value) + if (!float.IsNaN(_changeSlider) && _changeSlider != (float)check.Value) { - - var viewModel = (DataContext as ControllerInputViewModel); - viewModel.ParentModel.IsModified = true; + (DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true; _changeSlider = (float)check.Value; } } @@ -81,25 +74,20 @@ namespace Ryujinx.Ava.UI.Views.Input private void CheckBox_IsCheckedChanged(object sender, RoutedEventArgs e) { - if (sender is CheckBox check) + if (sender is CheckBox { IsPointerOver: true }) { - if ((bool)check.IsPointerOver) - { - - var viewModel = (DataContext as ControllerInputViewModel); - viewModel.ParentModel.IsModified = true; - _currentAssigner?.Cancel(); - _currentAssigner = null; - } + (DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true; + _currentAssigner?.Cancel(); + _currentAssigner = null; } } private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) { - if (sender is ToggleButton button ) + if (sender is ToggleButton button) { - if ((bool)button.IsChecked) + if (button.IsChecked is true) { if (_currentAssigner != null && button == _currentAssigner.ToggledButton) { @@ -204,7 +192,7 @@ namespace Ryujinx.Ava.UI.Views.Input } else { - if (_currentAssigner != null ) + if (_currentAssigner != null) { _currentAssigner.Cancel(); _currentAssigner = null; diff --git a/src/Ryujinx/UI/Views/Input/InputView.axaml.cs b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs index 5fda7ef6a..3c9d4040f 100644 --- a/src/Ryujinx/UI/Views/Input/InputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs @@ -37,7 +37,7 @@ namespace Ryujinx.Ava.UI.Views.Input { _dialogOpen = true; - var result = await ContentDialogHelper.CreateConfirmationDialogExtended( + var result = await ContentDialogHelper.CreateDeniableConfirmationDialog( LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage], LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage], LocaleManager.Instance[LocaleKeys.InputDialogYes], @@ -53,28 +53,19 @@ namespace Ryujinx.Ava.UI.Views.Input _dialogOpen = false; - if (result == UserResult.Cancel) - { - - return; - } - - ViewModel.IsModified = false; - - if (result != UserResult.Cancel) - { - ViewModel.PlayerId = ViewModel.PlayerIdChoose; - } - if (result == UserResult.Cancel) { if (e.AddedItems.Count > 0) { ViewModel.IsModified = true; - var player = (PlayerModel)e.AddedItems[0]; - ViewModel.PlayerId = player.Id; + ViewModel.PlayerId = ((PlayerModel)e.AddedItems[0])!.Id; } + return; } + + ViewModel.PlayerId = ViewModel.PlayerIdChoose; + + ViewModel.IsModified = false; } } diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 03b7cfbe4..883bf8971 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -265,11 +265,11 @@ - - + + - + diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 1acee3af5..ce4d9fd59 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -40,8 +40,8 @@ namespace Ryujinx.Ava.UI.Views.Main private CheckBox[] GenerateToggleFileTypeItems() => Enum.GetValues() .Select(it => (FileName: Enum.GetName(it)!, FileType: it)) - .Select(it => - new CheckBox + .Select(it => + new CheckBox { Content = $".{it.FileName}", IsChecked = it.FileType.GetConfigValue(ConfigurationState.Instance.UI.ShownFileTypes), diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml index c7736bf8d..2fc59f04d 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs index b771933eb..c69307522 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs @@ -1,12 +1,29 @@ using Avalonia.Controls; +using Avalonia.Interactivity; +using Ryujinx.Ava.UI.ViewModels; +using System; namespace Ryujinx.Ava.UI.Views.Settings { public partial class SettingsNetworkView : UserControl { + public SettingsViewModel ViewModel; + public SettingsNetworkView() { InitializeComponent(); } + + private void GenLdnPassButton_OnClick(object sender, RoutedEventArgs e) + { + byte[] code = new byte[4]; + new Random().NextBytes(code); + ViewModel.LdnPassphrase = $"Ryujinx-{BitConverter.ToUInt32(code):x8}"; + } + + private void ClearLdnPassButton_OnClick(object sender, RoutedEventArgs e) + { + ViewModel.LdnPassphrase = ""; + } } } diff --git a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs index edca7949a..8c8d56b34 100644 --- a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs @@ -75,7 +75,7 @@ namespace Ryujinx.Ava.UI.Windows string parentPath = currentCheatFile.Replace(titleModsPath, string.Empty); buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper(); - currentGroup = new CheatNode("", buildId, parentPath, true); + currentGroup = new CheatNode(string.Empty, buildId, parentPath, true); LoadedCheats.Add(currentGroup); } diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 24a1b62a2..a43c29518 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -154,6 +154,36 @@ namespace Ryujinx.Ava.UI.Windows }); } + private void ApplicationLibrary_LdnGameDataReceived(object sender, LdnGameDataReceivedEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + var ldnGameDataArray = e.LdnData; + ViewModel.LastLdnGameData = ldnGameDataArray; + foreach (var application in ViewModel.Applications) + { + UpdateApplicationWithLdnData(application); + } + ViewModel.RefreshView(); + }); + } + + private void UpdateApplicationWithLdnData(ApplicationData application) + { + if (application.ControlHolder.ByteSpan.Length > 0 && ViewModel.LastLdnGameData != null) + { + IEnumerable ldnGameData = ViewModel.LastLdnGameData.Where(game => application.ControlHolder.Value.LocalCommunicationId.Items.Contains(Convert.ToUInt64(game.TitleId, 16))); + + application.PlayerCount = ldnGameData.Sum(game => game.PlayerCount); + application.GameCount = ldnGameData.Count(); + } + else + { + application.PlayerCount = 0; + application.GameCount = 0; + } + } + public void Application_Opened(object sender, ApplicationOpenedEventArgs args) { if (args.Application != null) @@ -450,7 +480,20 @@ namespace Ryujinx.Ava.UI.Windows .Connect() .ObserveOn(SynchronizationContext.Current!) .Bind(ViewModel.Applications) + .OnItemAdded(UpdateApplicationWithLdnData) .Subscribe(); + ApplicationLibrary.LdnGameDataReceived += ApplicationLibrary_LdnGameDataReceived; + + ConfigurationState.Instance.Multiplayer.Mode.Event += (sender, evt) => + { + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); + }; + + ConfigurationState.Instance.Multiplayer.LdnServer.Event += (sender, evt) => + { + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); + }; + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); ViewModel.RefreshFirmwareStatus(); @@ -459,7 +502,7 @@ namespace Ryujinx.Ava.UI.Windows { LoadApplications(); } - + _ = CheckLaunchState(); } @@ -588,13 +631,26 @@ namespace Ryujinx.Ava.UI.Windows { switch (fileType) { - case "NSP": ConfigurationState.Instance.UI.ShownFileTypes.NSP.Toggle(); break; - case "PFS0": ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Toggle(); break; - case "XCI": ConfigurationState.Instance.UI.ShownFileTypes.XCI.Toggle(); break; - case "NCA": ConfigurationState.Instance.UI.ShownFileTypes.NCA.Toggle(); break; - case "NRO": ConfigurationState.Instance.UI.ShownFileTypes.NRO.Toggle(); break; - case "NSO": ConfigurationState.Instance.UI.ShownFileTypes.NSO.Toggle(); break; - default: throw new ArgumentOutOfRangeException(fileType); + case "NSP": + ConfigurationState.Instance.UI.ShownFileTypes.NSP.Toggle(); + break; + case "PFS0": + ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Toggle(); + break; + case "XCI": + ConfigurationState.Instance.UI.ShownFileTypes.XCI.Toggle(); + break; + case "NCA": + ConfigurationState.Instance.UI.ShownFileTypes.NCA.Toggle(); + break; + case "NRO": + ConfigurationState.Instance.UI.ShownFileTypes.NRO.Toggle(); + break; + case "NSO": + ConfigurationState.Instance.UI.ShownFileTypes.NSO.Toggle(); + break; + default: + throw new ArgumentOutOfRangeException(fileType); } ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs index 1a177d182..d8c88bed8 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs @@ -80,6 +80,7 @@ namespace Ryujinx.Ava.UI.Windows NavPanel.Content = AudioPage; break; case "NetworkPage": + NetworkPage.ViewModel = ViewModel; NavPanel.Content = NetworkPage; break; case "LoggingPage": diff --git a/src/Ryujinx/UI/Windows/StyleableWindow.cs b/src/Ryujinx/UI/Windows/StyleableWindow.cs index 493214ee2..9e4eed2e4 100644 --- a/src/Ryujinx/UI/Windows/StyleableWindow.cs +++ b/src/Ryujinx/UI/Windows/StyleableWindow.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Ava.UI.Windows LocaleManager.Instance.LocaleChanged += LocaleChanged; LocaleChanged(); - + Icon = MainWindowViewModel.IconBitmap; } diff --git a/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs b/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs index 580ebc9da..6df862283 100644 --- a/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs @@ -32,9 +32,9 @@ namespace Ryujinx.Ava.UI.Windows { ContentDialog contentDialog = new() { - PrimaryButtonText = "", - SecondaryButtonText = "", - CloseButtonText = "", + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = string.Empty, Content = new XCITrimmerWindow(mainWindowViewModel), Title = string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerWindowTitle]), }; diff --git a/src/Ryujinx/Updater.cs b/src/Ryujinx/Updater.cs index a466ea832..9deff5e86 100644 --- a/src/Ryujinx/Updater.cs +++ b/src/Ryujinx/Updater.cs @@ -32,7 +32,8 @@ namespace Ryujinx.Ava internal static class Updater { private const string GitHubApiUrl = "https://api.github.com"; - private const string LatestReleaseUrl = $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest"; + private const string LatestReleaseUrl = + $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest"; private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -83,7 +84,7 @@ namespace Ryujinx.Ava } catch { - Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!"); + Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {App.FullAppName} version!"); await ContentDialogHelper.CreateWarningDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],