Merge branch 'GreemDev:master' into master

This commit is contained in:
Vladimir Sokolov 2024-11-13 09:45:58 +10:00 committed by GitHub
commit 5bfa01b58a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
136 changed files with 5748 additions and 1823 deletions

View File

@ -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

View File

@ -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

View File

@ -33,6 +33,7 @@
<PackageVersion Include="OpenTK.Graphics" Version="4.8.2" />
<PackageVersion Include="OpenTK.Audio.OpenAL" Version="4.8.2" />
<PackageVersion Include="OpenTK.Windowing.GraphicsLibraryFramework" Version="4.8.2" />
<PackageVersion Include="Open.NAT.Core" Version="2.1.0.5" />
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />

View File

@ -77,7 +77,7 @@ namespace ARMeilleure.Translation
{
continue;
}
for (int pBlkIndex = 0; pBlkIndex < block.Predecessors.Count; pBlkIndex++)
{
BasicBlock current = block.Predecessors[pBlkIndex];

View File

@ -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<Thread> threads = new();
for (int i = 0; i < degreeOfParallelism; i++)
{
Thread thread = new(TranslateFuncs)
{
IsBackground = true,
Name = "Ptc.TranslateThread." + i
};
threads.Add(thread);
}
List<Thread> threads = Enumerable.Range(0, degreeOfParallelism)
.Select(idx =>
new Thread(TranslateFuncs)
{
IsBackground = true,
Name = "Ptc.TranslateThread." + idx
}
).ToList();
Stopwatch sw = Stopwatch.StartNew();

View File

@ -3,6 +3,7 @@ namespace Ryujinx.Common.Configuration.Multiplayer
public enum MultiplayerMode
{
Disabled,
LdnRyu,
LdnMitm,
}
}

View File

@ -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<LogEventArgs>(queueLimit);

View File

@ -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

View File

@ -803,18 +803,6 @@ namespace Ryujinx.Common.Memory
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array256<T> : IArray<T> where T : unmanaged
{
T _e0;
Array128<T> _other;
Array127<T> _other2;
public readonly int Length => 256;
public ref T this[int index] => ref AsSpan()[index];
[Pure]
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array140<T> : IArray<T> where T : unmanaged
{
T _e0;
@ -828,6 +816,18 @@ namespace Ryujinx.Common.Memory
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array256<T> : IArray<T> where T : unmanaged
{
T _e0;
Array128<T> _other;
Array127<T> _other2;
public readonly int Length => 256;
public ref T this[int index] => ref AsSpan()[index];
[Pure]
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
public struct Array384<T> : IArray<T> where T : unmanaged
{
T _e0;

View File

@ -1,11 +1,13 @@
using Ryujinx.Common.Logging;
using System;
using System.Globalization;
using System.Threading;
namespace Ryujinx.Common
{
public class ReactiveObject<T>
{
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<T> obj) => obj.Value;
}
public static class ReactiveObjectHelper
{
public static void LogValueChange<T>(LogClass logClass, ReactiveEventArgs<T> 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<bool> rBoolean) => rBoolean.Value = !rBoolean.Value;
}

View File

@ -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("%%");

View File

@ -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());

View File

@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.GAL
IPipeline Pipeline { get; }
IWindow Window { get; }
uint ProgramCount { get; }
void BackgroundContextAction(Action action, bool alwaysBackground = false);

View File

@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.OpenGL
public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
{
ProgramCount++;
return new Program(shaders, info.FragmentOutputMap);
}

View File

@ -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)

View File

@ -164,6 +164,21 @@ namespace Ryujinx.HLE
/// </summary>
public MultiplayerMode MultiplayerMode { internal get; set; }
/// <summary>
/// Disable P2P mode
/// </summary>
public bool MultiplayerDisableP2p { internal get; set; }
/// <summary>
/// Multiplayer Passphrase
/// </summary>
public string MultiplayerLdnPassphrase { internal get; set; }
/// <summary>
/// LDN Server
/// </summary>
public string MultiplayerLdnServer { internal get; set; }
/// <summary>
/// An action called when HLE force a refresh of output after docked mode changed.
/// </summary>
@ -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;
}
}
}

View File

@ -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");

View File

@ -116,18 +116,13 @@ namespace Ryujinx.HLE.HOS
private readonly Dictionary<ulong, ModCache> _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<DirectoryInfo> mod = new("", null, true);
Mod<DirectoryInfo> mod = new(string.Empty, null, true);
if (StrEquals(RomfsDir, modDir.Name))
{

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<byte> Data;

View File

@ -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<byte> UserName;

View File

@ -15,6 +15,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public Array8<NodeLatestUpdate> 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)

View File

@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
interface INetworkClient : IDisposable
{
ProxyConfig Config { get; }
bool NeedsRealId { get; }
event EventHandler<NetworkChangeEventArgs> NetworkChange;

View File

@ -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;
}
}

View File

@ -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<NetworkChangeEventArgs> 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<NetworkInfo>();
}

View File

@ -12,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
/// </summary>
internal class LdnMitmClient : INetworkClient
{
public ProxyConfig Config { get; }
public bool NeedsRealId => false;
public event EventHandler<NetworkChangeEventArgs> NetworkChange;

View File

@ -0,0 +1,7 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
{
interface IProxyClient
{
bool SendAsync(byte[] buffer);
}
}

View File

@ -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<NetworkInfo> _availableGames = new List<NetworkInfo>();
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<NetworkChangeEventArgs> 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<ushort> 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<NetworkInfo>();
}
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));
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<ushort> _ephemeralPorts = new List<ushort>();
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);
}
}
}
}

View File

