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 + }; + } +}