TESTERS WANTED: RyuLDN implementation (#65)

These changes allow players to matchmake for local wireless using a LDN
server. The network implementation originates from Berry's public TCP
RyuLDN fork. Logo and unrelated changes have been removed.

Additionally displays LDN game status in the game selection window when
RyuLDN is enabled.

Functionality is only enabled while network mode is set to "RyuLDN" in
the settings.
This commit is contained in:
Vudjun
2024-11-11 22:06:50 +00:00
committed by GitHub
parent abfcfcaf0f
commit 6d8738c048
93 changed files with 4100 additions and 189 deletions

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

@@ -1,4 +1,4 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
@@ -703,6 +703,9 @@ namespace Ryujinx.UI.Common.Configuration
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)
{

View File

@@ -13,7 +13,7 @@ namespace Ryujinx.UI.Common.Configuration
{
public partial class ConfigurationState
{
/// <summary>
/// <summary>
/// UI configuration section
/// </summary>
public class UISection
@@ -25,6 +25,7 @@ namespace Ryujinx.UI.Common.Configuration
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; }
@@ -38,6 +39,7 @@ namespace Ryujinx.UI.Common.Configuration
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>();
@@ -572,11 +574,32 @@ namespace Ryujinx.UI.Common.Configuration
/// </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));
}
}

View File

@@ -21,11 +21,11 @@ namespace Ryujinx.UI.Common.Configuration
if (Instance != null)
{
throw new InvalidOperationException("Configuration is already initialized");
}
}
Instance = new ConfigurationState();
}
}
public ConfigurationFileFormat ToFileFormat()
{
ConfigurationFileFormat configurationFile = new()
@@ -87,6 +87,7 @@ namespace Ryujinx.UI.Common.Configuration
AppColumn = UI.GuiColumns.AppColumn,
DevColumn = UI.GuiColumns.DevColumn,
VersionColumn = UI.GuiColumns.VersionColumn,
LdnInfoColumn = UI.GuiColumns.LdnInfoColumn,
TimePlayedColumn = UI.GuiColumns.TimePlayedColumn,
LastPlayedColumn = UI.GuiColumns.LastPlayedColumn,
FileExtColumn = UI.GuiColumns.FileExtColumn,
@@ -136,6 +137,9 @@ namespace Ryujinx.UI.Common.Configuration
PreferredGpu = Graphics.PreferredGpu,
MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId,
MultiplayerMode = Multiplayer.Mode,
MultiplayerDisableP2p = Multiplayer.DisableP2p,
MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase,
LdnServer = Multiplayer.LdnServer,
};
return configurationFile;
@@ -195,6 +199,9 @@ namespace Ryujinx.UI.Common.Configuration
System.UseHypervisor.Value = true;
Multiplayer.LanInterfaceId.Value = "0";
Multiplayer.Mode.Value = MultiplayerMode.Disabled;
Multiplayer.DisableP2p.Value = false;
Multiplayer.LdnPassphrase.Value = "";
Multiplayer.LdnServer.Value = "";
UI.GuiColumns.FavColumn.Value = true;
UI.GuiColumns.IconColumn.Value = true;
UI.GuiColumns.AppColumn.Value = true;
@@ -307,5 +314,5 @@ namespace Ryujinx.UI.Common.Configuration
return GraphicsBackend.OpenGl;
}
}
}
}
}

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