@ -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<LdnProxySocket> _sockets = new List<LdnProxySocket>();
private readonly Dictionary<ProtocolType, EphemeralPortPool> _ephemeralPorts = new Dictionary<ProtocolType, EphemeralPortPool>();
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<LdnProxySocket> 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<byte> 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();
}
}
}
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
class LdnProxySocket : ISocketImpl
{
private readonly LdnProxy _proxy;
private bool _isListening;
private readonly List<LdnProxySocket> _listenSockets = new List<LdnProxySocket>();
private readonly Queue<ProxyConnectRequest> _connectRequests = new Queue<ProxyConnectRequest>();
private readonly AutoResetEvent _acceptEvent = new AutoResetEvent(false);
private readonly int _acceptTimeout = -1;
private readonly Queue<int> _errors = new Queue<int>();
private readonly AutoResetEvent _connectEvent = new AutoResetEvent(false);
private ProxyConnectResponse _connectResponse;
private int _receiveTimeout = -1;
private readonly AutoResetEvent _receiveEvent = new AutoResetEvent(false);
private readonly Queue<ProxyDataPacket> _receiveQueue = new Queue<ProxyDataPacket>();
// 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<SocketOptionName, int> _socketOptions = new Dictionary<SocketOptionName, int>()
{
{ 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<byte> buffer)
{
EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
return ReceiveFrom(buffer, SocketFlags.None, ref dummy);
}
public int Receive(Span<byte> buffer, SocketFlags flags)
{
EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
return ReceiveFrom(buffer, flags, ref dummy);
}
public int Receive(Span<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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()
{
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<P2pProxySession> _players = new List<P2pProxySession>();
private readonly List<ExternalProxyToken> _waitingTokens = new List<ExternalProxyToken>();
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<ushort> 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<P2pProxySession> 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}");
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<LdnHeader>();
private readonly byte[] _buffer = new byte[MaxPacketSize];
private int _bufferEnd = 0;
// Client Packets.
public event Action<LdnHeader, InitializeMessage> Initialize;
public event Action<LdnHeader, PassphraseMessage> Passphrase;
public event Action<LdnHeader, NetworkInfo> Connected;
public event Action<LdnHeader, NetworkInfo> SyncNetwork;
public event Action<LdnHeader, NetworkInfo> ScanReply;
public event Action<LdnHeader> ScanReplyEnd;
public event Action<LdnHeader, DisconnectMessage> Disconnected;
// External Proxy Packets.
public event Action<LdnHeader, ExternalProxyConfig> ExternalProxy;
public event Action<LdnHeader, ExternalProxyConnectionState> ExternalProxyState;
public event Action<LdnHeader, ExternalProxyToken> ExternalProxyToken;
// Server Packets.
public event Action<LdnHeader, CreateAccessPointRequest, byte[]> CreateAccessPoint;
public event Action<LdnHeader, CreateAccessPointPrivateRequest, byte[]> CreateAccessPointPrivate;
public event Action<LdnHeader, RejectRequest> Reject;
public event Action<LdnHeader> RejectReply;
public event Action<LdnHeader, SetAcceptPolicyRequest> SetAcceptPolicy;
public event Action<LdnHeader, byte[]> SetAdvertiseData;
public event Action<LdnHeader, ConnectRequest> Connect;
public event Action<LdnHeader, ConnectPrivateRequest> ConnectPrivate;
public event Action<LdnHeader, ScanFilter> Scan;
// Proxy Packets.
public event Action<LdnHeader, ProxyConfig> ProxyConfig;
public event Action<LdnHeader, ProxyConnectRequest> ProxyConnect;
public event Action<LdnHeader, ProxyConnectResponse> ProxyConnectReply;
public event Action<LdnHeader, ProxyDataHeader, byte[]> ProxyData;
public event Action<LdnHeader, ProxyDisconnectMessage> ProxyDisconnect;
// Lifecycle Packets.
public event Action<LdnHeader, NetworkErrorMessage> NetworkError;
public event Action<LdnHeader, PingMessage> 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<byte, LdnHeader>(_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<T>(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<T>(data), remainder);
}
private void DecodeAndHandle(LdnHeader header, byte[] data)
{
switch ((PacketId)header.Type)
{
// Client Packets.
case PacketId.Initialize:
{
Initialize?.Invoke(header, MemoryMarshal.Read<InitializeMessage>(data));
break;
}
case PacketId.Passphrase:
{
Passphrase?.Invoke(header, MemoryMarshal.Read<PassphraseMessage>(data));
break;
}
case PacketId.Connected:
{
Connected?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
break;
}
case PacketId.SyncNetwork:
{
SyncNetwork?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
break;
}
case PacketId.ScanReply:
{
ScanReply?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
break;
}
case PacketId.ScanReplyEnd:
{
ScanReplyEnd?.Invoke(header);
break;
}
case PacketId.Disconnect:
{
Disconnected?.Invoke(header, MemoryMarshal.Read<DisconnectMessage>(data));
break;
}
// External Proxy Packets.
case PacketId.ExternalProxy:
{
ExternalProxy?.Invoke(header, MemoryMarshal.Read<ExternalProxyConfig>(data));
break;
}
case PacketId.ExternalProxyState:
{
ExternalProxyState?.Invoke(header, MemoryMarshal.Read<ExternalProxyConnectionState>(data));
break;
}
case PacketId.ExternalProxyToken:
{
ExternalProxyToken?.Invoke(header, MemoryMarshal.Read<ExternalProxyToken>(data));
break;
}
// Server Packets.
case PacketId.CreateAccessPoint:
{
(CreateAccessPointRequest packet, byte[] extraData) = ParseWithData<CreateAccessPointRequest>(data);
CreateAccessPoint?.Invoke(header, packet, extraData);
break;
}
case PacketId.CreateAccessPointPrivate:
{
(CreateAccessPointPrivateRequest packet, byte[] extraData) = ParseWithData<CreateAccessPointPrivateRequest>(data);
CreateAccessPointPrivate?.Invoke(header, packet, extraData);
break;
}
case PacketId.Reject:
{
Reject?.Invoke(header, MemoryMarshal.Read<RejectRequest>(data));
break;
}
case PacketId.RejectReply:
{
RejectReply?.Invoke(header);
break;
}
case PacketId.SetAcceptPolicy:
{
SetAcceptPolicy?.Invoke(header, MemoryMarshal.Read<SetAcceptPolicyRequest>(data));
break;
}
case PacketId.SetAdvertiseData:
{
SetAdvertiseData?.Invoke(header, data);
break;
}
case PacketId.Connect:
{
Connect?.Invoke(header, MemoryMarshal.Read<ConnectRequest>(data));
break;
}
case PacketId.ConnectPrivate:
{
ConnectPrivate?.Invoke(header, MemoryMarshal.Read<ConnectPrivateRequest>(data));
break;
}
case PacketId.Scan:
{
Scan?.Invoke(header, MemoryMarshal.Read<ScanFilter>(data));
break;
}
// Proxy Packets
case PacketId.ProxyConfig:
{
ProxyConfig?.Invoke(header, MemoryMarshal.Read<ProxyConfig>(data));
break;
}
case PacketId.ProxyConnect:
{
ProxyConnect?.Invoke(header, MemoryMarshal.Read<ProxyConnectRequest>(data));
break;
}
case PacketId.ProxyConnectReply:
{
ProxyConnectReply?.Invoke(header, MemoryMarshal.Read<ProxyConnectResponse>(data));
break;
}
case PacketId.ProxyData:
{
(ProxyDataHeader packet, byte[] extraData) = ParseWithData<ProxyDataHeader>(data);
ProxyData?.Invoke(header, packet, extraData);
break;
}
case PacketId.ProxyDisconnect:
{
ProxyDisconnect?.Invoke(header, MemoryMarshal.Read<ProxyDisconnectMessage>(data));
break;
}
// Lifecycle Packets.
case PacketId.Ping:
{
Ping?.Invoke(header, MemoryMarshal.Read<PingMessage>(data));
break;
}
case PacketId.NetworkError:
{
NetworkError?.Invoke(header, MemoryMarshal.Read<NetworkErrorMessage>(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<LdnHeader, byte>(ref header).ToArray();
}
public byte[] Encode(PacketId type, byte[] data)
{
LdnHeader header = GetHeader(type, data.Length);
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
Array.Resize(ref result, result.Length + data.Length);
Array.Copy(data, 0, result, Marshal.SizeOf<LdnHeader>(), data.Length);
return result;
}
public byte[] Encode<T>(PacketId type, T packet) where T : unmanaged
{
byte[] packetData = SpanHelpers.AsSpan<T, byte>(ref packet).ToArray();
LdnHeader header = GetHeader(type, packetData.Length);
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
Array.Resize(ref result, result.Length + packetData.Length);
Array.Copy(packetData, 0, result, Marshal.SizeOf<LdnHeader>(), packetData.Length);
return result;
}
public byte[] Encode<T>(PacketId type, T packet, byte[] data) where T : unmanaged
{
byte[] packetData = SpanHelpers.AsSpan<T, byte>(ref packet).ToArray();
LdnHeader header = GetHeader(type, packetData.Length + data.Length);
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
Array.Resize(ref result, result.Length + packetData.Length + data.Length);
Array.Copy(packetData, 0, result, Marshal.SizeOf<LdnHeader>(), packetData.Length);
Array.Copy(data, 0, result, Marshal.SizeOf<LdnHeader>() + packetData.Length, data.Length);
return result;
}
}
}

View File

@ -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;
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x26, Pack = 1)]
struct ExternalProxyConfig
{
public Array16<byte> ProxyIp;
public AddressFamily AddressFamily;
public ushort ProxyPort;
public Array16<byte> Token;
}
}

View File

@ -0,0 +1,18 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 4)]
struct ExternalProxyConnectionState
{
public uint IpAddress;
public bool Connected;
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x28)]
struct ExternalProxyToken
{
public uint VirtualIp;
public Array16<byte> Token;
public Array16<byte> PhysicalIp;
public AddressFamily AddressFamily;
}
}

