Merge branch 'master' into 4304-missing-updates

This commit is contained in:
Tomas Voracek 2023-10-28 21:58:02 +02:00 committed by GitHub
commit c0b2799b44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 1666 additions and 89 deletions

View file

@ -18,12 +18,13 @@
<PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" /> <PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" />
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" /> <PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
<PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" /> <PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" />
<PackageVersion Include="LibHac" Version="0.18.0" /> <PackageVersion Include="LibHac" Version="0.19.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" /> <PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" /> <PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
<PackageVersion Include="NetCoreServer" Version="7.0.0" />
<PackageVersion Include="NUnit" Version="3.13.3" /> <PackageVersion Include="NUnit" Version="3.13.3" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageVersion Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageVersion Include="OpenTK.Core" Version="4.7.7" /> <PackageVersion Include="OpenTK.Core" Version="4.7.7" />

View file

@ -141,4 +141,5 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system. - [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation. - [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes.
- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation. - [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.

View file

@ -190,6 +190,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState; ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
@ -408,6 +409,11 @@ namespace Ryujinx.Ava
}); });
} }
private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs<bool> e)
{
Device.Configuration.EnableInternetAccess = e.NewValue;
}
private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs<string> e) private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs<string> e)
{ {
Device.Configuration.MultiplayerLanInterfaceId = e.NewValue; Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;

View file

@ -650,7 +650,7 @@
"UserEditorTitle": "Edit User", "UserEditorTitle": "Edit User",
"UserEditorTitleCreate": "Create User", "UserEditorTitleCreate": "Create User",
"SettingsTabNetworkInterface": "Network Interface:", "SettingsTabNetworkInterface": "Network Interface:",
"NetworkInterfaceTooltip": "The network interface used for LAN features", "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features",
"NetworkInterfaceDefault": "Default", "NetworkInterfaceDefault": "Default",
"PackagingShaders": "Packaging Shaders", "PackagingShaders": "Packaging Shaders",
"AboutChangelogButton": "View Changelog on GitHub", "AboutChangelogButton": "View Changelog on GitHub",

View file

@ -173,7 +173,7 @@ namespace Ryujinx.Ava.Common
string extension = Path.GetExtension(titleFilePath).ToLower(); string extension = Path.GetExtension(titleFilePath).ToLower();
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci") if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
{ {
PartitionFileSystem pfs; IFileSystem pfs;
if (extension == ".xci") if (extension == ".xci")
{ {
@ -181,7 +181,9 @@ namespace Ryujinx.Ava.Common
} }
else else
{ {
pfs = new PartitionFileSystem(file.AsStorage()); var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
pfs = pfsTemp;
} }
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))

View file

@ -126,7 +126,8 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath); using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); PartitionFileSystem partitionFileSystem = new();
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
_virtualFileSystem.ImportTickets(partitionFileSystem); _virtualFileSystem.ImportTickets(partitionFileSystem);
@ -232,7 +233,8 @@ namespace Ryujinx.Ava.UI.ViewModels
using FileStream containerFile = File.OpenRead(path); using FileStream containerFile = File.OpenRead(path);
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage()); PartitionFileSystem partitionFileSystem = new();
partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
bool containsDownloadableContent = false; bool containsDownloadableContent = false;
_virtualFileSystem.ImportTickets(partitionFileSystem); _virtualFileSystem.ImportTickets(partitionFileSystem);

View file

@ -170,7 +170,9 @@ namespace Ryujinx.Ava.UI.ViewModels
try try
{ {
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, new PartitionFileSystem(file.AsStorage()), TitleId.ToString("x16"), 0); var pfs = new PartitionFileSystem();
pfs.Initialize(file.AsStorage()).ThrowIfFailure();
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0);
if (controlNca != null && patchNca != null) if (controlNca != null && patchNca != null)
{ {

View file

@ -3,5 +3,6 @@
public enum MultiplayerMode public enum MultiplayerMode
{ {
Disabled, Disabled,
LdnMitm,
} }
} }

View file

@ -12,6 +12,13 @@ namespace Ryujinx.Common.SystemInfo
{ {
internal MacOSSystemInfo() internal MacOSSystemInfo()
{ {
if (SysctlByName("kern.osversion", out string buildRevision) != 0)
{
buildRevision = "Unknown Build";
}
OsDescription = $"macOS {Environment.OSVersion.Version} ({buildRevision}) ({RuntimeInformation.OSArchitecture})";
string cpuName = GetCpuidCpuName(); string cpuName = GetCpuidCpuName();
if (cpuName == null && SysctlByName("machdep.cpu.brand_string", out cpuName) != 0) if (cpuName == null && SysctlByName("machdep.cpu.brand_string", out cpuName) != 0)

View file

@ -74,5 +74,10 @@ namespace Ryujinx.Common.Utilities
{ {
return ConvertIpv4Address(IPAddress.Parse(ipAddress)); return ConvertIpv4Address(IPAddress.Parse(ipAddress));
} }
public static IPAddress ConvertUint(uint ipAddress)
{
return new IPAddress(new byte[] { (byte)((ipAddress >> 24) & 0xFF), (byte)((ipAddress >> 16) & 0xFF), (byte)((ipAddress >> 8) & 0xFF), (byte)(ipAddress & 0xFF) });
}
} }
} }

View file

@ -766,7 +766,10 @@ namespace Ryujinx.Graphics.Shader.Instructions
flags |= offset == TexOffset.Ptp ? TextureFlags.Offsets : TextureFlags.Offset; flags |= offset == TexOffset.Ptp ? TextureFlags.Offsets : TextureFlags.Offset;
} }
if (!hasDepthCompare)
{
sourcesList.Add(Const((int)component)); sourcesList.Add(Const((int)component));
}
Operand[] sources = sourcesList.ToArray(); Operand[] sources = sourcesList.ToArray();
Operand[] dests = new Operand[BitOperations.PopCount((uint)componentMask)]; Operand[] dests = new Operand[BitOperations.PopCount((uint)componentMask)];

View file

@ -198,7 +198,7 @@ namespace Ryujinx.HLE.FileSystem
{ {
using var ncaFile = new UniqueRef<IFile>(); using var ncaFile = new UniqueRef<IFile>();
fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read); fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
if (nca.Header.ContentType != NcaContentType.Meta) if (nca.Header.ContentType != NcaContentType.Meta)
{ {
@ -210,7 +210,7 @@ namespace Ryujinx.HLE.FileSystem
using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel); using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel);
using var cnmtFile = new UniqueRef<IFile>(); using var cnmtFile = new UniqueRef<IFile>();
pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read); pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
var cnmt = new Cnmt(cnmtFile.Get.AsStream()); var cnmt = new Cnmt(cnmtFile.Get.AsStream());
if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId) if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId)
@ -220,7 +220,7 @@ namespace Ryujinx.HLE.FileSystem
string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower(); string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower();
AddAocItem(cnmt.TitleId, containerPath, $"{ncaId}.nca", true); AddAocItem(cnmt.TitleId, containerPath, $"/{ncaId}.nca", true);
} }
} }
@ -238,7 +238,8 @@ namespace Ryujinx.HLE.FileSystem
if (!mergedToContainer) if (!mergedToContainer)
{ {
using FileStream fileStream = File.OpenRead(containerPath); using FileStream fileStream = File.OpenRead(containerPath);
using PartitionFileSystem partitionFileSystem = new(fileStream.AsStorage()); using PartitionFileSystem partitionFileSystem = new();
partitionFileSystem.Initialize(fileStream.AsStorage()).ThrowIfFailure();
_virtualFileSystem.ImportTickets(partitionFileSystem); _virtualFileSystem.ImportTickets(partitionFileSystem);
} }
@ -259,17 +260,17 @@ namespace Ryujinx.HLE.FileSystem
{ {
var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read); var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read);
using var ncaFile = new UniqueRef<IFile>(); using var ncaFile = new UniqueRef<IFile>();
PartitionFileSystem pfs;
switch (Path.GetExtension(aoc.ContainerPath)) switch (Path.GetExtension(aoc.ContainerPath))
{ {
case ".xci": case ".xci":
pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure); var xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
break; break;
case ".nsp": case ".nsp":
pfs = new PartitionFileSystem(file.AsStorage()); var pfs = new PartitionFileSystem();
pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read); pfs.Initialize(file.AsStorage());
pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
break; break;
default: default:
return false; // Print error? return false; // Print error?
@ -606,11 +607,11 @@ namespace Ryujinx.HLE.FileSystem
if (filesystem.FileExists($"{path}/00")) if (filesystem.FileExists($"{path}/00"))
{ {
filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode); filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode).ThrowIfFailure();
} }
else else
{ {
filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode); filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode).ThrowIfFailure();
} }
return file.Release(); return file.Release();

