using Gommon; using Humanizer; using LibHac.Ns; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Common.Utilities; using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; using System.Text.Json.Serialization; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Models { public record LdnGameModel { public string Id { get; private init; } public bool IsPublic { get; private init; } public short PlayerCount { get; private init; } public short MaxPlayerCount { get; private init; } public TitleTuple Title { get; private init; } public ConnectionType ConnectionType { get; private init; } public bool IsJoinable { get; private init; } public ushort SceneId { get; private init; } public string[] Players { get; private init; } public string PlayersLabel => LocaleManager.GetFormatted(LocaleKeys.LdnGameListPlayersAndPlayerCount, PlayerCount, MaxPlayerCount); public string FormattedPlayers => Players.Chunk(4) .Select(x => x.FormatCollection(s => s, prefix: " ", separator: ", ")) .JoinToString("\n "); public DateTimeOffset CreatedAt { get; init; } public string FormattedCreatedAt => LocaleManager.GetFormatted(LocaleKeys.LdnGameListCreatedAt, CreatedAt.Humanize()); public string CreatedAtToolTip => CreatedAt.DateTime.ToString(CultureInfo.CurrentUICulture); public LocaleKeys ConnectionTypeLocaleKey => ConnectionType switch { ConnectionType.MasterServerProxy => LocaleKeys.LdnGameListConnectionTypeMasterServerProxy, ConnectionType.PeerToPeer => LocaleKeys.LdnGameListConnectionTypeP2P, _ => throw new ArgumentOutOfRangeException(nameof(ConnectionType), $"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{ConnectionType}'") }; public LocaleKeys ConnectionTypeToolTipLocaleKey => ConnectionType switch { ConnectionType.MasterServerProxy => LocaleKeys.LdnGameListConnectionTypeMasterServerProxyToolTip, ConnectionType.PeerToPeer => LocaleKeys.LdnGameListConnectionTypeP2PToolTip, _ => throw new ArgumentOutOfRangeException(nameof(ConnectionType), $"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{ConnectionType}'") }; public record struct TitleTuple { public required string Name { get; init; } public required string Id { get; init; } public required string Version { get; init; } } public static Array GetArrayForApp( LdnGameModel[] receivedData, ref ApplicationControlProperty acp, bool onlyJoinable = true, bool onlyPublic = true) { LibHac.Common.FixedArrays.Array8 communicationId = acp.LocalCommunicationId; return new Array(receivedData.Where(game => communicationId.AsReadOnlySpan().Contains(game.Title.Id.ToULong()) ), onlyJoinable, onlyPublic); } public class Array : IEnumerable { private readonly LdnGameModel[] _ldnDatas; internal Array(IEnumerable receivedData, bool onlyJoinable = false, bool onlyPublic = false) { if (onlyJoinable) receivedData = receivedData.Where(x => x.IsJoinable); if (onlyPublic) receivedData = receivedData.Where(x => x.IsPublic); _ldnDatas = receivedData.ToArray(); } public int PlayerCount => _ldnDatas.Sum(it => it.PlayerCount); public int GameCount => _ldnDatas.Length; public IEnumerator GetEnumerator() => (_ldnDatas as IEnumerable).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _ldnDatas.GetEnumerator(); } public static async Task> GetAllAsync(HttpClient client = null) => LdnGameJsonModel.ParseArray(await GetAllAsyncRequestImpl(client)) .Select(FromJson); private static async Task GetAllAsyncRequestImpl(HttpClient client = null) { string ldnWebHost = ConfigurationState.Instance.Multiplayer.GetLdnWebServer(); LocaleManager.Associate(LocaleKeys.LdnGameListRefreshToolTip, ldnWebHost); try { if (client != null) return await client.GetStringAsync($"https://{ldnWebHost}/api/public_games"); using HttpClient httpClient = new(); return await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games"); } catch { return "[]"; } } private static LdnGameModel FromJson(LdnGameJsonModel json) => new() { Id = json.Id, IsPublic = json.IsPublic, PlayerCount = json.PlayerCount, MaxPlayerCount = json.MaxPlayerCount, Title = new TitleTuple { Name = json.TitleName, Id = json.TitleId, Version = json.TitleVersion }, ConnectionType = json.ConnectionType switch { "P2P" => ConnectionType.PeerToPeer, "Master Server Proxy" => ConnectionType.MasterServerProxy, _ => throw new ArgumentOutOfRangeException(nameof(json), $"Expected either 'P2P' or 'Master Server Proxy' ConnectionType; got '{json.ConnectionType}'") }, IsJoinable = json.Joinability is "Joinable", SceneId = json.SceneId, Players = json.Players, CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(json.CreatedAtUnixTimestamp).ToLocalTime() }; } public class LdnGameJsonModel { [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("is_public")] public bool IsPublic { get; set; } [JsonPropertyName("player_count")] public short PlayerCount { get; set; } [JsonPropertyName("max_player_count")] public short MaxPlayerCount { get; set; } [JsonPropertyName("game_name")] public string TitleName { get; set; } [JsonPropertyName("title_id")] public string TitleId { get; set; } [JsonPropertyName("title_version")] public string TitleVersion { get; set; } [JsonPropertyName("mode")] public string ConnectionType { get; set; } [JsonPropertyName("status")] public string Joinability { get; set; } [JsonPropertyName("scene_id")] public ushort SceneId { get; set; } [JsonPropertyName("players")] public string[] Players { get; set; } [JsonPropertyName("created_at")] public long CreatedAtUnixTimestamp { get; set; } public static LdnGameJsonModel Parse(string value) => JsonHelper.Deserialize(value, LdnGameJsonModelSerializerContext.Default.LdnGameJsonModel); public static LdnGameJsonModel[] ParseArray(string value) => JsonHelper.Deserialize(value, LdnGameJsonModelSerializerContext.Default.LdnGameJsonModelArray); } public enum ConnectionType { PeerToPeer, MasterServerProxy } [JsonSerializable(typeof(LdnGameJsonModel[]))] partial class LdnGameJsonModelSerializerContext : JsonSerializerContext; }