CompatibilityCsv.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. using Gommon;
  2. using Humanizer;
  3. using nietras.SeparatedValues;
  4. using Ryujinx.Ava.Common.Locale;
  5. using Ryujinx.Common.Logging;
  6. using System;
  7. using System.IO;
  8. using System.Linq;
  9. using System.Reflection;
  10. using System.Text;
  11. namespace Ryujinx.Ava.Utilities.Compat
  12. {
  13. public struct ColumnIndices(Func<ReadOnlySpan<char>, int> getIndex)
  14. {
  15. public const string TitleIdCol = "\"title_id\"";
  16. public const string GameNameCol = "\"game_name\"";
  17. public const string LabelsCol = "\"labels\"";
  18. public const string StatusCol = "\"status\"";
  19. public const string LastUpdatedCol = "\"last_updated\"";
  20. public readonly int TitleId = getIndex(TitleIdCol);
  21. public readonly int GameName = getIndex(GameNameCol);
  22. public readonly int Labels = getIndex(LabelsCol);
  23. public readonly int Status = getIndex(StatusCol);
  24. public readonly int LastUpdated = getIndex(LastUpdatedCol);
  25. }
  26. public class CompatibilityCsv
  27. {
  28. static CompatibilityCsv() => Load();
  29. public static void Load()
  30. {
  31. using Stream csvStream = Assembly.GetExecutingAssembly()
  32. .GetManifestResourceStream("RyujinxGameCompatibilityList")!;
  33. csvStream.Position = 0;
  34. using SepReader reader = Sep.Reader().From(csvStream);
  35. ColumnIndices columnIndices = new(reader.Header.IndexOf);
  36. _entries = reader
  37. .Enumerate(row => new CompatibilityEntry(ref columnIndices, row))
  38. .OrderBy(it => it.GameName)
  39. .ToArray();
  40. Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
  41. }
  42. private static CompatibilityEntry[] _entries;
  43. public static CompatibilityEntry[] Entries
  44. {
  45. get
  46. {
  47. if (_entries == null)
  48. Load();
  49. return _entries;
  50. }
  51. }
  52. public static CompatibilityEntry Find(string titleId)
  53. => Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId));
  54. public static CompatibilityEntry Find(ulong titleId)
  55. => Find(titleId.ToString("X16"));
  56. public static LocaleKeys? GetStatus(string titleId)
  57. => Find(titleId)?.Status;
  58. public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16"));
  59. public static string GetLabels(string titleId)
  60. => Find(titleId)?.FormattedIssueLabels;
  61. public static string GetLabels(ulong titleId) => GetLabels(titleId.ToString("X16"));
  62. }
  63. public class CompatibilityEntry
  64. {
  65. public CompatibilityEntry(ref ColumnIndices indices, SepReader.Row row)
  66. {
  67. string titleIdRow = ColStr(row[indices.TitleId]);
  68. TitleId = !string.IsNullOrEmpty(titleIdRow)
  69. ? titleIdRow
  70. : default(Optional<string>);
  71. GameName = ColStr(row[indices.GameName]);
  72. Labels = ColStr(row[indices.Labels]).Split(';');
  73. Status = ColStr(row[indices.Status]).ToLower() switch
  74. {
  75. "playable" => LocaleKeys.CompatibilityListPlayable,
  76. "ingame" => LocaleKeys.CompatibilityListIngame,
  77. "menus" => LocaleKeys.CompatibilityListMenus,
  78. "boots" => LocaleKeys.CompatibilityListBoots,
  79. "nothing" => LocaleKeys.CompatibilityListNothing,
  80. _ => null
  81. };
  82. if (DateTime.TryParse(ColStr(row[indices.LastUpdated]), out DateTime dt))
  83. LastUpdated = dt;
  84. return;
  85. string ColStr(SepReader.Col col) => col.ToString().Trim('"');
  86. }
  87. public string GameName { get; }
  88. public Optional<string> TitleId { get; }
  89. public string[] Labels { get; }
  90. public LocaleKeys? Status { get; }
  91. public LocaleKeys? StatusDescription
  92. => Status switch
  93. {
  94. LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
  95. LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
  96. LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
  97. LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
  98. LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
  99. _ => null
  100. };
  101. public DateTime LastUpdated { get; }
  102. public string LocalizedLastUpdated =>
  103. LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize());
  104. public string LocalizedStatus => LocaleManager.Instance[Status!.Value];
  105. public string LocalizedStatusDescription => LocaleManager.Instance[StatusDescription!.Value];
  106. public string FormattedTitleId => TitleId
  107. .OrElse(new string(' ', 16));
  108. public string FormattedIssueLabels => Labels
  109. .Select(FormatLabelName)
  110. .JoinToString(", ");
  111. public override string ToString() =>
  112. new StringBuilder("CompatibilityEntry: {")
  113. .Append($"{nameof(GameName)}=\"{GameName}\", ")
  114. .Append($"{nameof(TitleId)}={TitleId}, ")
  115. .Append($"{nameof(Labels)}={
  116. Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
  117. }, ")
  118. .Append($"{nameof(Status)}=\"{Status}\", ")
  119. .Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"")
  120. .Append('}')
  121. .ToString();
  122. public static string FormatLabelName(string labelName) => labelName.ToLower() switch
  123. {
  124. "audio" => "Audio",
  125. "bug" => "Bug",
  126. "cpu" => "CPU",
  127. "gpu" => "GPU",
  128. "gui" => "GUI",
  129. "help wanted" => "Help Wanted",
  130. "horizon" => "Horizon",
  131. "infra" => "Project Infra",
  132. "invalid" => "Invalid",
  133. "kernel" => "Kernel",
  134. "ldn" => "LDN",
  135. "linux" => "Linux",
  136. "macos" => "macOS",
  137. "question" => "Question",
  138. "windows" => "Windows",
  139. "graphics-backend:opengl" => "Graphics: OpenGL",
  140. "graphics-backend:vulkan" => "Graphics: Vulkan",
  141. "ldn-works" => "LDN Works",
  142. "ldn-untested" => "LDN Untested",
  143. "ldn-broken" => "LDN Broken",
  144. "ldn-partial" => "Partial LDN",
  145. "nvdec" => "NVDEC",
  146. "services" => "NX Services",
  147. "services-horizon" => "Horizon OS Services",
  148. "slow" => "Runs Slow",
  149. "crash" => "Crashes",
  150. "deadlock" => "Deadlock",
  151. "regression" => "Regression",
  152. "opengl" => "OpenGL",
  153. "opengl-backend-bug" => "OpenGL Backend Bug",
  154. "vulkan-backend-bug" => "Vulkan Backend Bug",
  155. "mac-bug" => "Mac-specific Bug(s)",
  156. "amd-vendor-bug" => "AMD GPU Bug",
  157. "intel-vendor-bug" => "Intel GPU Bug",
  158. "loader-allocator" => "Loader Allocator",
  159. "audout" => "AudOut",
  160. "32-bit" => "32-bit Game",
  161. "UE4" => "Unreal Engine 4",
  162. "homebrew" => "Homebrew Content",
  163. "online-broken" => "Online Broken",
  164. _ => Capitalize(labelName)
  165. };
  166. public static string Capitalize(string value)
  167. {
  168. if (value == string.Empty)
  169. return string.Empty;
  170. char firstChar = value[0];
  171. string rest = value[1..];
  172. return $"{char.ToUpper(firstChar)}{rest}";
  173. }
  174. }
  175. }