View file

@ -7,6 +7,7 @@ using LibHac.Fs.Shim;
using LibHac.FsSrv; using LibHac.FsSrv;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.Ncm; using LibHac.Ncm;
using LibHac.Sdmmc;
using LibHac.Spl; using LibHac.Spl;
using LibHac.Tools.Es; using LibHac.Tools.Es;
using LibHac.Tools.Fs; using LibHac.Tools.Fs;
@ -32,7 +33,7 @@ namespace Ryujinx.HLE.FileSystem
public KeySet KeySet { get; private set; } public KeySet KeySet { get; private set; }
public EmulatedGameCard GameCard { get; private set; } public EmulatedGameCard GameCard { get; private set; }
public EmulatedSdCard SdCard { get; private set; } public SdmmcApi SdCard { get; private set; }
public ModLoader ModLoader { get; private set; } public ModLoader ModLoader { get; private set; }
private readonly ConcurrentDictionary<ulong, Stream> _romFsByPid; private readonly ConcurrentDictionary<ulong, Stream> _romFsByPid;
@ -198,15 +199,15 @@ namespace Ryujinx.HLE.FileSystem
fsServerObjects.FsCreators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator(); fsServerObjects.FsCreators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator();
GameCard = fsServerObjects.GameCard; GameCard = fsServerObjects.GameCard;
SdCard = fsServerObjects.SdCard; SdCard = fsServerObjects.Sdmmc;
SdCard.SetSdCardInsertionStatus(true); SdCard.SetSdCardInserted(true);
var fsServerConfig = new FileSystemServerConfig var fsServerConfig = new FileSystemServerConfig
{ {
DeviceOperator = fsServerObjects.DeviceOperator,
ExternalKeySet = KeySet.ExternalKeySet, ExternalKeySet = KeySet.ExternalKeySet,
FsCreators = fsServerObjects.FsCreators, FsCreators = fsServerObjects.FsCreators,
StorageDeviceManagerFactory = fsServerObjects.StorageDeviceManagerFactory,
RandomGenerator = randomGenerator, RandomGenerator = randomGenerator,
}; };
@ -263,7 +264,16 @@ namespace Ryujinx.HLE.FileSystem
if (result.IsSuccess()) if (result.IsSuccess())
{ {
Ticket ticket = new(ticketFile.Get.AsStream()); // When reading a file from a Sha256PartitionFileSystem, you can't start a read in the middle
// of the hashed portion (usually the first 0x200 bytes) of the file and end the read after
// the end of the hashed portion, so we read the ticket file using a single read.
byte[] ticketData = new byte[0x2C0];
result = ticketFile.Get.Read(out long bytesRead, 0, ticketData);
if (result.IsFailure() || bytesRead != ticketData.Length)
continue;
Ticket ticket = new(new MemoryStream(ticketData));
var titleKey = ticket.GetTitleKey(KeySet); var titleKey = ticket.GetTitleKey(KeySet);
if (titleKey != null) if (titleKey != null)

View file

@ -101,7 +101,7 @@ namespace Ryujinx.HLE
/// <summary> /// <summary>
/// Control if the guest application should be told that there is a Internet connection available. /// Control if the guest application should be told that there is a Internet connection available.
/// </summary> /// </summary>
internal readonly bool EnableInternetAccess; public bool EnableInternetAccess { internal get; set; }
/// <summary> /// <summary>
/// Control LibHac's integrity check level. /// Control LibHac's integrity check level.

View file

@ -533,7 +533,9 @@ namespace Ryujinx.HLE.HOS
Logger.Info?.Print(LogClass.ModLoader, "Using replacement ExeFS partition"); Logger.Info?.Print(LogClass.ModLoader, "Using replacement ExeFS partition");
exefs = new PartitionFileSystem(mods.ExefsContainers[0].Path.OpenRead().AsStorage()); var pfs = new PartitionFileSystem();
pfs.Initialize(mods.ExefsContainers[0].Path.OpenRead().AsStorage()).ThrowIfFailure();
exefs = pfs;
return true; return true;
} }

View file

