Analyzer.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. using Gommon;
  2. using MsgPack;
  3. using Ryujinx.Ava.Utilities.AppLibrary;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Collections.ObjectModel;
  7. using System.Globalization;
  8. using System.Linq;
  9. namespace Ryujinx.Ava.Utilities.PlayReport
  10. {
  11. /// <summary>
  12. /// The entrypoint for the Play Report analysis system.
  13. /// </summary>
  14. public class Analyzer
  15. {
  16. private readonly List<GameSpec> _specs = [];
  17. public string[] TitleIds => Specs.SelectMany(x => x.TitleIds).ToArray();
  18. public IReadOnlyList<GameSpec> Specs => new ReadOnlyCollection<GameSpec>(_specs);
  19. /// <summary>
  20. /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
  21. /// </summary>
  22. /// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
  23. /// <param name="transform">The configuration function for the analysis spec.</param>
  24. /// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
  25. public Analyzer AddSpec(string titleId, Func<GameSpec, GameSpec> transform)
  26. {
  27. Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
  28. $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
  29. _specs.Add(transform(new GameSpec { TitleIds = [titleId] }));
  30. return this;
  31. }
  32. /// <summary>
  33. /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
  34. /// </summary>
  35. /// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
  36. /// <param name="transform">The configuration function for the analysis spec.</param>
  37. /// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
  38. public Analyzer AddSpec(string titleId, Action<GameSpec> transform)
  39. {
  40. Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
  41. $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
  42. _specs.Add(new GameSpec { TitleIds = [titleId] }.Apply(transform));
  43. return this;
  44. }
  45. /// <summary>
  46. /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
  47. /// </summary>
  48. /// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
  49. /// <param name="transform">The configuration function for the analysis spec.</param>
  50. /// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
  51. public Analyzer AddSpec(IEnumerable<string> titleIds,
  52. Func<GameSpec, GameSpec> transform)
  53. {
  54. string[] tids = titleIds.ToArray();
  55. Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
  56. $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
  57. _specs.Add(transform(new GameSpec { TitleIds = [..tids] }));
  58. return this;
  59. }
  60. /// <summary>
  61. /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
  62. /// </summary>
  63. /// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
  64. /// <param name="transform">The configuration function for the analysis spec.</param>
  65. /// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
  66. public Analyzer AddSpec(IEnumerable<string> titleIds, Action<GameSpec> transform)
  67. {
  68. string[] tids = titleIds.ToArray();
  69. Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
  70. $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
  71. _specs.Add(new GameSpec { TitleIds = [..tids] }.Apply(transform));
  72. return this;
  73. }
  74. /// <summary>
  75. /// Runs the configured <see cref="FormatterSpec"/> for the specified game title ID.
  76. /// </summary>
  77. /// <param name="runningGameId">The game currently running.</param>
  78. /// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param>
  79. /// <param name="playReport">The Play Report received from HLE.</param>
  80. /// <returns>A struct representing a possible formatted value.</returns>
  81. public FormattedValue Format(
  82. string runningGameId,
  83. ApplicationMetadata appMeta,
  84. Horizon.Prepo.Types.PlayReport playReport
  85. )
  86. {
  87. if (!playReport.ReportData.IsDictionary)
  88. return FormattedValue.Unhandled;
  89. if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
  90. return FormattedValue.Unhandled;
  91. foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
  92. {
  93. if (!playReport.ReportData.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
  94. continue;
  95. return formatSpec.Formatter(new SingleValue(valuePackObject)
  96. {
  97. Application = appMeta,
  98. PlayReport = playReport
  99. });
  100. }
  101. foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
  102. {
  103. List<MessagePackObject> packedObjects = [];
  104. foreach (var reportKey in formatSpec.ReportKeys)
  105. {
  106. if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
  107. continue;
  108. packedObjects.Add(valuePackObject);
  109. }
  110. if (packedObjects.Count != formatSpec.ReportKeys.Length)
  111. return FormattedValue.Unhandled;
  112. return formatSpec.Formatter(new MultiValue(packedObjects)
  113. {
  114. Application = appMeta,
  115. PlayReport = playReport
  116. });
  117. }
  118. foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority))
  119. {
  120. Dictionary<string, MessagePackObject> packedObjects = [];
  121. foreach (var reportKey in formatSpec.ReportKeys)
  122. {
  123. if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
  124. continue;
  125. packedObjects.Add(reportKey, valuePackObject);
  126. }
  127. return formatSpec.Formatter(
  128. new SparseMultiValue(packedObjects)
  129. {
  130. Application = appMeta,
  131. PlayReport = playReport
  132. });
  133. }
  134. return FormattedValue.Unhandled;
  135. }
  136. }
  137. }