View File

@ -0,0 +1,20 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x16)]
struct InitializeMessage
{
// All 0 if we don't have an ID yet.
public Array16<byte> Id;
// All 0 if we don't have a mac yet.
public Array6<byte> MacAddress;
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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<byte> Passphrase;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,14 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Represents data sent over a transport layer.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x14)]
struct ProxyDataHeader
{
public ProxyInfo Info;
public uint DataLength; // Followed by the data with the specified byte length.
}
}

View File

@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
class ProxyDataPacket
{
public ProxyDataHeader Header;
public byte[] Data;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,20 @@
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
{
/// <summary>
/// Information included in all proxied communication.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 1)]
struct ProxyInfo
{
public uint SourceIpV4;
public ushort SourcePort;
public uint DestIpV4;
public ushort DestPort;
public ProtocolType Protocol;
}
}

View File

@ -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;
}
}
}

View File

@ -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<byte> 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<byte> PrivateIp;
public AddressFamily AddressFamily;
public ushort ExternalProxyPort;
public ushort InternalProxyPort;
}
}

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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
/// <remarks>
/// Advertise data is appended separately (remaining data in the buffer).
/// </remarks>
[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;
}
}

View File

@ -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;
}
}

View File

@ -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),
};

View File

@ -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<byte> 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<int>(optionValue) : MemoryMarshal.Read<byte>(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)
{

View File

@ -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<PollEvent> events, int timeoutMilliseconds, out int updatedCount)
{
List<Socket> readEvents = new();
List<Socket> writeEvents = new();
List<Socket> errorEvents = new();
List<ISocketImpl> readEvents = new();
List<ISocketImpl> writeEvents = new();
List<ISocketImpl> 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<PollEvent> events, int timeout, out int updatedCount)
{
List<Socket> readEvents = new();
List<Socket> writeEvents = new();
List<Socket> errorEvents = new();
List<ISocketImpl> readEvents = new();
List<ISocketImpl> writeEvents = new();
List<ISocketImpl> 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;
}
}
}