@ -26,7 +26,9 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
try try
{ {
LocalStorage storage = new(pfsPath, FileAccess.Read, FileMode.Open); LocalStorage storage = new(pfsPath, FileAccess.Read, FileMode.Open);
using SharedRef<LibHac.Fs.Fsa.IFileSystem> nsp = new(new PartitionFileSystem(storage)); var pfs = new PartitionFileSystem();
using SharedRef<LibHac.Fs.Fsa.IFileSystem> nsp = new(pfs);
pfs.Initialize(storage).ThrowIfFailure();
ImportTitleKeysFromNsp(nsp.Get, context.Device.System.KeySet); ImportTitleKeysFromNsp(nsp.Get, context.Device.System.KeySet);
@ -90,7 +92,8 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
try try
{ {
PartitionFileSystem nsp = new(pfsFile.AsStorage()); PartitionFileSystem nsp = new();
nsp.Initialize(pfsFile.AsStorage()).ThrowIfFailure();
ImportTitleKeysFromNsp(nsp, context.Device.System.KeySet); ImportTitleKeysFromNsp(nsp, context.Device.System.KeySet);

View file

@ -1,6 +1,7 @@
using LibHac; using LibHac;
using LibHac.Common; using LibHac.Common;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Fsa;
using Path = LibHac.FsSrv.Sf.Path; using Path = LibHac.FsSrv.Sf.Path;
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
@ -202,6 +203,16 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
return (ResultCode)result.Value; return (ResultCode)result.Value;
} }
[CommandCmif(16)]
public ResultCode GetFileSystemAttribute(ServiceCtx context)
{
Result result = _fileSystem.Get.GetFileSystemAttribute(out FileSystemAttribute attribute);
context.ResponseData.Write(SpanHelpers.AsReadOnlyByteSpan(in attribute));
return (ResultCode)result.Value;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
if (isDisposing) if (isDisposing)

View file

@ -1380,7 +1380,10 @@ namespace Ryujinx.HLE.HOS.Services.Fs
[CommandCmif(1016)] [CommandCmif(1016)]
public ResultCode FlushAccessLogOnSdCard(ServiceCtx context) public ResultCode FlushAccessLogOnSdCard(ServiceCtx context)
{ {
return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value; // Logging the access log to the SD card isn't implemented, meaning this function will be a no-op since
// there's nothing to flush. Return success until it's implemented.
// return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value;
return ResultCode.Success;
} }
[CommandCmif(1017)] [CommandCmif(1017)]

View file

@ -0,0 +1,12 @@
namespace Ryujinx.HLE.HOS.Services.Ldn
{
static class LdnConst
{
public const int SsidLengthMax = 0x20;
public const int AdvertiseDataSizeMax = 0x180;
public const int UserNameBytesMax = 0x20;
public const int NodeCountMax = 8;
public const int StationCountMax = NodeCountMax - 1;
public const int PassphraseLengthMax = 0x40;
}
}

View file

@ -1,4 +1,4 @@
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types namespace Ryujinx.HLE.HOS.Services.Ldn.Types

View file

@ -1,4 +1,4 @@
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types namespace Ryujinx.HLE.HOS.Services.Ldn.Types

View file

@ -1,4 +1,4 @@
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types namespace Ryujinx.HLE.HOS.Services.Ldn.Types
@ -48,7 +48,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{ {
result[i].Reserved = new Array7<byte>(); result[i].Reserved = new Array7<byte>();
if (i < 8) if (i < LdnConst.NodeCountMax)
{ {
result[i].State = array[i].State; result[i].State = array[i].State;
array[i].State = NodeLatestUpdateFlags.None; array[i].State = NodeLatestUpdateFlags.None;

View file

@ -1,4 +1,4 @@
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types namespace Ryujinx.HLE.HOS.Services.Ldn.Types

View file

@ -1,4 +1,4 @@
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types namespace Ryujinx.HLE.HOS.Services.Ldn.Types

View file

@ -1,4 +1,4 @@
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types namespace Ryujinx.HLE.HOS.Services.Ldn.Types

View file

@ -1,7 +1,6 @@
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
using System; using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
@ -30,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_parent.NetworkClient.NetworkChange -= NetworkChanged; _parent.NetworkClient.NetworkChange -= NetworkChanged;
} }
private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e) private void NetworkChanged(object sender, NetworkChangeEventArgs e)
{ {
LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes); LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);

View file

@ -1,12 +1,13 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
using System; using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{ {
interface INetworkClient : IDisposable interface INetworkClient : IDisposable
{ {
bool NeedsRealId { get; }
event EventHandler<NetworkChangeEventArgs> NetworkChange; event EventHandler<NetworkChangeEventArgs> NetworkChange;
void DisconnectNetwork(); void DisconnectNetwork();

View file

@ -8,7 +8,7 @@ using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
@ -395,7 +395,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
} }
else else
{ {
if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1) if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{ {
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented. // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@ -546,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment? context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment?
NetworkConfig networkConfig = context.RequestData.ReadStruct<NetworkConfig>(); NetworkConfig networkConfig = context.RequestData.ReadStruct<NetworkConfig>();
if (networkConfig.IntentId.LocalCommunicationId == -1) if (networkConfig.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{ {
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented. // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@ -555,7 +555,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
} }
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId); bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId);
if (!isLocalCommunicationIdValid) if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
{ {
return ResultCode.InvalidObject; return ResultCode.InvalidObject;
} }
@ -568,13 +568,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel); networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel);
securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode); securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode);
if (networkConfig.NodeCountMax <= 8) if (networkConfig.NodeCountMax <= LdnConst.NodeCountMax)
{ {
if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0) if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0)
{ {
if (securityConfig.SecurityMode <= SecurityMode.Retail) if (securityConfig.SecurityMode <= SecurityMode.Retail)
{ {
if (securityConfig.Passphrase.Length <= 0x40) if (securityConfig.Passphrase.Length <= LdnConst.PassphraseLengthMax)
{ {
if (_state == NetworkState.AccessPoint) if (_state == NetworkState.AccessPoint)
{ {
@ -678,7 +678,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
return _nifmResultCode; return _nifmResultCode;
} }
if (bufferSize == 0 || bufferSize > 0x180) if (bufferSize == 0 || bufferSize > LdnConst.AdvertiseDataSizeMax)
{ {
return ResultCode.InvalidArgument; return ResultCode.InvalidArgument;
} }
@ -848,10 +848,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
context.Memory.Read(bufferPosition, networkInfoBytes); context.Memory.Read(bufferPosition, networkInfoBytes);
networkInfo = MemoryMarshal.Cast<byte, NetworkInfo>(networkInfoBytes)[0]; networkInfo = MemoryMarshal.Read<NetworkInfo>(networkInfoBytes);
} }
if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1) if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{ {
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented. // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties; ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@ -860,7 +860,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
} }
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId); bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId);
if (!isLocalCommunicationIdValid) if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
{ {
return ResultCode.InvalidObject; return ResultCode.InvalidObject;
} }
@ -1061,10 +1061,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable())
{ {
MultiplayerMode mode = context.Device.Configuration.MultiplayerMode; MultiplayerMode mode = context.Device.Configuration.MultiplayerMode;
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initializing with multiplayer mode: {mode}");
switch (mode) switch (mode)
{ {
case MultiplayerMode.LdnMitm:
NetworkClient = new LdnMitmClient(context.Device.Configuration);
break;
case MultiplayerMode.Disabled: case MultiplayerMode.Disabled:
NetworkClient = new DisabledLdnClient(); NetworkClient = new LdnDisabledClient();
break; break;
} }

View file

@ -1,12 +1,13 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
using System; using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{ {
class DisabledLdnClient : INetworkClient class LdnDisabledClient : INetworkClient
{ {
public bool NeedsRealId => true;
public event EventHandler<NetworkChangeEventArgs> NetworkChange; public event EventHandler<NetworkChangeEventArgs> NetworkChange;
public NetworkError Connect(ConnectRequest request) public NetworkError Connect(ConnectRequest request)

View file

@ -0,0 +1,611 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
{
internal class LanDiscovery : IDisposable
{
private const int DefaultPort = 11452;
private const ushort CommonChannel = 6;
private const byte CommonLinkLevel = 3;
private const byte CommonNetworkType = 2;
private const int FailureTimeout = 4000;
private readonly LdnMitmClient _parent;
private readonly LanProtocol _protocol;
private bool _initialized;
private readonly Ssid _fakeSsid;
private ILdnTcpSocket _tcp;
private LdnProxyUdpServer _udp, _udp2;
private readonly List<LdnProxyTcpSession> _stations = new();
private readonly object _lock = new();
private readonly AutoResetEvent _apConnected = new(false);
internal readonly IPAddress LocalAddr;
internal readonly IPAddress LocalBroadcastAddr;
internal NetworkInfo NetworkInfo;
public bool IsHost => _tcp is LdnProxyTcpServer;
private readonly Random _random = new();
// NOTE: Credit to https://stackoverflow.com/a/39338188
private static IPAddress GetBroadcastAddress(IPAddress address, IPAddress mask)
{
uint ipAddress = BitConverter.ToUInt32(address.GetAddressBytes(), 0);
uint ipMaskV4 = BitConverter.ToUInt32(mask.GetAddressBytes(), 0);
uint broadCastIpAddress = ipAddress | ~ipMaskV4;
return new IPAddress(BitConverter.GetBytes(broadCastIpAddress));
}
private static NetworkInfo GetEmptyNetworkInfo()
{
NetworkInfo networkInfo = new()
{
NetworkId = new NetworkId
{
SessionId = new Array16<byte>(),
},
Common = new CommonNetworkInfo
{
MacAddress = new Array6<byte>(),
Ssid = new Ssid
{
Name = new Array33<byte>(),
},
},
Ldn = new LdnNetworkInfo
{
NodeCountMax = LdnConst.NodeCountMax,
SecurityParameter = new Array16<byte>(),
Nodes = new Array8<NodeInfo>(),
AdvertiseData = new Array384<byte>(),
Reserved4 = new Array140<byte>(),
},
};
for (int i = 0; i < LdnConst.NodeCountMax; i++)
{
networkInfo.Ldn.Nodes[i] = new NodeInfo
{
MacAddress = new Array6<byte>(),
UserName = new Array33<byte>(),
Reserved2 = new Array16<byte>(),
};
}
return networkInfo;
}
public LanDiscovery(LdnMitmClient parent, IPAddress ipAddress, IPAddress ipv4Mask)
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initialize LanDiscovery using IP: {ipAddress}");
_parent = parent;
LocalAddr = ipAddress;
LocalBroadcastAddr = GetBroadcastAddress(ipAddress, ipv4Mask);
_fakeSsid = new Ssid
{
Length = LdnConst.SsidLengthMax,
};
_random.NextBytes(_fakeSsid.Name.AsSpan()[..32]);
_protocol = new LanProtocol(this);
_protocol.Accept += OnConnect;
_protocol.SyncNetwork += OnSyncNetwork;
_protocol.DisconnectStation += DisconnectStation;
NetworkInfo = GetEmptyNetworkInfo();
ResetStations();
if (!InitUdp())
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Initialize: InitUdp failed.");
return;
}
_initialized = true;
}
protected void OnSyncNetwork(NetworkInfo info)
{
bool updated = false;
lock (_lock)
{
if (!NetworkInfo.Equals(info))
{
NetworkInfo = info;
updated = true;
Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"Host IP: {NetworkHelpers.ConvertUint(info.Ldn.Nodes[0].Ipv4Address)}");
}
}
if (updated)
{
_parent.InvokeNetworkChange(info, true);
}
_apConnected.Set();
}
protected void OnConnect(LdnProxyTcpSession station)
{
lock (_lock)
{
station.NodeId = LocateEmptyNode();
if (_stations.Count > LdnConst.StationCountMax || station.NodeId == -1)
{
station.Disconnect();
station.Dispose();
return;
}
_stations.Add(station);
UpdateNodes();
}
}
public void DisconnectStation(LdnProxyTcpSession station)
{
if (!station.IsDisposed)
{
if (station.IsConnected)
{
station.Disconnect();
}
station.Dispose();
}
lock (_lock)
{
if (_stations.Remove(station))
{
NetworkInfo.Ldn.Nodes[station.NodeId] = new NodeInfo()
{
MacAddress = new Array6<byte>(),
UserName = new Array33<byte>(),
Reserved2 = new Array16<byte>(),
};
UpdateNodes();
}
}
}
public bool SetAdvertiseData(byte[] data)
{
if (data.Length > LdnConst.AdvertiseDataSizeMax)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "AdvertiseData exceeds size limit.");
return false;
}
data.CopyTo(NetworkInfo.Ldn.AdvertiseData.AsSpan());
NetworkInfo.Ldn.AdvertiseDataSize = (ushort)data.Length;
// NOTE: Otherwise this results in SessionKeepFailed or MasterDisconnected
lock (_lock)
{
if (NetworkInfo.Ldn.Nodes[0].IsConnected == 1)
{
UpdateNodes(true);
}
}
return true;
}
public void InitNetworkInfo()
{
lock (_lock)
{
NetworkInfo.Common.MacAddress = GetFakeMac();
NetworkInfo.Common.Channel = CommonChannel;
NetworkInfo.Common.LinkLevel = CommonLinkLevel;
NetworkInfo.Common.NetworkType = CommonNetworkType;
NetworkInfo.Common.Ssid = _fakeSsid;
NetworkInfo.Ldn.Nodes = new Array8<NodeInfo>();
for (int i = 0; i < LdnConst.NodeCountMax; i++)
{
NetworkInfo.Ldn.Nodes[i].NodeId = (byte)i;
NetworkInfo.Ldn.Nodes[i].IsConnected = 0;
}
}
}
protected Array6<byte> GetFakeMac(IPAddress address = null)
{
address ??= LocalAddr;
byte[] ip = address.GetAddressBytes();
var macAddress = new Array6<byte>();
new byte[] { 0x02, 0x00, ip[0], ip[1], ip[2], ip[3] }.CopyTo(macAddress.AsSpan());
return macAddress;
}
public bool InitTcp(bool listening, IPAddress address = null, int port = DefaultPort)
{
Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"LanDiscovery InitTcp: IP: {address}, listening: {listening}");
if (_tcp != null)
{
_tcp.DisconnectAndStop();
_tcp.Dispose();
_tcp = null;
}
ILdnTcpSocket tcpSocket;
if (listening)
{
try
{
address ??= LocalAddr;
tcpSocket = new LdnProxyTcpServer(_protocol, address, port);
}
catch (Exception ex)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpServer: {ex}");
return false;
}
if (!tcpSocket.Start())
{
return false;
}
}
else
{
if (address == null)
{
return false;
}
try
{
tcpSocket = new LdnProxyTcpClient(_protocol, address, port);
}
catch (Exception ex)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpClient: {ex}");
return false;
}
}
_tcp = tcpSocket;
return true;
}
public bool InitUdp()
{
_udp?.Stop();
_udp2?.Stop();
try
{
// NOTE: Linux won't receive any broadcast packets if the socket is not bound to the broadcast address.
// Windows only works if bound to localhost or the local address.
// See this discussion: https://stackoverflow.com/questions/13666789/receiving-udp-broadcast-packets-on-linux
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
_udp2 = new LdnProxyUdpServer(_protocol, LocalBroadcastAddr, DefaultPort);
}
_udp = new LdnProxyUdpServer(_protocol, LocalAddr, DefaultPort);
}
catch (Exception ex)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyUdpServer: {ex}");
return false;
}
return true;
}
public NetworkInfo[] Scan(ushort channel, ScanFilter filter)
{
_udp.ClearScanResults();
if (_protocol.SendBroadcast(_udp, LanPacketType.Scan, DefaultPort) < 0)
{
return Array.Empty<NetworkInfo>();
}
List<NetworkInfo> outNetworkInfo = new();
foreach (KeyValuePair<ulong, NetworkInfo> item in _udp.GetScanResults())
{
bool copy = true;
if (filter.Flag.HasFlag(ScanFilterFlag.LocalCommunicationId))
{
copy &= filter.NetworkId.IntentId.LocalCommunicationId == item.Value.NetworkId.IntentId.LocalCommunicationId;
}
if (filter.Flag.HasFlag(ScanFilterFlag.SessionId))
{
copy &= filter.NetworkId.SessionId.AsSpan().SequenceEqual(item.Value.NetworkId.SessionId.AsSpan());
}
if (filter.Flag.HasFlag(ScanFilterFlag.NetworkType))
{
copy &= filter.NetworkType == (NetworkType)item.Value.Common.NetworkType;
}
if (filter.Flag.HasFlag(ScanFilterFlag.Ssid))
{
Span<byte> gameSsid = item.Value.Common.Ssid.Name.AsSpan()[item.Value.Common.Ssid.Length..];
Span<byte> scanSsid = filter.Ssid.Name.AsSpan()[filter.Ssid.Length..];
copy &= gameSsid.SequenceEqual(scanSsid);
}
if (filter.Flag.HasFlag(ScanFilterFlag.SceneId))
{
copy &= filter.NetworkId.IntentId.SceneId == item.Value.NetworkId.IntentId.SceneId;
}
if (copy)
{
if (item.Value.Ldn.Nodes[0].UserName[0] != 0)
{
outNetworkInfo.Add(item.Value);
}
else
{
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Scan: Got empty Username. There might be a timing issue somewhere...");
}
}
}
return outNetworkInfo.ToArray();
}
protected void ResetStations()
{
lock (_lock)
{
foreach (LdnProxyTcpSession station in _stations)
{
station.Disconnect();
station.Dispose();
}
_stations.Clear();
}
}
private int LocateEmptyNode()
{
Array8<NodeInfo> nodes = NetworkInfo.Ldn.Nodes;
for (int i = 1; i < nodes.Length; i++)
{
if (nodes[i].IsConnected == 0)
{
return i;
}
}
return -1;
}
protected void UpdateNodes(bool forceUpdate = false)
{
int countConnected = 1;
foreach (LdnProxyTcpSession station in _stations.Where(station => station.IsConnected))
{
countConnected++;
station.OverrideInfo();
// NOTE: This is not part of the original implementation.
NetworkInfo.Ldn.Nodes[station.NodeId] = station.NodeInfo;
}
byte nodeCount = (byte)countConnected;
bool networkInfoChanged = forceUpdate || NetworkInfo.Ldn.NodeCount != nodeCount;
NetworkInfo.Ldn.NodeCount = nodeCount;
foreach (LdnProxyTcpSession station in _stations)
{
if (station.IsConnected)
{
if (_protocol.SendPacket(station, LanPacketType.SyncNetwork, SpanHelpers.AsSpan<NetworkInfo, byte>(ref NetworkInfo).ToArray()) < 0)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to send {LanPacketType.SyncNetwork} to station {station.NodeId}");
}
}
}
if (networkInfoChanged)
{
_parent.InvokeNetworkChange(NetworkInfo, true);
}
}
protected NodeInfo GetNodeInfo(NodeInfo node, UserConfig userConfig, ushort localCommunicationVersion)
{
uint ipAddress = NetworkHelpers.ConvertIpv4Address(LocalAddr);
node.MacAddress = GetFakeMac();
node.IsConnected = 1;
node.UserName = userConfig.UserName;
node.LocalCommunicationVersion = localCommunicationVersion;
node.Ipv4Address = ipAddress;
return node;
}
public bool CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig)
{
if (!InitTcp(true))
{
return false;
}
InitNetworkInfo();
NetworkInfo.Ldn.NodeCountMax = networkConfig.NodeCountMax;
NetworkInfo.Ldn.SecurityMode = (ushort)securityConfig.SecurityMode;
NetworkInfo.Common.Channel = networkConfig.Channel == 0 ? (ushort)6 : networkConfig.Channel;
NetworkInfo.NetworkId.SessionId = new Array16<byte>();
_random.NextBytes(NetworkInfo.NetworkId.SessionId.AsSpan());
NetworkInfo.NetworkId.IntentId = networkConfig.IntentId;
NetworkInfo.Ldn.Nodes[0] = GetNodeInfo(NetworkInfo.Ldn.Nodes[0], userConfig, networkConfig.LocalCommunicationVersion);
NetworkInfo.Ldn.Nodes[0].IsConnected = 1;
NetworkInfo.Ldn.NodeCount++;
_parent.InvokeNetworkChange(NetworkInfo, true);
return true;
}
public void DestroyNetwork()
{
if (_tcp != null)
{
try
{
_tcp.DisconnectAndStop();
}
finally
{
_tcp.Dispose();
_tcp = null;
}
}
ResetStations();
}
public NetworkError Connect(NetworkInfo networkInfo, UserConfig userConfig, uint localCommunicationVersion)
{
_apConnected.Reset();
if (networkInfo.Ldn.NodeCount == 0)
{
return NetworkError.Unknown;
}
IPAddress address = NetworkHelpers.ConvertUint(networkInfo.Ldn.Nodes[0].Ipv4Address);
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Connecting to host: {address}");
if (!InitTcp(false, address))
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Could not initialize TCPClient");
return NetworkError.ConnectNotFound;
}
if (!_tcp.Connect())
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Failed to connect.");
return NetworkError.ConnectFailure;
}
NodeInfo myNode = GetNodeInfo(new NodeInfo(), userConfig, (ushort)localCommunicationVersion);
if (_protocol.SendPacket(_tcp, LanPacketType.Connect, SpanHelpers.AsSpan<NodeInfo, byte>(ref myNode).ToArray()) < 0)
{
return NetworkError.Unknown;
}
return _apConnected.WaitOne(FailureTimeout) ? NetworkError.None : NetworkError.ConnectTimeout;
}
public void Dispose()
{
if (_initialized)
{
DisconnectAndStop();
ResetStations();
_initialized = false;
}
_protocol.Accept -= OnConnect;
_protocol.SyncNetwork -= OnSyncNetwork;
_protocol.DisconnectStation -= DisconnectStation;
}
public void DisconnectAndStop()
{
if (_udp != null)
{
try
{
_udp.Stop();
}
finally
{
_udp.Dispose();
_udp = null;
}
}
if (_udp2 != null)
{
try
{
_udp2.Stop();
}
finally
{
_udp2.Dispose();
_udp2 = null;
}
}
if (_tcp != null)
{
try
{
_tcp.DisconnectAndStop();
}
finally
{
_tcp.Dispose();
_tcp = null;
}
}
}
}
}

