diff --git a/docs/compatibility.csv b/docs/compatibility.csv
index 53ad389b6..0fd8eadca 100644
--- a/docs/compatibility.csv
+++ b/docs/compatibility.csv
@@ -332,6 +332,7 @@
0100E680149DC000,"Arcaea",,playable,2023-03-16 19:31:21
01003C2010C78000,"Archaica: The Path Of Light",crash,nothing,2020-10-16 13:22:26
01004DA012976000,"Area 86",,playable,2020-12-16 16:45:52
+01008d8006a6a000,"Arena of Valor",crash,boots,2025-02-03 22:19:34
0100691013C46000,"ARIA CHRONICLE",,playable,2022-11-16 13:50:55
0100D4A00B284000,"ARK: Survival Evolved",gpu;nvdec;online-broken;UE4;ldn-untested,ingame,2024-04-16 00:53:56
0100C56012C96000,"Arkanoid vs. Space Invaders",services,ingame,2021-01-21 12:50:30
@@ -426,6 +427,7 @@
0100E48013A34000,"Balan Wonderworld Demo",gpu;services;UE4;demo,ingame,2023-02-16 20:05:07
0100CD801CE5E000,"Balatro",,ingame,2024-04-21 02:01:53
010010A00DA48000,"Baldur's Gate and Baldur's Gate II: Enhanced Editions",32-bit,playable,2022-09-12 23:52:15
+0100fd1014726000,"Baldur's Gate: Dark Alliance",ldn-untested,ingame,2025-02-03 22:21:00
0100BC400FB64000,"Balthazar's Dream",,playable,2022-09-13 00:13:22
01008D30128E0000,"Bamerang",,playable,2022-10-26 00:29:39
010013C010C5C000,"Banner of the Maid",,playable,2021-06-14 15:23:37
@@ -528,6 +530,7 @@
01005950022EC000,"Blade Strangers",nvdec,playable,2022-07-17 19:02:43
0100DF0011A6A000,"Bladed Fury",,playable,2022-10-26 11:36:26
0100CFA00CC74000,"Blades of Time",deadlock;online,boots,2022-07-17 19:19:58
+01003d700dd8a000,"Blades",,boots,2025-02-03 22:22:00
01006CC01182C000,"Blair Witch",nvdec;UE4,playable,2022-10-01 14:06:16
010039501405E000,"Blanc",gpu;slow,ingame,2023-02-22 14:00:13
0100698009C6E000,"Blasphemous",nvdec,playable,2021-03-01 12:15:31
@@ -955,7 +958,7 @@
010012800EBAE000,"Disney TSUM TSUM FESTIVAL",crash,menus,2020-07-14 14:05:28
01009740120FE000,"DISTRAINT 2",,playable,2020-09-03 16:08:12
010075B004DD2000,"DISTRAINT: Deluxe Edition",,playable,2020-06-15 23:42:24
-010027400CDC6000,"Divinity: Original Sin 2 - Definitive Edition",services;crash;online-broken;regression,menus,2023-08-13 17:20:03
+010027400CDC6000,"Divinity: Original Sin 2 - Definitive Edition",services;crash;online-broken;regression,ingame,2025-02-03 22:12:30
01001770115C8000,"Dodo Peak",nvdec;UE4,playable,2022-10-04 16:13:05
010077B0100DA000,"Dogurai",,playable,2020-10-04 02:40:16
010048100D51A000,"Dokapon Up! Mugen no Roulette",gpu;Needs Update,menus,2022-12-08 19:39:10
@@ -1654,7 +1657,7 @@
0100A73006E74000,"Legendary Eleven",,playable,2021-06-08 12:09:03
0100A7700B46C000,"Legendary Fishing",online,playable,2021-04-14 15:08:46
0100739018020000,"LEGO® 2K Drive",gpu;ldn-works,ingame,2024-04-09 02:05:12
-01003A30012C0000,"LEGO® CITY Undercover",nvdec,playable,2024-09-30 08:44:27
+010085500130a000,"LEGO® CITY Undercover",nvdec,playable,2024-09-30 08:44:27
010070D009FEC000,"LEGO® DC Super-Villains",,playable,2021-05-27 18:10:37
010052A00B5D2000,"LEGO® Harry Potter™ Collection",crash,ingame,2024-01-31 10:28:07
010073C01AF34000,"LEGO® Horizon Adventures™",vulkan-backend-bug;opengl-backend-bug;UE4,ingame,2025-01-07 04:24:56
@@ -1913,6 +1916,7 @@
010073E008E6E000,"Mugsters",,playable,2021-01-28 17:57:17
0100A8400471A000,"MUJO",,playable,2020-05-08 16:31:04
0100211005E94000,"Mulaka",,playable,2021-01-28 18:07:20
+01008e2013fb4000,"Multi Quiz",ldn-untested,ingame,2025-02-03 22:26:00
010038B00B9AE000,"Mummy Pinball",,playable,2022-08-05 16:08:11
01008E200C5C2000,"Muse Dash",,playable,2020-06-06 14:41:29
010035901046C000,"Mushroom Quest",,playable,2020-05-17 13:07:08
@@ -2028,6 +2032,7 @@
010003C00B868000,"Ninjin: Clash of Carrots",online-broken,playable,2024-07-10 05:12:26
0100746010E4C000,"NinNinDays",,playable,2022-11-20 15:17:29
0100C9A00ECE6000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07
+0100e0601c632000,"Nintendo 64™ – Nintendo Switch Online: MATURE 17+",,ingame,2025-02-03 22:27:00
0100D870045B6000,"Nintendo Entertainment System™ - Nintendo Switch Online",online,playable,2022-07-01 15:45:06
0100C4B0034B2000,"Nintendo Labo Toy-Con 01 Variety Kit",gpu,ingame,2022-08-07 12:56:07
01001E9003502000,"Nintendo Labo Toy-Con 03 Vehicle Kit",services;crash,menus,2022-08-03 17:20:11
@@ -2532,7 +2537,7 @@
0100C3E00B700000,"SEGA AGES Space Harrier",,playable,2021-01-11 12:57:40
010054400D2E6000,"SEGA AGES Virtua Racing",online-broken,playable,2023-01-29 17:08:39
01001E700AC60000,"SEGA AGES Wonder Boy: Monster Land",online,playable,2021-05-05 16:28:25
-0100B3C014BDA000,"SEGA Genesis™ – Nintendo Switch Online",crash;regression,nothing,2022-04-11 07:27:21
+0100B3C014BDA000,"SEGA Genesis™ – Nintendo Switch Online",crash;regression,ingame,2025-02-03 22:13:30
0100F7300B24E000,"SEGA Mega Drive Classics",online,playable,2021-01-05 11:08:00
01009840046BC000,"Semispheres",,playable,2021-01-06 23:08:31
0100D1800D902000,"SENRAN KAGURA Peach Ball",,playable,2021-06-03 15:12:10
@@ -2964,6 +2969,7 @@
0100C38004DCC000,"The Flame In The Flood: Complete Edition",gpu;nvdec;UE4,ingame,2022-08-22 16:23:49
010007700D4AC000,"The Forbidden Arts",,playable,2021-01-26 16:26:24
010030700CBBC000,"The friends of Ringo Ishikawa",,playable,2022-08-22 16:33:17
+0100b620139d8000,"The Game of Life 2",ldn-untested,ingame,2025-02-03 22:30:00
01006350148DA000,"The Gardener and the Wild Vines",gpu,ingame,2024-04-29 16:32:10
0100B13007A6A000,"The Gardens Between",,playable,2021-01-29 16:16:53
010036E00FB20000,"The Great Ace Attorney Chronicles",,playable,2023-06-22 21:26:29
@@ -2981,6 +2987,8 @@
010015D003EE4000,"The Jackbox Party Pack 2",online-working,playable,2022-08-22 18:23:40
0100CC80013D6000,"The Jackbox Party Pack 3",slow;online-working,playable,2022-08-22 18:41:06
0100E1F003EE8000,"The Jackbox Party Pack 4",online-working,playable,2022-08-22 18:56:34
+01006fe0096ac000,"The Jackbox Party Pack 5",ldn-untested,boots,2025-02-03 22:32:00
+01005a400db52000,"The Jackbox Party Pack 6",ldn-untested,boots,2025-02-03 22:32:00
010052C00B184000,"The Journey Down: Chapter One",nvdec,playable,2021-02-24 13:32:41
01006BC00B188000,"The Journey Down: Chapter Three",nvdec,playable,2021-02-24 13:45:27
01009AB00B186000,"The Journey Down: Chapter Two",nvdec,playable,2021-02-24 13:32:13
@@ -3159,6 +3167,7 @@
010055E00CA68000,"Trine 4: The Nightmare Prince",gpu,nothing,2025-01-07 05:47:46
0100D9000A930000,"Trine Enchanted Edition",ldn-untested;nvdec,playable,2021-06-03 11:28:15
01002D7010A54000,"Trinity Trigger",crash,ingame,2023-03-03 03:09:09
+010020700a5e0000,"TRIVIAL PURSUIT Live!",ldn-untested,ingame,2025-02-03 22:35:00
0100868013FFC000,"TRIVIAL PURSUIT Live! 2",,boots,2022-12-19 00:04:33
0100F78002040000,"Troll and I™",gpu;nvdec,ingame,2021-06-04 16:58:50
0100145011008000,"Trollhunters: Defenders of Arcadia",gpu;nvdec,ingame,2020-11-30 13:27:09
@@ -3208,6 +3217,7 @@
0100AB2010B4C000,"Unlock The King",,playable,2020-09-01 13:58:27
0100A3E011CB0000,"Unlock the King 2",,playable,2021-06-15 20:43:55
01005AA00372A000,"UNO® for Nintendo Switch",nvdec;ldn-untested,playable,2022-07-28 14:49:47
+0100b6e012ebe000,"UNO",ldn-untested,ingame,2025-02-03 22:40:00
0100E5D00CC0C000,"Unravel Two",nvdec,playable,2024-05-23 15:45:05
010001300CC4A000,"Unruly Heroes",,playable,2021-01-07 18:09:31
0100B410138C0000,"Unspottable",,playable,2022-10-25 19:28:49
@@ -3372,6 +3382,7 @@
0100F47016F26000,"Yomawari 3",,playable,2022-05-10 08:26:51
010012F00B6F2000,"Yomawari: The Long Night Collection",,playable,2022-09-03 14:36:59
0100CC600ABB2000,"Yonder: The Cloud Catcher Chronicles (Retail Only)",,playable,2021-01-28 14:06:25
+0100534009ff2000,"Yonder: The Cloud Catcher Chronicles",,playable,2025-02-03 22:19:13
0100BE50042F6000,"Yono and the Celestial Elephants",,playable,2021-01-28 18:23:58
0100F110029C8000,"Yooka-Laylee",,playable,2021-01-28 14:21:45
010022F00DA66000,"Yooka-Laylee and the Impossible Lair",,playable,2021-03-05 17:32:21
diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json
index 8cf5b0d7c..25a40b29e 100644
--- a/src/Ryujinx/Assets/locales.json
+++ b/src/Ryujinx/Assets/locales.json
@@ -1576,50 +1576,50 @@
"ID": "GameListHeaderTimePlayed",
"Translations": {
"ar_SA": "",
- "de_DE": "Spielzeit: {0}",
- "el_GR": "Χρόνος: {0}",
- "en_US": "Play Time: {0}",
- "es_ES": "Tiempo jugado: {0}",
- "fr_FR": "Temps de jeu: {0}",
+ "de_DE": "Spielzeit:",
+ "el_GR": "Χρόνος:",
+ "en_US": "Play Time:",
+ "es_ES": "Tiempo jugado:",
+ "fr_FR": "Temps de jeu:",
"he_IL": "",
- "it_IT": "Tempo di gioco: {0}",
- "ja_JP": "プレイ時間: {0}",
- "ko_KR": "플레이 타임: {0}",
- "no_NO": "Spilletid: {0}",
- "pl_PL": "Czas w grze: {0}",
- "pt_BR": "Tempo de jogo: {0}",
- "ru_RU": "Время в игре: {0}",
- "sv_SE": "Speltid: {0}",
- "th_TH": "เล่นไปแล้ว: {0}",
- "tr_TR": "Oynama Süresi: {0}",
- "uk_UA": "Зіграно часу: {0}",
- "zh_CN": "游玩时长: {0}",
- "zh_TW": "遊玩時數: {0}"
+ "it_IT": "Tempo di gioco:",
+ "ja_JP": "プレイ時間:",
+ "ko_KR": "플레이 타임:",
+ "no_NO": "Spilletid:",
+ "pl_PL": "Czas w grze:",
+ "pt_BR": "Tempo de jogo:",
+ "ru_RU": "Время в игре:",
+ "sv_SE": "Speltid:",
+ "th_TH": "เล่นไปแล้ว:",
+ "tr_TR": "Oynama Süresi:",
+ "uk_UA": "Зіграно часу:",
+ "zh_CN": "游玩时长:",
+ "zh_TW": "遊玩時數:"
}
},
{
"ID": "GameListHeaderLastPlayed",
"Translations": {
"ar_SA": "",
- "de_DE": "Zuletzt gespielt: {0}",
- "el_GR": "Παίχτηκε: {0}",
- "en_US": "Last Played: {0}",
- "es_ES": "Jugado por última vez: {0}",
- "fr_FR": "Dernière partie jouée: {0}",
+ "de_DE": "Zuletzt gespielt: ",
+ "el_GR": "Παίχτηκε: ",
+ "en_US": "Last Played:",
+ "es_ES": "Jugado por última vez:",
+ "fr_FR": "Dernière partie jouée:",
"he_IL": "",
- "it_IT": "Ultima partita: {0}",
- "ja_JP": "最終プレイ日時: {0}",
- "ko_KR": "마지막 플레이: {0}",
- "no_NO": "Sist Spilt: {0}",
- "pl_PL": "Ostatnio grane: {0}",
- "pt_BR": "Último jogo: {0}",
- "ru_RU": "Последний запуск: {0}",
- "sv_SE": "Senast spelad: {0}",
- "th_TH": "เล่นล่าสุด: {0}",
- "tr_TR": "Son Oynama Tarihi: {0}",
- "uk_UA": "Востаннє зіграно: {0}",
- "zh_CN": "最近游玩: {0}",
- "zh_TW": "最近遊玩: {0}"
+ "it_IT": "Ultima partita:",
+ "ja_JP": "最終プレイ日時:",
+ "ko_KR": "마지막 플레이:",
+ "no_NO": "Sist Spilt:",
+ "pl_PL": "Ostatnio grane:",
+ "pt_BR": "Último jogo:",
+ "ru_RU": "Последний запуск:",
+ "sv_SE": "Senast spelad:",
+ "th_TH": "เล่นล่าสุด:",
+ "tr_TR": "Son Oynama Tarihi:",
+ "uk_UA": "Востаннє зіграно:",
+ "zh_CN": "最近游玩:",
+ "zh_TW": "最近遊玩:"
}
},
{
@@ -23696,6 +23696,56 @@
"zh_CN": "选择一个要解压的 DLC",
"zh_TW": ""
}
+ },
+ {
+ "ID": "GameInfoRpcImage",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Rich Presence Image",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
+ },
+ {
+ "ID": "GameInfoRpcDynamic",
+ "Translations": {
+ "ar_SA": "",
+ "de_DE": "",
+ "el_GR": "",
+ "en_US": "Dynamic Rich Presence",
+ "es_ES": "",
+ "fr_FR": "",
+ "he_IL": "",
+ "it_IT": "",
+ "ja_JP": "",
+ "ko_KR": "",
+ "no_NO": "",
+ "pl_PL": "",
+ "pt_BR": "",
+ "ru_RU": "",
+ "sv_SE": "",
+ "th_TH": "",
+ "tr_TR": "",
+ "uk_UA": "",
+ "zh_CN": "",
+ "zh_TW": ""
+ }
}
]
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs
index 20b296511..229b6ee09 100644
--- a/src/Ryujinx/DiscordIntegrationModule.cs
+++ b/src/Ryujinx/DiscordIntegrationModule.cs
@@ -4,15 +4,12 @@ using MsgPack;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
+using Ryujinx.Ava.Utilities.PlayReport;
using Ryujinx.Common;
-using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon;
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
@@ -41,6 +38,9 @@ namespace Ryujinx.Ava
private static RichPresence _discordPresencePlaying;
private static ApplicationMetadata _currentApp;
+ public static bool HasAssetImage(string titleId) => TitleIDs.DiscordGameAssetKeys.ContainsIgnoreCase(titleId);
+ public static bool HasAnalyzer(string titleId) => PlayReports.Analyzer.TitleIds.ContainsIgnoreCase(titleId);
+
public static void Initialize()
{
_discordPresenceMain = new RichPresence
@@ -130,14 +130,16 @@ namespace Ryujinx.Ava
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
if (_discordPresencePlaying is null) return;
- PlayReportAnalyzer.FormattedValue formattedValue =
- PlayReport.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
+ FormattedValue formattedValue =
+ PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
if (!formattedValue.Handled) return;
- _discordPresencePlaying.Details = formattedValue.Reset
- ? $"Playing {_currentApp.Title}"
- : formattedValue.FormattedString;
+ _discordPresencePlaying.Details = TruncateToByteLength(
+ formattedValue.Reset
+ ? $"Playing {_currentApp.Title}"
+ : formattedValue.FormattedString
+ );
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
return; //don't trigger an update if the set presence Details are identical to current
diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml
index 45ae75639..c40b6e192 100644
--- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml
+++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml
@@ -4,6 +4,7 @@
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
+ xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
@@ -85,6 +86,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx/UI/Helpers/Converters/PlayabilityStatusConverter.cs b/src/Ryujinx/UI/Helpers/Converters/PlayabilityStatusConverter.cs
index 99c6a0fce..c8082ec62 100644
--- a/src/Ryujinx/UI/Helpers/Converters/PlayabilityStatusConverter.cs
+++ b/src/Ryujinx/UI/Helpers/Converters/PlayabilityStatusConverter.cs
@@ -18,7 +18,7 @@ namespace Ryujinx.Ava.UI.Helpers
LocaleKeys.CompatibilityListNothing or
LocaleKeys.CompatibilityListBoots or
LocaleKeys.CompatibilityListMenus => Brushes.Red,
- LocaleKeys.CompatibilityListIngame => Brushes.Yellow,
+ LocaleKeys.CompatibilityListIngame => Brushes.DarkOrange,
_ => Brushes.ForestGreen
};
diff --git a/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs
index 9e0a3554a..592d5f81a 100644
--- a/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/ApplicationDataViewModel.cs
@@ -12,10 +12,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
-
public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);
- public string FormattedLastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed].Format(AppData.LastPlayedString);
- public string FormattedPlayTime => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed].Format(AppData.TimePlayedString);
public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString);
public string FormattedLdnInfo =>
diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs
index 18bf39c96..99c81ee44 100644
--- a/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs
+++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs
@@ -63,6 +63,9 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public int GameCount { get; set; }
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
+
+ public bool HasRichPresenceAsset => DiscordIntegrationModule.HasAssetImage(IdString);
+ public bool HasDynamicRichPresenceSupport => DiscordIntegrationModule.HasAnalyzer(IdString);
public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs
deleted file mode 100644
index f518fb902..000000000
--- a/src/Ryujinx/Utilities/PlayReport.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
-
-namespace Ryujinx.Ava.Utilities
-{
- public static class PlayReport
- {
- public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
- .AddSpec(
- "01007ef00011e000",
- spec => spec
- .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
- // reset to normal status when switching between normal & master mode in title screen
- .AddValueFormatter("AoCVer", PlayReportFormattedValue.AlwaysResets)
- )
- .AddSpec(
- "0100f2c0115b6000",
- spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
- .AddSpec(
- "0100000000010000",
- spec =>
- spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
- )
- .AddSpec(
- "010075000ecbe000",
- spec =>
- spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
- )
- .AddSpec(
- "010028600ebda000",
- spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
- )
- .AddSpec( // Global & China IDs
- ["0100152000022000", "010075100e8ec000"],
- spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
- );
-
- private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
- => value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
-
- private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) =>
- value.DoubleValue switch
- {
- > 800d => "Exploring the Sky Islands",
- < -201d => "Exploring the Depths",
- _ => "Roaming Hyrule"
- };
-
- private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(PlayReportValue value)
- => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
-
- private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
- => value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
-
- private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
- => value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
-
- private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value)
- => value.StringValue switch
- {
- // Single Player
- "Single" => "Single Player",
- // Multiplayer
- "Multi-2players" => "Multiplayer 2 Players",
- "Multi-3players" => "Multiplayer 3 Players",
- "Multi-4players" => "Multiplayer 4 Players",
- // Wireless/LAN Play
- "Local-Single" => "Wireless/LAN Play",
- "Local-2players" => "Wireless/LAN Play 2 Players",
- // CC Classes
- "50cc" => "50cc",
- "100cc" => "100cc",
- "150cc" => "150cc",
- "Mirror" => "Mirror (150cc)",
- "200cc" => "200cc",
- // Modes
- "GrandPrix" => "Grand Prix",
- "TimeAttack" => "Time Trials",
- "VS" => "VS Races",
- "Battle" => "Battle Mode",
- "RaceStart" => "Selecting a Course",
- "Race" => "Racing",
- _ => PlayReportFormattedValue.ForceReset
- };
- }
-}
diff --git a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs
new file mode 100644
index 000000000..338c198a1
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs
@@ -0,0 +1,150 @@
+using Gommon;
+using MsgPack;
+using Ryujinx.Ava.Utilities.AppLibrary;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ ///
+ /// The entrypoint for the Play Report analysis system.
+ ///
+ public class Analyzer
+ {
+ private readonly List _specs = [];
+
+ public string[] TitleIds => Specs.SelectMany(x => x.TitleIds).ToArray();
+
+ public IReadOnlyList Specs => new ReadOnlyCollection(_specs);
+
+ ///
+ /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
+ ///
+ /// The ID of the game to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
+ public Analyzer AddSpec(string titleId, Func transform)
+ {
+ Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
+
+ _specs.Add(transform(new GameSpec { TitleIds = [titleId] }));
+ return this;
+ }
+
+ ///
+ /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
+ ///
+ /// The ID of the game to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
+ public Analyzer AddSpec(string titleId, Action transform)
+ {
+ Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
+
+ _specs.Add(new GameSpec { TitleIds = [titleId] }.Apply(transform));
+ return this;
+ }
+
+ ///
+ /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
+ ///
+ /// The IDs of the games to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
+ public Analyzer AddSpec(IEnumerable titleIds,
+ Func transform)
+ {
+ string[] tids = titleIds.ToArray();
+ Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
+
+ _specs.Add(transform(new GameSpec { TitleIds = [..tids] }));
+ return this;
+ }
+
+ ///
+ /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
+ ///
+ /// The IDs of the games to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
+ public Analyzer AddSpec(IEnumerable titleIds, Action transform)
+ {
+ string[] tids = titleIds.ToArray();
+ Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
+
+ _specs.Add(new GameSpec { TitleIds = [..tids] }.Apply(transform));
+ return this;
+ }
+
+
+ ///
+ /// Runs the configured for the specified game title ID.
+ ///
+ /// The game currently running.
+ /// The Application metadata information, including localized game name and play time information.
+ /// The Play Report received from HLE.
+ /// A struct representing a possible formatted value.
+ public FormattedValue Format(
+ string runningGameId,
+ ApplicationMetadata appMeta,
+ MessagePackObject playReport
+ )
+ {
+ if (!playReport.IsDictionary)
+ return FormattedValue.Unhandled;
+
+ if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
+ return FormattedValue.Unhandled;
+
+ foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
+ {
+ if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
+ continue;
+
+ return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject });
+ }
+
+ foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
+ {
+ List packedObjects = [];
+ foreach (var reportKey in formatSpec.ReportKeys)
+ {
+ if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
+ continue;
+
+ packedObjects.Add(valuePackObject);
+ }
+
+ if (packedObjects.Count != formatSpec.ReportKeys.Length)
+ return FormattedValue.Unhandled;
+
+ return formatSpec.Formatter(packedObjects
+ .Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
+ .ToArray());
+ }
+
+ foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority))
+ {
+ Dictionary packedObjects = [];
+ foreach (var reportKey in formatSpec.ReportKeys)
+ {
+ if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
+ continue;
+
+ packedObjects.Add(reportKey, new Value { Application = appMeta, PackedValue = valuePackObject });
+ }
+
+ return formatSpec.Formatter(packedObjects);
+ }
+
+ return FormattedValue.Unhandled;
+ }
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReport/Delegates.cs b/src/Ryujinx/Utilities/PlayReport/Delegates.cs
new file mode 100644
index 000000000..7c8952e18
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/Delegates.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ ///
+ /// The delegate type that powers single value formatters.
+ /// Takes in the result value from the Play Report, and outputs:
+ ///
+ /// a formatted string,
+ ///
+ /// a signal that nothing was available to handle it,
+ ///
+ /// OR a signal to reset the value that the caller is using the for.
+ ///
+ public delegate FormattedValue ValueFormatter(Value value);
+
+ ///
+ /// The delegate type that powers multiple value formatters.
+ /// Takes in the result values from the Play Report, and outputs:
+ ///
+ /// a formatted string,
+ ///
+ /// a signal that nothing was available to handle it,
+ ///
+ /// OR a signal to reset the value that the caller is using the for.
+ ///
+ public delegate FormattedValue MultiValueFormatter(Value[] value);
+
+ ///
+ /// The delegate type that powers multiple value formatters.
+ /// The dictionary passed to this delegate is sparsely populated;
+ /// that is, not every key specified in the Play Report needs to match for this to be used.
+ /// Takes in the result values from the Play Report, and outputs:
+ ///
+ /// a formatted string,
+ ///
+ /// a signal that nothing was available to handle it,
+ ///
+ /// OR a signal to reset the value that the caller is using the for.
+ ///
+ public delegate FormattedValue SparseMultiValueFormatter(Dictionary values);
+}
diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs
new file mode 100644
index 000000000..ae954c81c
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs
@@ -0,0 +1,127 @@
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ public static class PlayReports
+ {
+ public static Analyzer Analyzer { get; } = new Analyzer()
+ .AddSpec(
+ "01007ef00011e000",
+ spec => spec
+ .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
+ // reset to normal status when switching between normal & master mode in title screen
+ .AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
+ )
+ .AddSpec(
+ "0100f2c0115b6000",
+ spec => spec
+ .AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
+ .AddSpec(
+ "0100000000010000",
+ spec =>
+ spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
+ )
+ .AddSpec(
+ "010075000ecbe000",
+ spec =>
+ spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
+ )
+ .AddSpec(
+ "010028600ebda000",
+ spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
+ )
+ .AddSpec( // Global & China IDs
+ ["0100152000022000", "010075100e8ec000"],
+ spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
+ )
+ .AddSpec(
+ ["0100a3d008c5c000", "01008f6008c5e000"],
+ spec => spec
+ .AddValueFormatter("area_no", PokemonSVArea)
+ .AddValueFormatter("team_circle", PokemonSVUnionCircle)
+ );
+
+ private static FormattedValue BreathOfTheWild_MasterMode(Value value)
+ => value.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset;
+
+ private static FormattedValue TearsOfTheKingdom_CurrentField(Value value) =>
+ value.DoubleValue switch
+ {
+ > 800d => "Exploring the Sky Islands",
+ < -201d => "Exploring the Depths",
+ _ => "Roaming Hyrule"
+ };
+
+ private static FormattedValue SuperMarioOdyssey_AssistMode(Value value)
+ => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
+
+ private static FormattedValue SuperMarioOdysseyChina_AssistMode(Value value)
+ => value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
+
+ private static FormattedValue SuperMario3DWorldOrBowsersFury(Value value)
+ => value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
+
+ private static FormattedValue MarioKart8Deluxe_Mode(Value value)
+ => value.StringValue switch
+ {
+ // Single Player
+ "Single" => "Single Player",
+ // Multiplayer
+ "Multi-2players" => "Multiplayer 2 Players",
+ "Multi-3players" => "Multiplayer 3 Players",
+ "Multi-4players" => "Multiplayer 4 Players",
+ // Wireless/LAN Play
+ "Local-Single" => "Wireless/LAN Play",
+ "Local-2players" => "Wireless/LAN Play 2 Players",
+ // CC Classes
+ "50cc" => "50cc",
+ "100cc" => "100cc",
+ "150cc" => "150cc",
+ "Mirror" => "Mirror (150cc)",
+ "200cc" => "200cc",
+ // Modes
+ "GrandPrix" => "Grand Prix",
+ "TimeAttack" => "Time Trials",
+ "VS" => "VS Races",
+ "Battle" => "Battle Mode",
+ "RaceStart" => "Selecting a Course",
+ "Race" => "Racing",
+ _ => FormattedValue.ForceReset
+ };
+
+ private static FormattedValue PokemonSVUnionCircle(Value value)
+ => value.BoxedValue is 0 ? "Playing Alone" : "Playing in a group";
+
+ private static FormattedValue PokemonSVArea(Value value)
+ => value.StringValue switch
+ {
+ // Base Game Locations
+ "a_w01" => "South Area One",
+ "a_w02" => "Mesagoza",
+ "a_w03" => "The Pokemon League",
+ "a_w04" => "South Area Two",
+ "a_w05" => "South Area Four",
+ "a_w06" => "South Area Six",
+ "a_w07" => "South Area Five",
+ "a_w08" => "South Area Three",
+ "a_w09" => "West Area One",
+ "a_w10" => "Asado Desert",
+ "a_w11" => "West Area Two",
+ "a_w12" => "Medali",
+ "a_w13" => "Tagtree Thicket",
+ "a_w14" => "East Area Three",
+ "a_w15" => "Artazon",
+ "a_w16" => "East Area Two",
+ "a_w18" => "Casseroya Lake",
+ "a_w19" => "Glaseado Mountain",
+ "a_w20" => "North Area Three",
+ "a_w21" => "North Area One",
+ "a_w22" => "North Area Two",
+ "a_w23" => "The Great Crater of Paldea",
+ "a_w24" => "South Paldean Sea",
+ "a_w25" => "West Paldean Sea",
+ "a_w26" => "East Paldean Sea",
+ "a_w27" => "Nouth Paldean Sea",
+ //TODO DLC Locations
+ _ => FormattedValue.ForceReset
+ };
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs
new file mode 100644
index 000000000..649813b7a
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs
@@ -0,0 +1,140 @@
+using FluentAvalonia.Core;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ ///
+ /// A mapping of title IDs to value formatter specs.
+ ///
+ /// Generally speaking, use the .AddSpec(...) methods instead of creating this class yourself.
+ ///
+ public class GameSpec
+ {
+ public required string[] TitleIds { get; init; }
+ public List SimpleValueFormatters { get; } = [];
+ public List MultiValueFormatters { get; } = [];
+ public List SparseMultiValueFormatters { get; } = [];
+
+
+ ///
+ /// Add a value formatter to the current
+ /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The key name to match.
+ /// The function which can return a potential formatted value.
+ /// The current , for chaining convenience.
+ public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
+ => AddValueFormatter(SimpleValueFormatters.Count, reportKey, valueFormatter);
+
+ ///
+ /// Add a value formatter at a specific priority to the current
+ /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The resolution priority of this value formatter. Higher resolves sooner.
+ /// The key name to match.
+ /// The function which can return a potential formatted value.
+ /// The current , for chaining convenience.
+ public GameSpec AddValueFormatter(int priority, string reportKey,
+ ValueFormatter valueFormatter)
+ {
+ SimpleValueFormatters.Add(new FormatterSpec
+ {
+ Priority = priority, ReportKey = reportKey, Formatter = valueFormatter
+ });
+ return this;
+ }
+
+ ///
+ /// Add a multi-value formatter to the current
+ /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The key names to match.
+ /// The function which can format the values.
+ /// The current , for chaining convenience.
+ public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter)
+ => AddMultiValueFormatter(MultiValueFormatters.Count, reportKeys, valueFormatter);
+
+ ///
+ /// Add a multi-value formatter at a specific priority to the current
+ /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The resolution priority of this value formatter. Higher resolves sooner.
+ /// The key names to match.
+ /// The function which can format the values.
+ /// The current , for chaining convenience.
+ public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
+ MultiValueFormatter valueFormatter)
+ {
+ MultiValueFormatters.Add(new MultiFormatterSpec
+ {
+ Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
+ });
+ return this;
+ }
+
+ ///
+ /// Add a multi-value formatter to the current
+ /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The 'Sparse' multi-value formatters do not require every key to be present.
+ /// If you need this requirement, use .
+ ///
+ /// The key names to match.
+ /// The function which can format the values.
+ /// The current , for chaining convenience.
+ public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter)
+ => AddSparseMultiValueFormatter(SparseMultiValueFormatters.Count, reportKeys, valueFormatter);
+
+ ///
+ /// Add a multi-value formatter at a specific priority to the current
+ /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The 'Sparse' multi-value formatters do not require every key to be present.
+ /// If you need this requirement, use .
+ ///
+ /// The resolution priority of this value formatter. Higher resolves sooner.
+ /// The key names to match.
+ /// The function which can format the values.
+ /// The current , for chaining convenience.
+ public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys,
+ SparseMultiValueFormatter valueFormatter)
+ {
+ SparseMultiValueFormatters.Add(new SparseMultiFormatterSpec
+ {
+ Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
+ });
+ return this;
+ }
+ }
+
+ ///
+ /// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
+ ///
+ public struct FormatterSpec
+ {
+ public required int Priority { get; init; }
+ public required string ReportKey { get; init; }
+ public ValueFormatter Formatter { get; init; }
+ }
+
+ ///
+ /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
+ ///
+ public struct MultiFormatterSpec
+ {
+ public required int Priority { get; init; }
+ public required string[] ReportKeys { get; init; }
+ public MultiValueFormatter Formatter { get; init; }
+ }
+
+ ///
+ /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their sparsely populated potential values.
+ ///
+ public struct SparseMultiFormatterSpec
+ {
+ public required int Priority { get; init; }
+ public required string[] ReportKeys { get; init; }
+ public SparseMultiValueFormatter Formatter { get; init; }
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReport/Value.cs b/src/Ryujinx/Utilities/PlayReport/Value.cs
new file mode 100644
index 000000000..46d47366d
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/Value.cs
@@ -0,0 +1,130 @@
+using MsgPack;
+using Ryujinx.Ava.Utilities.AppLibrary;
+using System;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+ ///
+ /// The input data to a ,
+ /// containing the currently running application's ,
+ /// and the matched from the Play Report.
+ ///
+ public class Value
+ {
+ ///
+ /// The currently running application's .
+ ///
+ public ApplicationMetadata Application { get; init; }
+
+ ///
+ /// The matched value from the Play Report.
+ ///
+ public MessagePackObject PackedValue { get; init; }
+
+ ///
+ /// Access the as its underlying .NET type.
+ ///
+ /// Does not seem to work well with comparing numeric types,
+ /// so use XValue properties for that.
+ ///
+ public object BoxedValue => PackedValue.ToObject();
+
+ public override string ToString()
+ {
+ object boxed = BoxedValue;
+ return boxed == null
+ ? "null"
+ : boxed.ToString();
+ }
+
+ #region AsX accessors
+
+ public bool BooleanValue => PackedValue.AsBoolean();
+ public byte ByteValue => PackedValue.AsByte();
+ public sbyte SByteValue => PackedValue.AsSByte();
+ public short ShortValue => PackedValue.AsInt16();
+ public ushort UShortValue => PackedValue.AsUInt16();
+ public int IntValue => PackedValue.AsInt32();
+ public uint UIntValue => PackedValue.AsUInt32();
+ public long LongValue => PackedValue.AsInt64();
+ public ulong ULongValue => PackedValue.AsUInt64();
+ public float FloatValue => PackedValue.AsSingle();
+ public double DoubleValue => PackedValue.AsDouble();
+ public string StringValue => PackedValue.AsString();
+ public Span BinaryValue => PackedValue.AsBinary();
+
+ #endregion
+ }
+
+ ///
+ /// A potential formatted value returned by a .
+ ///
+ public readonly struct FormattedValue
+ {
+ ///
+ /// Was any handler able to match anything in the Play Report?
+ ///
+ public bool Handled { get; private init; }
+
+ ///
+ /// Did the handler request the caller of the to reset the existing value?
+ ///
+ public bool Reset { get; private init; }
+
+ ///
+ /// The formatted value, only present if is true, and is false.
+ ///
+ public string FormattedString { get; private init; }
+
+ ///
+ /// The intended path of execution for having a string to return: simply return the string.
+ /// This implicit conversion will make the struct for you.
+ ///
+ /// If the input is null, is returned.
+ ///
+ /// The formatted string value.
+ /// The automatically constructed struct.
+ public static implicit operator FormattedValue(string formattedValue)
+ => formattedValue is not null
+ ? new FormattedValue { Handled = true, FormattedString = formattedValue }
+ : Unhandled;
+
+ public override string ToString()
+ {
+ if (!Handled)
+ return "";
+
+ if (Reset)
+ return "";
+
+ return FormattedString;
+ }
+
+ ///
+ /// Return this to tell the caller there is no value to return.
+ ///
+ public static FormattedValue Unhandled => default;
+
+ ///
+ /// Return this to suggest the caller reset the value it's using the for.
+ ///
+ public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
+
+ ///
+ /// A delegate singleton you can use to always return in a .
+ ///
+ public static readonly ValueFormatter SingleAlwaysResets = _ => ForceReset;
+
+ ///
+ /// A delegate singleton you can use to always return in a .
+ ///
+ public static readonly MultiValueFormatter MultiAlwaysResets = _ => ForceReset;
+
+ ///
+ /// A delegate factory you can use to always return the specified
+ /// in a .
+ ///
+ /// The string to always return for this delegate instance.
+ public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
+ }
+}
diff --git a/src/Ryujinx/Utilities/PlayReportAnalyzer.cs b/src/Ryujinx/Utilities/PlayReportAnalyzer.cs
deleted file mode 100644
index 47c36a396..000000000
--- a/src/Ryujinx/Utilities/PlayReportAnalyzer.cs
+++ /dev/null
@@ -1,282 +0,0 @@
-using Gommon;
-using MsgPack;
-using Ryujinx.Ava.Utilities.AppLibrary;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-
-namespace Ryujinx.Ava.Utilities
-{
- ///
- /// The entrypoint for the Play Report analysis system.
- ///
- public class PlayReportAnalyzer
- {
- private readonly List _specs = [];
-
- ///
- /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
- ///
- /// The ID of the game to listen to Play Reports in.
- /// The configuration function for the analysis spec.
- /// The current , for chaining convenience.
- public PlayReportAnalyzer AddSpec(string titleId, Func transform)
- {
- Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
- $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
-
- _specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
- return this;
- }
-
- ///
- /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
- ///
- /// The ID of the game to listen to Play Reports in.
- /// The configuration function for the analysis spec.
- /// The current , for chaining convenience.
- public PlayReportAnalyzer AddSpec(string titleId, Action transform)
- {
- Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
- $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
-
- _specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
- return this;
- }
-
- ///
- /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
- ///
- /// The IDs of the games to listen to Play Reports in.
- /// The configuration function for the analysis spec.
- /// The current , for chaining convenience.
- public PlayReportAnalyzer AddSpec(IEnumerable titleIds,
- Func transform)
- {
- string[] tids = titleIds.ToArray();
- Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
- $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
-
- _specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] }));
- return this;
- }
-
- ///
- /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
- ///
- /// The IDs of the games to listen to Play Reports in.
- /// The configuration function for the analysis spec.
- /// The current , for chaining convenience.
- public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Action transform)
- {
- string[] tids = titleIds.ToArray();
- Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
- $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
-
- _specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform));
- return this;
- }
-
-
- ///
- /// Runs the configured for the specified game title ID.
- ///
- /// The game currently running.
- /// The Application metadata information, including localized game name and play time information.
- /// The Play Report received from HLE.
- /// A struct representing a possible formatted value.
- public FormattedValue Format(
- string runningGameId,
- ApplicationMetadata appMeta,
- MessagePackObject playReport
- )
- {
- if (!playReport.IsDictionary)
- return FormattedValue.Unhandled;
-
- if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
- return FormattedValue.Unhandled;
-
- foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
- {
- if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
- continue;
-
- return formatSpec.ValueFormatter(new PlayReportValue
- {
- Application = appMeta, PackedValue = valuePackObject
- });
- }
-
- return FormattedValue.Unhandled;
- }
-
- ///
- /// A potential formatted value returned by a .
- ///
- public readonly struct FormattedValue
- {
- ///
- /// Was any handler able to match anything in the Play Report?
- ///
- public bool Handled { get; private init; }
-
- ///
- /// Did the handler request the caller of the to reset the existing value?
- ///
- public bool Reset { get; private init; }
-
- ///
- /// The formatted value, only present if is true, and is false.
- ///
- public string FormattedString { get; private init; }
-
- ///
- /// The intended path of execution for having a string to return: simply return the string.
- /// This implicit conversion will make the struct for you.
- ///
- /// If the input is null, is returned.
- ///
- /// The formatted string value.
- /// The automatically constructed struct.
- public static implicit operator FormattedValue(string formattedValue)
- => formattedValue is not null
- ? new FormattedValue { Handled = true, FormattedString = formattedValue }
- : Unhandled;
-
- ///
- /// Return this to tell the caller there is no value to return.
- ///
- public static FormattedValue Unhandled => default;
-
- ///
- /// Return this to suggest the caller reset the value it's using the for.
- ///
- public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
-
- ///
- /// A delegate singleton you can use to always return in a .
- ///
- public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset;
-
- ///
- /// A delegate factory you can use to always return the specified
- /// in a .
- ///
- /// The string to always return for this delegate instance.
- public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
- }
- }
-
- ///
- /// A mapping of title IDs to value formatter specs.
- ///
- /// Generally speaking, use the .AddSpec(...) methods instead of creating this class yourself.
- ///
- public class PlayReportGameSpec
- {
- public required string[] TitleIds { get; init; }
- public List SimpleValueFormatters { get; } = [];
-
- ///
- /// Add a value formatter to the current
- /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
- ///
- /// The key name to match.
- /// The function which can return a potential formatted value.
- /// The current , for chaining convenience.
- public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
- {
- SimpleValueFormatters.Add(new FormatterSpec
- {
- Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter
- });
- return this;
- }
-
- ///
- /// Add a value formatter at a specific priority to the current
- /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
- ///
- /// The resolution priority of this value formatter. Higher resolves sooner.
- /// The key name to match.
- /// The function which can return a potential formatted value.
- /// The current , for chaining convenience.
- public PlayReportGameSpec AddValueFormatter(int priority, string reportKey,
- PlayReportValueFormatter valueFormatter)
- {
- SimpleValueFormatters.Add(new FormatterSpec
- {
- Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
- });
- return this;
- }
-
- ///
- /// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
- ///
- public struct FormatterSpec
- {
- public required int Priority { get; init; }
- public required string ReportKey { get; init; }
- public PlayReportValueFormatter ValueFormatter { get; init; }
- }
- }
-
- ///
- /// The input data to a ,
- /// containing the currently running application's ,
- /// and the matched from the Play Report.
- ///
- public class PlayReportValue
- {
- ///
- /// The currently running application's .
- ///
- public ApplicationMetadata Application { get; init; }
-
- ///
- /// The matched value from the Play Report.
- ///
- public MessagePackObject PackedValue { get; init; }
-
- ///
- /// Access the as its underlying .NET type.
- ///
- /// Does not seem to work well with comparing numeric types,
- /// so use and the AsX (where X is a numerical type name i.e. Int32) methods for that.
- ///
- public object BoxedValue => PackedValue.ToObject();
-
- #region AsX accessors
-
- public bool BooleanValue => PackedValue.AsBoolean();
- public byte ByteValye => PackedValue.AsByte();
- public sbyte SByteValye => PackedValue.AsSByte();
- public short ShortValye => PackedValue.AsInt16();
- public ushort UShortValye => PackedValue.AsUInt16();
- public int IntValye => PackedValue.AsInt32();
- public uint UIntValye => PackedValue.AsUInt32();
- public long LongValye => PackedValue.AsInt64();
- public ulong ULongValye => PackedValue.AsUInt64();
- public float FloatValue => PackedValue.AsSingle();
- public double DoubleValue => PackedValue.AsDouble();
- public string StringValue => PackedValue.AsString();
- public Span BinaryValue => PackedValue.AsBinary();
-
- #endregion
- }
-
- ///
- /// The delegate type that powers the entire analysis system (as it currently is).
- /// Takes in the result value from the Play Report, and outputs:
- ///
- /// a formatted string,
- ///
- /// a signal that nothing was available to handle it,
- ///
- /// OR a signal to reset the value that the caller is using the for.
- ///
- public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
-}