View File

@ -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<byte> buffer)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Receive(buffer);
}
public int Receive(Span<byte> buffer, SocketFlags flags)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Receive(buffer, flags);
}
public int Receive(Span<byte> buffer, SocketFlags flags, out SocketError socketError)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Receive(buffer, flags, out socketError);
}
public int ReceiveFrom(Span<byte> buffer, SocketFlags flags, ref EndPoint remoteEP)
{
EnsureNetworkInterfaceBound();
return BaseSocket.ReceiveFrom(buffer, flags, ref remoteEP);
}
public int Send(ReadOnlySpan<byte> buffer)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Send(buffer);
}
public int Send(ReadOnlySpan<byte> buffer, SocketFlags flags)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Send(buffer, flags);
}
public int Send(ReadOnlySpan<byte> buffer, SocketFlags flags, out SocketError socketError)
{
EnsureNetworkInterfaceBound();
return BaseSocket.Send(buffer, flags, out socketError);
}
public int SendTo(ReadOnlySpan<byte> 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();
}
}
}

View File

@ -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<byte> buffer);
int Receive(Span<byte> buffer, SocketFlags flags);
int Receive(Span<byte> buffer, SocketFlags flags, out SocketError socketError);
int ReceiveFrom(Span<byte> buffer, SocketFlags flags, ref EndPoint remoteEP);
int Send(ReadOnlySpan<byte> buffer);
int Send(ReadOnlySpan<byte> buffer, SocketFlags flags);
int Send(ReadOnlySpan<byte> buffer, SocketFlags flags, out SocketError socketError);
int SendTo(ReadOnlySpan<byte> 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();
}
}

View File

@ -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<ISocketImpl> readEvents, List<ISocketImpl> writeEvents, List<ISocketImpl> 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<ISocketImpl> removeFrom, List<Socket> selectedSockets, Func<LdnProxySocket, bool> 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);
}
}
}

View File

@ -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}");

View File

@ -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();

View File

@ -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")}]");

View File

@ -29,6 +29,7 @@
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="NetCoreServer" />
<PackageReference Include="Open.NAT.Core" />
</ItemGroup>
<ItemGroup>

View File

@ -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;
}

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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"

View File

@ -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; }

View File