View file

@ -0,0 +1,314 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
{
internal class LanProtocol
{
private const uint LanMagic = 0x11451400;
public const int BufferSize = 2048;
public const int TcpTxBufferSize = 0x800;
public const int TcpRxBufferSize = 0x1000;
public const int TxBufferSizeMax = 0x2000;
public const int RxBufferSizeMax = 0x2000;
private readonly int _headerSize = Marshal.SizeOf<LanPacketHeader>();
private readonly LanDiscovery _discovery;
public event Action<LdnProxyTcpSession> Accept;
public event Action<EndPoint, LanPacketType, byte[]> Scan;
public event Action<NetworkInfo> ScanResponse;
public event Action<NetworkInfo> SyncNetwork;
public event Action<NodeInfo, EndPoint> Connect;
public event Action<LdnProxyTcpSession> DisconnectStation;
public LanProtocol(LanDiscovery parent)
{
_discovery = parent;
}
public void InvokeAccept(LdnProxyTcpSession session)
{
Accept?.Invoke(session);
}
public void InvokeDisconnectStation(LdnProxyTcpSession session)
{
DisconnectStation?.Invoke(session);
}
private void DecodeAndHandle(LanPacketHeader header, byte[] data, EndPoint endPoint = null)
{
switch (header.Type)
{
case LanPacketType.Scan:
// UDP
if (_discovery.IsHost)
{
Scan?.Invoke(endPoint, LanPacketType.ScanResponse, SpanHelpers.AsSpan<NetworkInfo, byte>(ref _discovery.NetworkInfo).ToArray());
}
break;
case LanPacketType.ScanResponse:
// UDP
ScanResponse?.Invoke(MemoryMarshal.Cast<byte, NetworkInfo>(data)[0]);
break;
case LanPacketType.SyncNetwork:
// TCP
SyncNetwork?.Invoke(MemoryMarshal.Cast<byte, NetworkInfo>(data)[0]);
break;
case LanPacketType.Connect:
// TCP Session / Station
Connect?.Invoke(MemoryMarshal.Cast<byte, NodeInfo>(data)[0], endPoint);
break;
default:
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decode error: Unhandled type {header.Type}");
break;
}
}
public void Read(scoped ref byte[] buffer, scoped ref int bufferEnd, byte[] data, int offset, int size, EndPoint endPoint = null)
{
if (endPoint != null && _discovery.LocalAddr.Equals(((IPEndPoint)endPoint).Address))
{
return;
}
int index = 0;
while (index < size)
{
if (bufferEnd < _headerSize)
{
int copyable2 = Math.Min(size - index, Math.Min(size, _headerSize - bufferEnd));
Array.Copy(data, index + offset, buffer, bufferEnd, copyable2);
index += copyable2;
bufferEnd += copyable2;
}
if (bufferEnd >= _headerSize)
{
LanPacketHeader header = MemoryMarshal.Cast<byte, LanPacketHeader>(buffer)[0];
if (header.Magic != LanMagic)
{
bufferEnd = 0;
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, $"Invalid magic number in received packet. [magic: {header.Magic}] [EP: {endPoint}]");
return;
}
int totalSize = _headerSize + header.Length;
if (totalSize > BufferSize)
{
bufferEnd = 0;
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Max packet size {BufferSize} exceeded.");
return;
}
int copyable = Math.Min(size - index, Math.Min(size, totalSize - bufferEnd));
Array.Copy(data, index + offset, buffer, bufferEnd, copyable);
index += copyable;
bufferEnd += copyable;
if (totalSize == bufferEnd)
{
byte[] ldnData = new byte[totalSize - _headerSize];
Array.Copy(buffer, _headerSize, ldnData, 0, ldnData.Length);
if (header.Compressed == 1)
{
if (Decompress(ldnData, out byte[] decompressedLdnData) != 0)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error:\n {header}, {_headerSize}\n {ldnData}, {ldnData.Length}");
return;
}
if (decompressedLdnData.Length != header.DecompressLength)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error: length does not match. ({decompressedLdnData.Length} != {header.DecompressLength})");
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error data: '{string.Join("", decompressedLdnData.Select(x => (int)x).ToArray())}'");
return;
}
ldnData = decompressedLdnData;
}
DecodeAndHandle(header, ldnData, endPoint);
bufferEnd = 0;
}
}
}
}
public int SendBroadcast(ILdnSocket s, LanPacketType type, int port)
{
return SendPacket(s, type, Array.Empty<byte>(), new IPEndPoint(_discovery.LocalBroadcastAddr, port));
}
public int SendPacket(ILdnSocket s, LanPacketType type, byte[] data, EndPoint endPoint = null)
{
byte[] buf = PreparePacket(type, data);
return s.SendPacketAsync(endPoint, buf) ? 0 : -1;
}
public int SendPacket(LdnProxyTcpSession s, LanPacketType type, byte[] data)
{
byte[] buf = PreparePacket(type, data);
return s.SendAsync(buf) ? 0 : -1;
}
private LanPacketHeader PrepareHeader(LanPacketHeader header, LanPacketType type)
{
header.Magic = LanMagic;
header.Type = type;
header.Compressed = 0;
header.Length = 0;
header.DecompressLength = 0;
header.Reserved = new Array2<byte>();
return header;
}
private byte[] PreparePacket(LanPacketType type, byte[] data)
{
LanPacketHeader header = PrepareHeader(new LanPacketHeader(), type);
header.Length = (ushort)data.Length;
byte[] buf;
if (data.Length > 0)
{
if (Compress(data, out byte[] compressed) == 0)
{
header.DecompressLength = header.Length;
header.Length = (ushort)compressed.Length;
header.Compressed = 1;
buf = new byte[compressed.Length + _headerSize];
SpanHelpers.AsSpan<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
compressed.CopyTo(buf, _headerSize);
}
else
{
buf = new byte[data.Length + _headerSize];
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Compressing packet data failed.");
SpanHelpers.AsSpan<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
data.CopyTo(buf, _headerSize);
}
}
else
{
buf = new byte[_headerSize];
SpanHelpers.AsSpan<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
}
return buf;
}
private int Compress(byte[] input, out byte[] output)
{
List<byte> outputList = new();
int i = 0;
int maxCount = 0xFF;
while (i < input.Length)
{
byte inputByte = input[i++];
int count = 0;
if (inputByte == 0)
{
while (i < input.Length && input[i] == 0 && count < maxCount)
{
count += 1;
i++;
}
}
if (inputByte == 0)
{
outputList.Add(0);
if (outputList.Count == BufferSize)
{
output = null;
return -1;
}
outputList.Add((byte)count);
}
else
{
outputList.Add(inputByte);
}
}
output = outputList.ToArray();
return i == input.Length ? 0 : -1;
}
private int Decompress(byte[] input, out byte[] output)
{
List<byte> outputList = new();
int i = 0;
while (i < input.Length && outputList.Count < BufferSize)
{
byte inputByte = input[i++];
outputList.Add(inputByte);
if (inputByte == 0)
{
if (i == input.Length)
{
output = null;
return -1;
}
int count = input[i++];
for (int j = 0; j < count; j++)
{
if (outputList.Count == BufferSize)
{
break;
}
outputList.Add(inputByte);
}
}
}
output = outputList.ToArray();
return i == input.Length ? 0 : -1;
}
}
}

