diff --git a/Directory.Packages.props b/Directory.Packages.props index 57db5394..c2427448 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,6 +17,7 @@ + @@ -37,6 +38,7 @@ + @@ -49,4 +51,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx.Common/Ryujinx.Common.csproj b/src/Ryujinx.Common/Ryujinx.Common.csproj index da2f13a2..11ce8543 100644 --- a/src/Ryujinx.Common/Ryujinx.Common.csproj +++ b/src/Ryujinx.Common/Ryujinx.Common.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Ryujinx/Modules/Compatibility/CompatibilityCsv.cs b/src/Ryujinx/Modules/Compatibility/CompatibilityCsv.cs new file mode 100644 index 00000000..861891e8 --- /dev/null +++ b/src/Ryujinx/Modules/Compatibility/CompatibilityCsv.cs @@ -0,0 +1,150 @@ +using Humanizer; +using nietras.SeparatedValues; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Common.Logging; +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Ryujinx.Ava.Modules.Compatibility +{ + public struct ColumnIndices(Func getIndex) + { + public const string TitleIdCol = "\"title_id\""; + public const string GameNameCol = "\"game_name\""; + public const string LabelsCol = "\"labels\""; + public const string StatusCol = "\"status\""; + public const string LastUpdatedCol = "\"last_updated\""; + + public readonly int TitleId = getIndex(TitleIdCol); + public readonly int GameName = getIndex(GameNameCol); + public readonly int Labels = getIndex(LabelsCol); + public readonly int Status = getIndex(StatusCol); + public readonly int LastUpdated = getIndex(LastUpdatedCol); + } + + public class CompatibilityCsv + { + static CompatibilityCsv() + { + using Stream csvStream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("RyujinxGameCompatibilityList")!; + csvStream.Position = 0; + + using SepReader reader = Sep.Reader().From(csvStream); + ColumnIndices columnIndices = new(reader.Header.IndexOf); + + Entries = reader + .Enumerate(row => new CompatibilityEntry(ref columnIndices, row)) + .OrderBy(it => it.GameName) + .ToArray(); + + Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatCsv"); + } + + public static CompatibilityEntry[] Entries { get; private set; } + } + + public class CompatibilityEntry + { + public CompatibilityEntry(ref ColumnIndices indices, SepReader.Row row) + { + string titleIdRow = ColStr(row[indices.TitleId]); + TitleId = !string.IsNullOrEmpty(titleIdRow) + ? titleIdRow + : null; + + GameName = ColStr(row[indices.GameName]); + + Labels = ColStr(row[indices.Labels]).Split(';'); + Status = ColStr(row[indices.Status]).ToLower() switch + { + "playable" => LocaleKeys.CompatibilityListPlayable, + "ingame" => LocaleKeys.CompatibilityListIngame, + "menus" => LocaleKeys.CompatibilityListMenus, + "boots" => LocaleKeys.CompatibilityListBoots, + "nothing" => LocaleKeys.CompatibilityListNothing, + _ => null + }; + + if (DateTime.TryParse(ColStr(row[indices.LastUpdated]), out var dt)) + LastUpdated = dt; + + return; + + string ColStr(SepReader.Col col) => col.ToString().Trim('"'); + } + + public string GameName { get; } + public string TitleId { get; } + public string[] Labels { get; } + internal LocaleKeys? Status { get; } + public DateTime LastUpdated { get; } + + public string LocalizedLastUpdated => + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize()); + + public string LocalizedStatus => LocaleManager.Instance[Status!.Value]; + public string FormattedTitleId => TitleId ?? new string(' ', 16); + + public string FormattedIssueLabels => string.Join(",", Labels + .Select(FormatLabelName)); + + public static string FormatLabelName(string labelName) => labelName.ToLower() switch + { + "audio" => "Audio", + "bug" => "Bug", + "cpu" => "CPU", + "gpu" => "GPU", + "gui" => "GUI", + "help wanted" => "Help Wanted", + "horizon" => "Horizon", + "infra" => "Project Infra", + "invalid" => "Invalid", + "kernel" => "Kernel", + "ldn" => "LDN", + "linux" => "Linux", + "macos" => "macOS", + "question" => "Question", + "windows" => "Windows", + "graphics-backend:opengl" => "Graphics: OpenGL", + "graphics-backend:vulkan" => "Graphics: Vulkan", + "ldn-works" => "LDN Works", + "ldn-untested" => "LDN Untested", + "ldn-broken" => "LDN Broken", + "ldn-partial" => "Partial LDN", + "nvdec" => "NVDEC", + "services" => "NX Services", + "services-horizon" => "Horizon OS Services", + "slow" => "Runs Slow", + "crash" => "Crashes", + "deadlock" => "Deadlock", + "regression" => "Regression", + "opengl" => "OpenGL", + "opengl-backend-bug" => "OpenGL Backend Bug", + "vulkan-backend-bug" => "Vulkan Backend Bug", + "mac-bug" => "Mac-specific Bug(s)", + "amd-vendor-bug" => "AMD GPU Bug", + "intel-vendor-bug" => "Intel GPU Bug", + "loader-allocator" => "Loader Allocator", + "audout" => "AudOut", + "32-bit" => "32-bit Game", + "UE4" => "Unreal Engine 4", + "homebrew" => "Homebrew Content", + "online-broken" => "Online Broken", + _ => Capitalize(labelName) + }; + + public static string Capitalize(string value) + { + if (value == string.Empty) + return string.Empty; + + char firstChar = value[0]; + string rest = value[1..]; + + return $"{char.ToUpper(firstChar)}{rest}"; + } + } +} diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 8303fac5..5d390591 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -44,7 +44,6 @@ - @@ -52,6 +51,7 @@ + @@ -137,6 +137,9 @@ + + Assets\RyujinxGameCompatibility.csv +