@ -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<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
public event EventHandler<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
public readonly IObservableCache<ApplicationData, ulong> 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<LdnGameData> ldnGameDataArray = Array.Empty<LdnGameData>();
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<LdnGameData>()
});
}
}
else
{
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs()
{
LdnData = Array.Empty<LdnGameData>()
});
}
}
// Replace the currently stored DLC state for the game with the provided DLC state.
public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs)
{

View File

@ -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<string> Players { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
namespace Ryujinx.UI.App.Common
{
public class LdnGameDataReceivedEventArgs : EventArgs
{
public IEnumerable<LdnGameData> LdnData { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Ryujinx.UI.App.Common
{
[JsonSerializable(typeof(IEnumerable<LdnGameData>))]
internal partial class LdnGameDataSerializerContext : JsonSerializerContext
{
}
}

View File

@ -392,6 +392,21 @@ namespace Ryujinx.UI.Common.Configuration
/// </summary>
public string MultiplayerLanInterfaceId { get; set; }
/// <summary>
/// Disable P2p Toggle
/// </summary>
public bool MultiplayerDisableP2p { get; set; }
/// <summary>
/// Local network passphrase, for private networks.
/// </summary>
public string MultiplayerLdnPassphrase { get; set; }
/// <summary>
/// Custom LDN Server
/// </summary>
public string LdnServer { get; set; }
/// <summary>
/// Uses Hypervisor over JIT if available
/// </summary>

View File

@ -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<InputConfig>
{
new StandardKeyboardInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.WindowKeyboard,
Id = "0",
PlayerIndex = PlayerIndex.Player1,
ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
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<Key>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
{
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<Key>
{
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}");
}
}
}
}

View File

@ -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
{
/// <summary>
/// UI configuration section
/// </summary>
public class UISection
{
public class Columns
{
public ReactiveObject<bool> FavColumn { get; private set; }
public ReactiveObject<bool> IconColumn { get; private set; }
public ReactiveObject<bool> AppColumn { get; private set; }
public ReactiveObject<bool> DevColumn { get; private set; }
public ReactiveObject<bool> VersionColumn { get; private set; }
public ReactiveObject<bool> LdnInfoColumn { get; private set; }
public ReactiveObject<bool> TimePlayedColumn { get; private set; }
public ReactiveObject<bool> LastPlayedColumn { get; private set; }
public ReactiveObject<bool> FileExtColumn { get; private set; }
public ReactiveObject<bool> FileSizeColumn { get; private set; }
public ReactiveObject<bool> PathColumn { get; private set; }
public Columns()
{
FavColumn = new ReactiveObject<bool>();
IconColumn = new ReactiveObject<bool>();
AppColumn = new ReactiveObject<bool>();
DevColumn = new ReactiveObject<bool>();
VersionColumn = new ReactiveObject<bool>();
LdnInfoColumn = new ReactiveObject<bool>();
TimePlayedColumn = new ReactiveObject<bool>();
LastPlayedColumn = new ReactiveObject<bool>();
FileExtColumn = new ReactiveObject<bool>();
FileSizeColumn = new ReactiveObject<bool>();
PathColumn = new ReactiveObject<bool>();
}
}
public class ColumnSortSettings
{
public ReactiveObject<int> SortColumnId { get; private set; }
public ReactiveObject<bool> SortAscending { get; private set; }
public ColumnSortSettings()
{
SortColumnId = new ReactiveObject<int>();
SortAscending = new ReactiveObject<bool>();
}
}
/// <summary>
/// Used to toggle which file types are shown in the UI
/// </summary>
public class ShownFileTypeSettings
{
public ReactiveObject<bool> NSP { get; private set; }
public ReactiveObject<bool> PFS0 { get; private set; }
public ReactiveObject<bool> XCI { get; private set; }
public ReactiveObject<bool> NCA { get; private set; }
public ReactiveObject<bool> NRO { get; private set; }
public ReactiveObject<bool> NSO { get; private set; }
public ShownFileTypeSettings()
{
NSP = new ReactiveObject<bool>();
PFS0 = new ReactiveObject<bool>();
XCI = new ReactiveObject<bool>();
NCA = new ReactiveObject<bool>();
NRO = new ReactiveObject<bool>();
NSO = new ReactiveObject<bool>();
}
}
// <summary>
/// Determines main window start-up position, size and state
///<summary>
public class WindowStartupSettings
{
public ReactiveObject<int> WindowSizeWidth { get; private set; }
public ReactiveObject<int> WindowSizeHeight { get; private set; }
public ReactiveObject<int> WindowPositionX { get; private set; }
public ReactiveObject<int> WindowPositionY { get; private set; }
public ReactiveObject<bool> WindowMaximized { get; private set; }
public WindowStartupSettings()
{
WindowSizeWidth = new ReactiveObject<int>();
WindowSizeHeight = new ReactiveObject<int>();
WindowPositionX = new ReactiveObject<int>();
WindowPositionY = new ReactiveObject<int>();
WindowMaximized = new ReactiveObject<bool>();
}
}
/// <summary>
/// Used to toggle columns in the GUI
/// </summary>
public Columns GuiColumns { get; private set; }
/// <summary>
/// Used to configure column sort settings in the GUI
/// </summary>
public ColumnSortSettings ColumnSort { get; private set; }
/// <summary>
/// A list of directories containing games to be used to load games into the games list
/// </summary>
public ReactiveObject<List<string>> GameDirs { get; private set; }
/// <summary>
/// A list of directories containing DLC/updates the user wants to autoload during library refreshes
/// </summary>
public ReactiveObject<List<string>> AutoloadDirs { get; private set; }
/// <summary>
/// A list of file types to be hidden in the games List
/// </summary>
public ShownFileTypeSettings ShownFileTypes { get; private set; }
/// <summary>
/// Determines main window start-up position, size and state
/// </summary>
public WindowStartupSettings WindowStartup { get; private set; }
/// <summary>
/// Language Code for the UI
/// </summary>
public ReactiveObject<string> LanguageCode { get; private set; }
/// <summary>
/// Selects the base style
/// </summary>
public ReactiveObject<string> BaseStyle { get; private set; }
/// <summary>
/// Start games in fullscreen mode
/// </summary>
public ReactiveObject<bool> StartFullscreen { get; private set; }
/// <summary>
/// Hide / Show Console Window
/// </summary>
public ReactiveObject<bool> ShowConsole { get; private set; }
/// <summary>
/// View Mode of the Game list
/// </summary>
public ReactiveObject<int> GameListViewMode { get; private set; }
/// <summary>
/// Show application name in Grid Mode
/// </summary>
public ReactiveObject<bool> ShowNames { get; private set; }
/// <summary>
/// Sets App Icon Size in Grid Mode
/// </summary>
public ReactiveObject<int> GridSize { get; private set; }
/// <summary>
/// Sorts Apps in Grid Mode
/// </summary>
public ReactiveObject<int> ApplicationSort { get; private set; }
/// <summary>
/// Sets if Grid is ordered in Ascending Order
/// </summary>
public ReactiveObject<bool> IsAscendingOrder { get; private set; }
public UISection()
{
GuiColumns = new Columns();
ColumnSort = new ColumnSortSettings();
GameDirs = new ReactiveObject<List<string>>();
AutoloadDirs = new ReactiveObject<List<string>>();
ShownFileTypes = new ShownFileTypeSettings();
WindowStartup = new WindowStartupSettings();
BaseStyle = new ReactiveObject<string>();
StartFullscreen = new ReactiveObject<bool>();
GameListViewMode = new ReactiveObject<int>();
ShowNames = new ReactiveObject<bool>();
GridSize = new ReactiveObject<int>();
ApplicationSort = new ReactiveObject<int>();
IsAscendingOrder = new ReactiveObject<bool>();
LanguageCode = new ReactiveObject<string>();
ShowConsole = new ReactiveObject<bool>();
ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue);
}
}
/// <summary>
/// Logger configuration section
/// </summary>
public class LoggerSection
{
/// <summary>
/// Enables printing debug log messages
/// </summary>
public ReactiveObject<bool> EnableDebug { get; private set; }
/// <summary>
/// Enables printing stub log messages
/// </summary>
public ReactiveObject<bool> EnableStub { get; private set; }
/// <summary>
/// Enables printing info log messages
/// </summary>
public ReactiveObject<bool> EnableInfo { get; private set; }
/// <summary>
/// Enables printing warning log messages
/// </summary>
public ReactiveObject<bool> EnableWarn { get; private set; }
/// <summary>
/// Enables printing error log messages
/// </summary>
public ReactiveObject<bool> EnableError { get; private set; }
/// <summary>
/// Enables printing trace log messages
/// </summary>
public ReactiveObject<bool> EnableTrace { get; private set; }
/// <summary>
/// Enables printing guest log messages
/// </summary>
public ReactiveObject<bool> EnableGuest { get; private set; }
/// <summary>
/// Enables printing FS access log messages
/// </summary>
public ReactiveObject<bool> EnableFsAccessLog { get; private set; }
/// <summary>
/// Controls which log messages are written to the log targets
/// </summary>
public ReactiveObject<LogClass[]> FilteredClasses { get; private set; }
/// <summary>
/// Enables or disables logging to a file on disk
/// </summary>
public ReactiveObject<bool> EnableFileLog { get; private set; }
/// <summary>
/// Controls which OpenGL log messages are recorded in the log
/// </summary>
public ReactiveObject<GraphicsDebugLevel> GraphicsDebugLevel { get; private set; }
public LoggerSection()
{
EnableDebug = new ReactiveObject<bool>();
EnableDebug.LogChangesToValue(nameof(EnableDebug));
EnableStub = new ReactiveObject<bool>();
EnableInfo = new ReactiveObject<bool>();
EnableWarn = new ReactiveObject<bool>();
EnableError = new ReactiveObject<bool>();
EnableTrace = new ReactiveObject<bool>();
EnableGuest = new ReactiveObject<bool>();
EnableFsAccessLog = new ReactiveObject<bool>();
FilteredClasses = new ReactiveObject<LogClass[]>();
EnableFileLog = new ReactiveObject<bool>();
EnableFileLog.LogChangesToValue(nameof(EnableFileLog));
GraphicsDebugLevel = new ReactiveObject<GraphicsDebugLevel>();
}
}
/// <summary>
/// System configuration section
/// </summary>
public class SystemSection
{
/// <summary>
/// Change System Language
/// </summary>
public ReactiveObject<Language> Language { get; private set; }
/// <summary>
/// Change System Region
/// </summary>
public ReactiveObject<Region> Region { get; private set; }
/// <summary>
/// Change System TimeZone
/// </summary>
public ReactiveObject<string> TimeZone { get; private set; }
/// <summary>
/// System Time Offset in Seconds
/// </summary>
public ReactiveObject<long> SystemTimeOffset { get; private set; }
/// <summary>
/// Enables or disables Docked Mode
/// </summary>
public ReactiveObject<bool> EnableDockedMode { get; private set; }
/// <summary>
/// Enables or disables persistent profiled translation cache
/// </summary>
public ReactiveObject<bool> EnablePtc { get; private set; }
/// <summary>
/// Enables or disables low-power persistent profiled translation cache loading
/// </summary>
public ReactiveObject<bool> EnableLowPowerPtc { get; private set; }
/// <summary>
/// Enables or disables guest Internet access
/// </summary>
public ReactiveObject<bool> EnableInternetAccess { get; private set; }
/// <summary>
/// Enables integrity checks on Game content files
/// </summary>
public ReactiveObject<bool> EnableFsIntegrityChecks { get; private set; }
/// <summary>
/// Enables FS access log output to the console. Possible modes are 0-3
/// </summary>
public ReactiveObject<int> FsGlobalAccessLogMode { get; private set; }
/// <summary>
/// The selected audio backend
/// </summary>
public ReactiveObject<AudioBackend> AudioBackend { get; private set; }
/// <summary>
/// The audio backend volume
/// </summary>
public ReactiveObject<float> AudioVolume { get; private set; }
/// <summary>
/// The selected memory manager mode
/// </summary>
public ReactiveObject<MemoryManagerMode> MemoryManagerMode { get; private set; }
/// <summary>
/// Defines the amount of RAM available on the emulated system, and how it is distributed
/// </summary>
public ReactiveObject<MemoryConfiguration> DramSize { get; private set; }
/// <summary>
/// Enable or disable ignoring missing services
/// </summary>
public ReactiveObject<bool> IgnoreMissingServices { get; private set; }
/// <summary>
/// Uses Hypervisor over JIT if available
/// </summary>
public ReactiveObject<bool> UseHypervisor { get; private set; }
public SystemSection()
{
Language = new ReactiveObject<Language>();
Language.LogChangesToValue(nameof(Language));
Region = new ReactiveObject<Region>();
Region.LogChangesToValue(nameof(Region));
TimeZone = new ReactiveObject<string>();
TimeZone.LogChangesToValue(nameof(TimeZone));
SystemTimeOffset = new ReactiveObject<long>();
SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset));
EnableDockedMode = new ReactiveObject<bool>();
EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode));
EnablePtc = new ReactiveObject<bool>();
EnablePtc.LogChangesToValue(nameof(EnablePtc));
EnableLowPowerPtc = new ReactiveObject<bool>();
EnableLowPowerPtc.LogChangesToValue(nameof(EnableLowPowerPtc));
EnableLowPowerPtc.Event += (_, evnt)
=> Optimizations.LowPower = evnt.NewValue;
EnableInternetAccess = new ReactiveObject<bool>();
EnableInternetAccess.LogChangesToValue(nameof(EnableInternetAccess));
EnableFsIntegrityChecks = new ReactiveObject<bool>();
EnableFsIntegrityChecks.LogChangesToValue(nameof(EnableFsIntegrityChecks));
FsGlobalAccessLogMode = new ReactiveObject<int>();
FsGlobalAccessLogMode.LogChangesToValue(nameof(FsGlobalAccessLogMode));
AudioBackend = new ReactiveObject<AudioBackend>();
AudioBackend.LogChangesToValue(nameof(AudioBackend));
MemoryManagerMode = new ReactiveObject<MemoryManagerMode>();
MemoryManagerMode.LogChangesToValue(nameof(MemoryManagerMode));
DramSize = new ReactiveObject<MemoryConfiguration>();
DramSize.LogChangesToValue(nameof(DramSize));
IgnoreMissingServices = new ReactiveObject<bool>();
IgnoreMissingServices.LogChangesToValue(nameof(IgnoreMissingServices));
AudioVolume = new ReactiveObject<float>();
AudioVolume.LogChangesToValue(nameof(AudioVolume));
UseHypervisor = new ReactiveObject<bool>();
UseHypervisor.LogChangesToValue(nameof(UseHypervisor));
}
}
/// <summary>
/// Hid configuration section
/// </summary>
public class HidSection
{
/// <summary>
/// Enable or disable keyboard support (Independent from controllers binding)
/// </summary>
public ReactiveObject<bool> EnableKeyboard { get; private set; }
/// <summary>
/// Enable or disable mouse support (Independent from controllers binding)
/// </summary>
public ReactiveObject<bool> EnableMouse { get; private set; }
/// <summary>
/// Hotkey Keyboard Bindings
/// </summary>
public ReactiveObject<KeyboardHotkeys> Hotkeys { get; private set; }
/// <summary>
/// Input device configuration.
/// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed.
/// TODO: Implement a ReactiveList class.
/// </summary>
public ReactiveObject<List<InputConfig>> InputConfig { get; private set; }
public HidSection()
{
EnableKeyboard = new ReactiveObject<bool>();
EnableMouse = new ReactiveObject<bool>();
Hotkeys = new ReactiveObject<KeyboardHotkeys>();
InputConfig = new ReactiveObject<List<InputConfig>>();
}
}
/// <summary>
/// Graphics configuration section
/// </summary>
public class GraphicsSection
{
/// <summary>
/// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime.
/// </summary>
public ReactiveObject<BackendThreading> BackendThreading { get; private set; }
/// <summary>
/// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide.
/// </summary>
public ReactiveObject<float> MaxAnisotropy { get; private set; }
/// <summary>
/// Aspect Ratio applied to the renderer window.
/// </summary>
public ReactiveObject<AspectRatio> AspectRatio { get; private set; }
/// <summary>
/// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead.
/// </summary>
public ReactiveObject<int> ResScale { get; private set; }
/// <summary>
/// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1.
/// </summary>
public ReactiveObject<float> ResScaleCustom { get; private set; }
/// <summary>
/// Dumps shaders in this local directory
/// </summary>
public ReactiveObject<string> ShadersDumpPath { get; private set; }
/// <summary>
/// Enables or disables Vertical Sync
/// </summary>
public ReactiveObject<bool> EnableVsync { get; private set; }
/// <summary>
/// Enables or disables Shader cache
/// </summary>
public ReactiveObject<bool> EnableShaderCache { get; private set; }
/// <summary>
/// Enables or disables texture recompression
/// </summary>
public ReactiveObject<bool> EnableTextureRecompression { get; private set; }
/// <summary>
/// Enables or disables Macro high-level emulation
/// </summary>
public ReactiveObject<bool> EnableMacroHLE { get; private set; }
/// <summary>
/// Enables or disables color space passthrough, if available.
/// </summary>
public ReactiveObject<bool> EnableColorSpacePassthrough { get; private set; }
/// <summary>
/// Graphics backend
/// </summary>
public ReactiveObject<GraphicsBackend> GraphicsBackend { get; private set; }
/// <summary>
/// Applies anti-aliasing to the renderer.
/// </summary>
public ReactiveObject<AntiAliasing> AntiAliasing { get; private set; }
/// <summary>
/// Sets the framebuffer upscaling type.
/// </summary>
public ReactiveObject<ScalingFilter> ScalingFilter { get; private set; }
/// <summary>
/// Sets the framebuffer upscaling level.
/// </summary>
public ReactiveObject<int> ScalingFilterLevel { get; private set; }
/// <summary>
/// Preferred GPU
/// </summary>
public ReactiveObject<string> PreferredGpu { get; private set; }
public GraphicsSection()
{
BackendThreading = new ReactiveObject<BackendThreading>();
BackendThreading.LogChangesToValue(nameof(BackendThreading));
ResScale = new ReactiveObject<int>();
ResScale.LogChangesToValue(nameof(ResScale));
ResScaleCustom = new ReactiveObject<float>();
ResScaleCustom.LogChangesToValue(nameof(ResScaleCustom));
MaxAnisotropy = new ReactiveObject<float>();
MaxAnisotropy.LogChangesToValue(nameof(MaxAnisotropy));
AspectRatio = new ReactiveObject<AspectRatio>();
AspectRatio.LogChangesToValue(nameof(AspectRatio));
ShadersDumpPath = new ReactiveObject<string>();
EnableVsync = new ReactiveObject<bool>();
EnableVsync.LogChangesToValue(nameof(EnableVsync));
EnableShaderCache = new ReactiveObject<bool>();
EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache));
EnableTextureRecompression = new ReactiveObject<bool>();
EnableTextureRecompression.LogChangesToValue(nameof(EnableTextureRecompression));
GraphicsBackend = new ReactiveObject<GraphicsBackend>();
GraphicsBackend.LogChangesToValue(nameof(GraphicsBackend));
PreferredGpu = new ReactiveObject<string>();
PreferredGpu.LogChangesToValue(nameof(PreferredGpu));
EnableMacroHLE = new ReactiveObject<bool>();
EnableMacroHLE.LogChangesToValue(nameof(EnableMacroHLE));
EnableColorSpacePassthrough = new ReactiveObject<bool>();
EnableColorSpacePassthrough.LogChangesToValue(nameof(EnableColorSpacePassthrough));
AntiAliasing = new ReactiveObject<AntiAliasing>();
AntiAliasing.LogChangesToValue(nameof(AntiAliasing));
ScalingFilter = new ReactiveObject<ScalingFilter>();
ScalingFilter.LogChangesToValue(nameof(ScalingFilter));
ScalingFilterLevel = new ReactiveObject<int>();
ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel));
}
}
/// <summary>
/// Multiplayer configuration section
/// </summary>
public class MultiplayerSection
{
/// <summary>
/// GUID for the network interface used by LAN (or 0 for default)
/// </summary>
public ReactiveObject<string> LanInterfaceId { get; private set; }
/// <summary>
/// Multiplayer Mode
/// </summary>
public ReactiveObject<MultiplayerMode> Mode { get; private set; }
/// <summary>
/// Disable P2P
/// </summary>
public ReactiveObject<bool> DisableP2p { get; private set; }
/// <summary>
/// LDN PassPhrase
/// </summary>
public ReactiveObject<string> LdnPassphrase { get; private set; }
/// <summary>
/// LDN Server
/// </summary>
public ReactiveObject<string> LdnServer { get; private set; }
public MultiplayerSection()
{
LanInterfaceId = new ReactiveObject<string>();
Mode = new ReactiveObject<MultiplayerMode>();
Mode.LogChangesToValue(nameof(MultiplayerMode));
DisableP2p = new ReactiveObject<bool>();
DisableP2p.LogChangesToValue(nameof(DisableP2p));
LdnPassphrase = new ReactiveObject<string>();
LdnPassphrase.LogChangesToValue(nameof(LdnPassphrase));
LdnServer = new ReactiveObject<string>();
LdnServer.LogChangesToValue(nameof(LdnServer));
}
}
/// <summary>
/// The default configuration instance
/// </summary>
public static ConfigurationState Instance { get; private set; }
/// <summary>
/// The UI section
/// </summary>
public UISection UI { get; private set; }
/// <summary>
/// The Logger section
/// </summary>
public LoggerSection Logger { get; private set; }
/// <summary>
/// The System section
/// </summary>
public SystemSection System { get; private set; }
/// <summary>
/// The Graphics section
/// </summary>
public GraphicsSection Graphics { get; private set; }
/// <summary>
/// The Hid section
/// </summary>
public HidSection Hid { get; private set; }
/// <summary>
/// The Multiplayer section
/// </summary>
public MultiplayerSection Multiplayer { get; private set; }
/// <summary>
/// Enables or disables Discord Rich Presence
/// </summary>
public ReactiveObject<bool> EnableDiscordIntegration { get; private set; }
/// <summary>
/// Checks for updates when Ryujinx starts when enabled
/// </summary>
public ReactiveObject<bool> CheckUpdatesOnStart { get; private set; }
/// <summary>
/// Show "Confirm Exit" Dialog
/// </summary>
public ReactiveObject<bool> ShowConfirmExit { get; private set; }
/// <summary>
/// Ignore Applet
/// </summary>
public ReactiveObject<bool> IgnoreApplet { get; private set; }
/// <summary>
/// Enables or disables save window size, position and state on close.
/// </summary>
public ReactiveObject<bool> RememberWindowState { get; private set; }
/// <summary>
/// Enables or disables the redesigned title bar
/// </summary>
public ReactiveObject<bool> ShowTitleBar { get; private set; }
/// <summary>
/// Enables hardware-accelerated rendering for Avalonia
/// </summary>
public ReactiveObject<bool> EnableHardwareAcceleration { get; private set; }
/// <summary>
/// Hide Cursor on Idle
/// </summary>
public ReactiveObject<HideCursorMode> 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<bool>();
CheckUpdatesOnStart = new ReactiveObject<bool>();
ShowConfirmExit = new ReactiveObject<bool>();
IgnoreApplet = new ReactiveObject<bool>();
IgnoreApplet.LogChangesToValue(nameof(IgnoreApplet));
RememberWindowState = new ReactiveObject<bool>();
ShowTitleBar = new ReactiveObject<bool>();
EnableHardwareAcceleration = new ReactiveObject<bool>();
HideCursor = new ReactiveObject<HideCursorMode>();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<bool> e)
{
Logger.SetEnable(LogLevel.Debug, e.NewValue);
}
private static void ReloadEnableStub(object sender, ReactiveEventArgs<bool> e)
{
Logger.SetEnable(LogLevel.Stub, e.NewValue);
}
private static void ReloadEnableInfo(object sender, ReactiveEventArgs<bool> e)
{
Logger.SetEnable(LogLevel.Info, e.NewValue);
}
private static void ReloadEnableWarning(object sender, ReactiveEventArgs<bool> e)
{
Logger.SetEnable(LogLevel.Warning, e.NewValue);
}
private static void ReloadEnableError(object sender, ReactiveEventArgs<bool> e)
{
Logger.SetEnable(LogLevel.Error, e.NewValue);
}
private static void ReloadEnableTrace(object sender, ReactiveEventArgs<bool> e)
{
Logger.SetEnable(LogLevel.Trace, e.NewValue);
}
private static void ReloadEnableGuest(object sender, ReactiveEventArgs<bool> e)
{
Logger.SetEnable(LogLevel.Guest, e.NewValue);
}
private static void ReloadEnableFsAccessLog(object sender, ReactiveEventArgs<bool> e)
{
Logger.SetEnable(LogLevel.AccessLog, e.NewValue);
}
private static void ReloadFilteredClasses(object sender, ReactiveEventArgs<LogClass[]> e)
{
bool noFilter = e.NewValue.Length == 0;
foreach (var logClass in Enum.GetValues<LogClass>())
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<bool> e)
{
if (e.NewValue)
{
string logDir = AppDataManager.LogsDirPath;
FileStream logFile = null;
if (!string.IsNullOrEmpty(logDir))
foreach (var logClass in Enum.GetValues<LogClass>())
{
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");
}
};
}
}
}

View File

@ -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; }

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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<string> e)
{
Device.Configuration.MultiplayerLdnPassphrase = e.NewValue;
}
private void UpdateLdnServerState(object sender, ReactiveEventArgs<string> e)
{
Device.Configuration.MultiplayerLdnServer = e.NewValue;
}
private void UpdateDisableP2pState(object sender, ReactiveEventArgs<bool> 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)";

View File

@ -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": "تم الاستخراج بنجاح.",

View File

@ -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.",

View File

@ -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": "Η εξαγωγή ολοκληρώθηκε με επιτυχία.",

View File

@ -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>\""
}

View File

@ -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.",

View File

@ -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.",

View File

@ -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": "החילוץ הושלם בהצלחה.",

View File

@ -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.",

View File

@ -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": "展開が正常終了しました",

Some files were not shown because too many files have changed in this diff Show More