View file

@ -0,0 +1,104 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
using System.Net.NetworkInformation;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
{
/// <summary>
/// Client implementation for <a href="https://github.com/spacemeowx2/ldn_mitm">ldn_mitm</a>
/// </summary>
internal class LdnMitmClient : INetworkClient
{
public bool NeedsRealId => false;
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
private readonly LanDiscovery _lanDiscovery;
public LdnMitmClient(HLEConfiguration config)
{
UnicastIPAddressInformation localIpInterface = NetworkHelpers.GetLocalInterface(config.MultiplayerLanInterfaceId).Item2;
_lanDiscovery = new LanDiscovery(this, localIpInterface.Address, localIpInterface.IPv4Mask);
}
internal void InvokeNetworkChange(NetworkInfo info, bool connected, DisconnectReason reason = DisconnectReason.None)
{
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, connected: connected, disconnectReason: reason));
}
public NetworkError Connect(ConnectRequest request)
{
return _lanDiscovery.Connect(request.NetworkInfo, request.UserConfig, request.LocalCommunicationVersion);
}
public NetworkError ConnectPrivate(ConnectPrivateRequest request)
{
// NOTE: This method is not implemented in ldn_mitm
Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient ConnectPrivate");
return NetworkError.None;
}
public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
{
return _lanDiscovery.CreateNetwork(request.SecurityConfig, request.UserConfig, request.NetworkConfig);
}
public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
{
// NOTE: This method is not implemented in ldn_mitm
Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient CreateNetworkPrivate");
return true;
}
public void DisconnectAndStop()
{
_lanDiscovery.DisconnectAndStop();
}
public void DisconnectNetwork()
{
_lanDiscovery.DestroyNetwork();
}
public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId)
{
// NOTE: This method is not implemented in ldn_mitm
Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient Reject");
return ResultCode.Success;
}
public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
{
return _lanDiscovery.Scan(channel, scanFilter);
}
public void SetAdvertiseData(byte[] data)
{
_lanDiscovery.SetAdvertiseData(data);
}
public void SetGameVersion(byte[] versionString)
{
// NOTE: This method is not implemented in ldn_mitm
Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetGameVersion");
}
public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy)
{
// NOTE: This method is not implemented in ldn_mitm
Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetStationAcceptPolicy");
}
public void Dispose()
{
_lanDiscovery.Dispose();
}
}
}

