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 0abfc3b11..2456c4f34 100644
--- a/src/Ryujinx/Assets/locales.json
+++ b/src/Ryujinx/Assets/locales.json
@@ -23698,4 +23698,4 @@
}
}
]
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs
index 20b296511..d95bb80dd 100644
--- a/src/Ryujinx/DiscordIntegrationModule.cs
+++ b/src/Ryujinx/DiscordIntegrationModule.cs
@@ -4,16 +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;
namespace Ryujinx.Ava
@@ -130,8 +126,8 @@ 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);
+ Analyzer.FormattedValue formattedValue =
+ PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
if (!formattedValue.Handled) return;
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/PlayReportAnalyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs
similarity index 59%
rename from src/Ryujinx/Utilities/PlayReportAnalyzer.cs
rename to src/Ryujinx/Utilities/PlayReport/Analyzer.cs
index 47c36a396..84bdbf085 100644
--- a/src/Ryujinx/Utilities/PlayReportAnalyzer.cs
+++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs
@@ -6,27 +6,27 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-namespace Ryujinx.Ava.Utilities
+namespace Ryujinx.Ava.Utilities.PlayReport
{
///
/// The entrypoint for the Play Report analysis system.
///
- public class PlayReportAnalyzer
+ public class Analyzer
{
- private readonly List _specs = [];
+ 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)
+ /// 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(PlayReportGameSpec)}.");
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
- _specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
+ _specs.Add(transform(new GameSpec { TitleIds = [titleId] }));
return this;
}
@@ -35,13 +35,13 @@ namespace Ryujinx.Ava.Utilities
///
/// 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)
+ /// 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(PlayReportGameSpec)}.");
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
- _specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
+ _specs.Add(new GameSpec { TitleIds = [titleId] }.Apply(transform));
return this;
}
@@ -50,15 +50,15 @@ namespace Ryujinx.Ava.Utilities
///
/// 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)
+ /// 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(PlayReportGameSpec)}.");
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
- _specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] }));
+ _specs.Add(transform(new GameSpec { TitleIds = [..tids] }));
return this;
}
@@ -67,20 +67,20 @@ namespace Ryujinx.Ava.Utilities
///
/// 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)
+ /// 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(PlayReportGameSpec)}.");
+ $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
- _specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform));
+ _specs.Add(new GameSpec { TitleIds = [..tids] }.Apply(transform));
return this;
}
///
- /// Runs the configured for the specified game title ID.
+ /// 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.
@@ -95,25 +95,44 @@ namespace Ryujinx.Ava.Utilities
if (!playReport.IsDictionary)
return FormattedValue.Unhandled;
- if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
+ if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
return FormattedValue.Unhandled;
- foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
+ foreach (GameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
{
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
continue;
- return formatSpec.ValueFormatter(new PlayReportValue
+ return formatSpec.ValueFormatter(new Value
{
Application = appMeta, PackedValue = valuePackObject
});
}
+
+ foreach (GameSpec.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.ValueFormatter(packedObjects
+ .Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
+ .ToArray());
+ }
return FormattedValue.Unhandled;
}
///
- /// A potential formatted value returned by a .
+ /// A potential formatted value returned by a .
///
public readonly struct FormattedValue
{
@@ -123,7 +142,7 @@ namespace Ryujinx.Ava.Utilities
public bool Handled { get; private init; }
///
- /// Did the handler request the caller of the to reset the existing value?
+ /// Did the handler request the caller of the to reset the existing value?
///
public bool Reset { get; private init; }
@@ -151,42 +170,43 @@ namespace Ryujinx.Ava.Utilities
public static FormattedValue Unhandled => default;
///
- /// Return this to suggest the caller reset the value it's using the for.
+ /// 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 .
+ /// A delegate singleton you can use to always return in a .
///
- public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset;
+ public static readonly ValueFormatter AlwaysResets = _ => ForceReset;
///
/// A delegate factory you can use to always return the specified
- /// in a .
+ /// in a .
///
/// The string to always return for this delegate instance.
- public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
+ public static ValueFormatter 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.
+ /// Generally speaking, use the .AddSpec(...) methods instead of creating this class yourself.
///
- public class PlayReportGameSpec
+ public class GameSpec
{
public required string[] TitleIds { get; init; }
public List SimpleValueFormatters { get; } = [];
+ public List MultiValueFormatters { get; } = [];
///
- /// Add a value formatter to the current
+ /// 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)
+ /// The current , for chaining convenience.
+ public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
{
SimpleValueFormatters.Add(new FormatterSpec
{
@@ -196,15 +216,15 @@ namespace Ryujinx.Ava.Utilities
}
///
- /// Add a value formatter at a specific priority to the current
+ /// 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)
+ /// The current , for chaining convenience.
+ public GameSpec AddValueFormatter(int priority, string reportKey,
+ ValueFormatter valueFormatter)
{
SimpleValueFormatters.Add(new FormatterSpec
{
@@ -212,6 +232,40 @@ namespace Ryujinx.Ava.Utilities
});
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)
+ {
+ MultiValueFormatters.Add(new MultiFormatterSpec
+ {
+ Priority = SimpleValueFormatters.Count, ReportKeys = reportKeys, ValueFormatter = valueFormatter
+ });
+ return this;
+ }
+
+ ///
+ /// 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, 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.
@@ -220,16 +274,26 @@ namespace Ryujinx.Ava.Utilities
{
public required int Priority { get; init; }
public required string ReportKey { get; init; }
- public PlayReportValueFormatter ValueFormatter { get; init; }
+ public ValueFormatter ValueFormatter { 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 ValueFormatter { get; init; }
}
}
///
- /// The input data to a ,
+ /// The input data to a ,
/// containing the currently running application's ,
/// and the matched from the Play Report.
///
- public class PlayReportValue
+ public class Value
{
///
/// The currently running application's .
@@ -245,7 +309,7 @@ namespace Ryujinx.Ava.Utilities
/// 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.
+ /// so use XValue properties for that.
///
public object BoxedValue => PackedValue.ToObject();
@@ -269,14 +333,26 @@ namespace Ryujinx.Ava.Utilities
}
///
- /// The delegate type that powers the entire analysis system (as it currently is).
+ /// 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.
+ /// OR a signal to reset the value that the caller is using the for.
///
- public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
+ public delegate Analyzer.FormattedValue ValueFormatter(Value value);
+
+ ///
+ /// The delegate type that powers multiple 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 Analyzer.FormattedValue MultiValueFormatter(Value[] value);
}
diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs
new file mode 100644
index 000000000..25457744e
--- /dev/null
+++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs
@@ -0,0 +1,129 @@
+using static Ryujinx.Ava.Utilities.PlayReport.Analyzer;
+
+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.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)
+ )
+ .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
+ };
+ }
+}