View file

@ -0,0 +1,12 @@
using System;
using System.Net;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
{
internal interface ILdnSocket : IDisposable
{
bool SendPacketAsync(EndPoint endpoint, byte[] buffer);
bool Start();
bool Stop();
}
}

View file

@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
{
internal interface ILdnTcpSocket : ILdnSocket
{
bool Connect();
void DisconnectAndStop();
}
}

View file

@ -0,0 +1,99 @@
using Ryujinx.Common.Logging;
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
{
internal class LdnProxyTcpClient : NetCoreServer.TcpClient, ILdnTcpSocket
{
private readonly LanProtocol _protocol;
private byte[] _buffer;
private int _bufferEnd;
public LdnProxyTcpClient(LanProtocol protocol, IPAddress address, int port) : base(address, port)
{
_protocol = protocol;
_buffer = new byte[LanProtocol.BufferSize];
OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
OptionSendBufferLimit = LanProtocol.TxBufferSizeMax;
OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax;
}
protected override void OnConnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient connected!");
}
protected override void OnReceived(byte[] buffer, long offset, long size)
{
_protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size);
}
public void DisconnectAndStop()
{
DisconnectAsync();
while (IsConnected)
{
Thread.Yield();
}
}
public bool SendPacketAsync(EndPoint endPoint, byte[] data)
{
if (endPoint != null)
{
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTcpClient is sending a packet but endpoint is not null.");
}
if (IsConnecting && !IsConnected)
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPClient needs to connect before sending packets. Waiting...");
while (IsConnecting && !IsConnected)
{
Thread.Yield();
}
}
return SendAsync(data);
}
protected override void OnError(SocketError error)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient caught an error with code {error}");
}
protected override void Dispose(bool disposingManagedResources)
{
DisconnectAndStop();
base.Dispose(disposingManagedResources);
}
public override bool Connect()
{
// TODO: NetCoreServer has a Connect() method, but it currently leads to weird issues.
base.ConnectAsync();
while (IsConnecting)
{
Thread.Sleep(1);
}
return IsConnected;
}
public bool Start()
{
throw new InvalidOperationException("Start was called.");
}
public bool Stop()
{
throw new InvalidOperationException("Stop was called.");
}
}
}

View file

@ -0,0 +1,54 @@
using NetCoreServer;
using Ryujinx.Common.Logging;
using System;
using System.Net;
using System.Net.Sockets;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
{
internal class LdnProxyTcpServer : TcpServer, ILdnTcpSocket
{
private readonly LanProtocol _protocol;
public LdnProxyTcpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port)
{
_protocol = protocol;
OptionReuseAddress = true;
OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer created a server for this address: {address}:{port}");
}
protected override TcpSession CreateSession()
{
return new LdnProxyTcpSession(this, _protocol);
}
protected override void OnError(SocketError error)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer caught an error with code {error}");
}
protected override void Dispose(bool disposingManagedResources)
{
Stop();
base.Dispose(disposingManagedResources);
}
public bool Connect()
{
throw new InvalidOperationException("Connect was called.");
}
public void DisconnectAndStop()
{
Stop();
}
public bool SendPacketAsync(EndPoint endpoint, byte[] buffer)
{
throw new InvalidOperationException("SendPacketAsync was called.");
}
}
}

View file

@ -0,0 +1,83 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Net;
using System.Net.Sockets;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
{
internal class LdnProxyTcpSession : NetCoreServer.TcpSession
{
private readonly LanProtocol _protocol;
internal int NodeId;
internal NodeInfo NodeInfo;
private byte[] _buffer;
private int _bufferEnd;
public LdnProxyTcpSession(LdnProxyTcpServer server, LanProtocol protocol) : base(server)
{
_protocol = protocol;
_protocol.Connect += OnConnect;
_buffer = new byte[LanProtocol.BufferSize];
OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
OptionSendBufferLimit = LanProtocol.TxBufferSizeMax;
OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax;
}
public void OverrideInfo()
{
NodeInfo.NodeId = (byte)NodeId;
NodeInfo.IsConnected = (byte)(IsConnected ? 1 : 0);
}
protected override void OnConnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession connected!");
}
protected override void OnDisconnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession disconnected!");
_protocol.InvokeDisconnectStation(this);
}
protected override void OnReceived(byte[] buffer, long offset, long size)
{
_protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, this.Socket.RemoteEndPoint);
}
protected override void OnError(SocketError error)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession caught an error with code {error}");
Dispose();
}
protected override void Dispose(bool disposingManagedResources)
{
_protocol.Connect -= OnConnect;
base.Dispose(disposingManagedResources);
}
private void OnConnect(NodeInfo info, EndPoint endPoint)
{
try
{
if (endPoint.Equals(this.Socket.RemoteEndPoint))
{
NodeInfo = info;
_protocol.InvokeAccept(this);
}
}
catch (System.ObjectDisposedException)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession was disposed. [IP: {NodeInfo.Ipv4Address}]");
_protocol.InvokeDisconnectStation(this);
}
}
}
}

View file

@ -0,0 +1,157 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
{
internal class LdnProxyUdpServer : NetCoreServer.UdpServer, ILdnSocket
{
private const long ScanFrequency = 1000;
private readonly LanProtocol _protocol;
private byte[] _buffer;
private int _bufferEnd;
private readonly object _scanLock = new();
private Dictionary<ulong, NetworkInfo> _scanResultsLast = new();
private Dictionary<ulong, NetworkInfo> _scanResults = new();
private readonly AutoResetEvent _scanResponse = new(false);
private long _lastScanTime;
public LdnProxyUdpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port)
{
_protocol = protocol;
_protocol.Scan += HandleScan;
_protocol.ScanResponse += HandleScanResponse;
_buffer = new byte[LanProtocol.BufferSize];
OptionReuseAddress = true;
OptionReceiveBufferSize = LanProtocol.RxBufferSizeMax;
OptionSendBufferSize = LanProtocol.TxBufferSizeMax;
Start();
}
protected override Socket CreateSocket()
{
return new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp)
{
EnableBroadcast = true,
};
}
protected override void OnStarted()
{
ReceiveAsync();
}
protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size)
{
_protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, endpoint);
ReceiveAsync();
}
protected override void OnError(SocketError error)
{
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyUdpServer caught an error with code {error}");
}
protected override void Dispose(bool disposingManagedResources)
{
_protocol.Scan -= HandleScan;
_protocol.ScanResponse -= HandleScanResponse;
_scanResponse.Dispose();
base.Dispose(disposingManagedResources);
}
public bool SendPacketAsync(EndPoint endpoint, byte[] data)
{
return SendAsync(endpoint, data);
}
private void HandleScan(EndPoint endpoint, LanPacketType type, byte[] data)
{
_protocol.SendPacket(this, type, data, endpoint);
}
private void HandleScanResponse(NetworkInfo info)
{
Span<byte> mac = stackalloc byte[8];
info.Common.MacAddress.AsSpan().CopyTo(mac);
lock (_scanLock)
{
_scanResults[BitConverter.ToUInt64(mac)] = info;
_scanResponse.Set();
}
}
public void ClearScanResults()
{
// Rate limit scans.
long timeMs = Stopwatch.GetTimestamp() / (Stopwatch.Frequency / 1000);
long delay = ScanFrequency - (timeMs - _lastScanTime);
if (delay > 0)
{
Thread.Sleep((int)delay);
}
_lastScanTime = timeMs;
lock (_scanLock)
{
var newResults = _scanResultsLast;
newResults.Clear();
_scanResultsLast = _scanResults;
_scanResults = newResults;
_scanResponse.Reset();
}
}
public Dictionary<ulong, NetworkInfo> GetScanResults()
{
// NOTE: Try to minimize waiting time for scan results.
// After we receive the first response, wait a short time for follow-ups and return.
// Responses that were too late to catch will appear in the next scan.
// ldn_mitm does not do this, but this improves latency for games that expect it to be low (it is on console).
if (_scanResponse.WaitOne(1000))
{
// Wait a short while longer in case there are some other responses.
Thread.Sleep(33);
}
lock (_scanLock)
{
var results = new Dictionary<ulong, NetworkInfo>();
foreach (KeyValuePair<ulong, NetworkInfo> last in _scanResultsLast)
{
results[last.Key] = last.Value;
}
foreach (KeyValuePair<ulong, NetworkInfo> scan in _scanResults)
{
results[scan.Key] = scan.Value;
}
return results;
}
}
}
}

View file

@ -0,0 +1,16 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
{
[StructLayout(LayoutKind.Sequential, Size = 12)]
internal struct LanPacketHeader
{
public uint Magic;
public LanPacketType Type;
public byte Compressed;
public ushort Length;
public ushort DecompressLength;
public Array2<byte> Reserved;
}
}

View file

@ -0,0 +1,10 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
{
internal enum LanPacketType : byte
{
Scan,
ScanResponse,
Connect,
SyncNetwork,
}
}

View file

@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System; using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{ {
class NetworkChangeEventArgs : EventArgs class NetworkChangeEventArgs : EventArgs
{ {

View file

@ -1,7 +1,6 @@
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
using System; using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
@ -22,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_parent.NetworkClient.NetworkChange += NetworkChanged; _parent.NetworkClient.NetworkChange += NetworkChanged;
} }
private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e) private void NetworkChanged(object sender, NetworkChangeEventArgs e)
{ {
LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes); LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);

View file

@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{ {
[StructLayout(LayoutKind.Sequential, Size = 0xBC)] [StructLayout(LayoutKind.Sequential, Size = 0xBC)]
struct ConnectPrivateRequest struct ConnectPrivateRequest

View file

@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{ {
[StructLayout(LayoutKind.Sequential, Size = 0x4FC)] [StructLayout(LayoutKind.Sequential, Size = 0x4FC)]
struct ConnectRequest struct ConnectRequest

View file

@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{ {
/// <remarks> /// <remarks>
/// Advertise data is appended separately (remaining data in the buffer). /// Advertise data is appended separately (remaining data in the buffer).

View file

@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{ {
/// <remarks> /// <remarks>
/// Advertise data is appended separately (remaining data in the buffer). /// Advertise data is appended separately (remaining data in the buffer).

View file

@ -1,4 +1,4 @@
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{ {
enum NetworkError : int enum NetworkError : int
{ {

View file

@ -1,6 +1,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{ {
[StructLayout(LayoutKind.Sequential, Size = 0x4)] [StructLayout(LayoutKind.Sequential, Size = 0x4)]
struct NetworkErrorMessage struct NetworkErrorMessage

View file

@ -20,7 +20,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
internal static (bool, ProcessResult) TryLoad(this PartitionFileSystem partitionFileSystem, Switch device, string path, out string errorMessage) internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, string path, out string errorMessage)
where TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
where TFormat : IPartitionFileSystemFormat
where THeader : unmanaged, IPartitionFileSystemHeader
where TEntry : unmanaged, IPartitionFileSystemEntry
{ {
errorMessage = null; errorMessage = null;
@ -91,7 +95,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
if (File.Exists(updatePath)) if (File.Exists(updatePath))
{ {
PartitionFileSystem updatePartitionFileSystem = new(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()); PartitionFileSystem updatePartitionFileSystem = new();
updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure();
device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem); device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);

View file

@ -69,7 +69,8 @@ namespace Ryujinx.HLE.Loaders.Processes
public bool LoadNsp(string path) public bool LoadNsp(string path)
{ {
FileStream file = new(path, FileMode.Open, FileAccess.Read); FileStream file = new(path, FileMode.Open, FileAccess.Read);
PartitionFileSystem partitionFileSystem = new(file.AsStorage()); PartitionFileSystem partitionFileSystem = new();
partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage); (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage);

View file

@ -1,8 +1,8 @@
using LibHac.Account; using LibHac.Account;
using LibHac.Common; using LibHac.Common;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Fs.Shim; using LibHac.Fs.Shim;
using LibHac.FsSystem;
using LibHac.Loader; using LibHac.Loader;
using LibHac.Ncm; using LibHac.Ncm;
using LibHac.Ns; using LibHac.Ns;
@ -33,7 +33,7 @@ namespace Ryujinx.HLE.Loaders.Processes
// TODO: Remove this workaround when ASLR is implemented. // TODO: Remove this workaround when ASLR is implemented.
private const ulong CodeStartOffset = 0x500000UL; private const ulong CodeStartOffset = 0x500000UL;
public static LibHac.Result RegisterProgramMapInfo(Switch device, PartitionFileSystem partitionFileSystem) public static LibHac.Result RegisterProgramMapInfo(Switch device, IFileSystem partitionFileSystem)
{ {
ulong applicationId = 0; ulong applicationId = 0;
int programCount = 0; int programCount = 0;

View file

@ -27,6 +27,7 @@
<PackageReference Include="SixLabors.ImageSharp" /> <PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" />
<PackageReference Include="NetCoreServer" />
</ItemGroup> </ItemGroup>
<!-- Due to Concentus. --> <!-- Due to Concentus. -->

View file

@ -65,7 +65,7 @@ namespace Ryujinx.Ui.App.Common
if (extension is ".nsp" or ".xci") if (extension is ".nsp" or ".xci")
{ {
PartitionFileSystem pfs; IFileSystem pfs;
if (extension == ".xci") if (extension == ".xci")
{ {
@ -75,7 +75,9 @@ namespace Ryujinx.Ui.App.Common
} }
else else
{ {
pfs = new PartitionFileSystem(file.AsStorage()); var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
pfs = pfsTemp;
} }
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))

View file

@ -174,7 +174,7 @@ namespace Ryujinx.Ui.App.Common
{ {
try try
{ {
PartitionFileSystem pfs; IFileSystem pfs;
bool isExeFs = false; bool isExeFs = false;
@ -186,7 +186,9 @@ namespace Ryujinx.Ui.App.Common
} }
else else
{ {
pfs = new PartitionFileSystem(file.AsStorage()); var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
pfs = pfsTemp;
// If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
bool hasMainNca = false; bool hasMainNca = false;
@ -500,7 +502,7 @@ namespace Ryujinx.Ui.App.Common
ApplicationCountUpdated?.Invoke(null, e); ApplicationCountUpdated?.Invoke(null, e);
} }
private void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId) private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId)
{ {
(_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0); (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0);
@ -563,7 +565,7 @@ namespace Ryujinx.Ui.App.Common
{ {
try try
{ {
PartitionFileSystem pfs; IFileSystem pfs;
bool isExeFs = false; bool isExeFs = false;
@ -575,7 +577,9 @@ namespace Ryujinx.Ui.App.Common
} }
else else
{ {
pfs = new PartitionFileSystem(file.AsStorage()); var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
pfs = pfsTemp;
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
{ {
@ -827,7 +831,7 @@ namespace Ryujinx.Ui.App.Common
return false; return false;
} }
public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex) public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex)
{ {
Nca mainNca = null; Nca mainNca = null;
Nca patchNca = null; Nca patchNca = null;
@ -936,7 +940,8 @@ namespace Ryujinx.Ui.App.Common
if (File.Exists(updatePath)) if (File.Exists(updatePath))
{ {
FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); FileStream file = new(updatePath, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new(file.AsStorage()); PartitionFileSystem nsp = new();
nsp.Initialize(file.AsStorage()).ThrowIfFailure();
return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex);
} }

View file

@ -571,6 +571,7 @@ namespace Ryujinx.Ui.Common.Configuration
{ {
LanInterfaceId = new ReactiveObject<string>(); LanInterfaceId = new ReactiveObject<string>();
Mode = new ReactiveObject<MultiplayerMode>(); Mode = new ReactiveObject<MultiplayerMode>();
Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode));
} }
} }

View file

@ -1156,6 +1156,14 @@ namespace Ryujinx.Ui
Graphics.Gpu.GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; Graphics.Gpu.GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE;
} }
public void UpdateInternetAccess()
{
if (_gameLoaded)
{
_emulationContext.Configuration.EnableInternetAccess = ConfigurationState.Instance.System.EnableInternetAccess.Value;
}
}
public static void SaveConfig() public static void SaveConfig()
{ {
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);

View file

@ -211,7 +211,7 @@ namespace Ryujinx.Ui.Widgets
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") ||
(System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci"))
{ {
PartitionFileSystem pfs; IFileSystem pfs;
if (System.IO.Path.GetExtension(_titleFilePath) == ".xci") if (System.IO.Path.GetExtension(_titleFilePath) == ".xci")
{ {
@ -221,7 +221,9 @@ namespace Ryujinx.Ui.Widgets
} }
else else
{ {
pfs = new PartitionFileSystem(file.AsStorage()); var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
pfs = pfsTemp;
} }
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))

View file

@ -88,7 +88,8 @@ namespace Ryujinx.Ui.Windows
using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
PartitionFileSystem pfs = new(containerFile.AsStorage()); PartitionFileSystem pfs = new();
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
_virtualFileSystem.ImportTickets(pfs); _virtualFileSystem.ImportTickets(pfs);
@ -153,7 +154,8 @@ namespace Ryujinx.Ui.Windows
using FileStream containerFile = File.OpenRead(containerPath); using FileStream containerFile = File.OpenRead(containerPath);
PartitionFileSystem pfs = new(containerFile.AsStorage()); PartitionFileSystem pfs = new();
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
bool containsDlc = false; bool containsDlc = false;
_virtualFileSystem.ImportTickets(pfs); _virtualFileSystem.ImportTickets(pfs);

View file

@ -671,6 +671,8 @@ namespace Ryujinx.Ui.Windows
} }
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
_parent.UpdateInternetAccess();
MainWindow.UpdateGraphicsConfig(); MainWindow.UpdateGraphicsConfig();
ThemeHelper.ApplyTheme(); ThemeHelper.ApplyTheme();
} }

View file

@ -2993,6 +2993,7 @@
<property name="active-id">Disabled</property> <property name="active-id">Disabled</property>
<items> <items>
<item id="Disabled" translatable="yes">Disabled</item> <item id="Disabled" translatable="yes">Disabled</item>
<item id="LdnMitm" translatable="yes">ldn_mitm</item>
</items> </items>
</object> </object>
<packing> <packing>
@ -3064,7 +3065,7 @@
<object class="GtkLabel"> <object class="GtkLabel">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property> <property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">The network interface used for LAN features</property> <property name="tooltip-text" translatable="yes">The network interface used for LAN/LDN features</property>
<property name="halign">end</property> <property name="halign">end</property>
<property name="label" translatable="yes">Network Interface:</property> <property name="label" translatable="yes">Network Interface:</property>
</object> </object>
@ -3079,7 +3080,7 @@
<object class="GtkComboBoxText" id="_multiLanSelect"> <object class="GtkComboBoxText" id="_multiLanSelect">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property> <property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">The network interface used for LAN features</property> <property name="tooltip-text" translatable="yes">The network interface used for LAN/LDN features</property>
<property name="active-id">0</property> <property name="active-id">0</property>
<items> <items>
<item id="0" translatable="yes">Default</item> <item id="0" translatable="yes">Default</item>

View file

@ -96,7 +96,8 @@ namespace Ryujinx.Ui.Windows
{ {
using FileStream file = new(path, FileMode.Open, FileAccess.Read); using FileStream file = new(path, FileMode.Open, FileAccess.Read);
PartitionFileSystem nsp = new(file.AsStorage()); PartitionFileSystem nsp = new();
nsp.Initialize(file.AsStorage()).ThrowIfFailure();
try try
{ {