From 54b233dd7871c1b1483e49de7a5c638c90d75e5e Mon Sep 17 00:00:00 2001 From: Daenorth Date: Thu, 6 Feb 2025 11:46:23 +0100 Subject: [PATCH 01/89] Updated the compat list. (#618) --- docs/compatibility.csv | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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 From a4211fec33afd135958238f903484c024a39211a Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Thu, 6 Feb 2025 22:56:25 -0600 Subject: [PATCH 02/89] UI: Properly space the play time & last play date in the game info popup --- src/Ryujinx/Assets/locales.json | 72 +++++++++---------- .../UI/Controls/ApplicationDataView.axaml | 45 +++++++----- .../UI/ViewModels/ApplicationDataViewModel.cs | 3 - 3 files changed, 65 insertions(+), 55 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 5afb46c13..f30aac3bc 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": "最近遊玩:" } }, { diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml index 45ae75639..a0b6ad7b3 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml @@ -92,22 +92,35 @@ TextAlignment="Start" TextWrapping="Wrap" /> - - - + + + + + + + + + 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 => From 2c8edaf89e29b33c5f3021269b58345f47cec24f Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Fri, 7 Feb 2025 15:43:50 -0600 Subject: [PATCH 03/89] PlayReport: Add Sparse Multi Value formatters --- src/Ryujinx/DiscordIntegrationModule.cs | 10 +- src/Ryujinx/Utilities/PlayReport/Analyzer.cs | 259 ++---------------- src/Ryujinx/Utilities/PlayReport/Delegates.cs | 42 +++ .../Utilities/PlayReport/PlayReports.cs | 6 +- src/Ryujinx/Utilities/PlayReport/Specs.cs | 140 ++++++++++ src/Ryujinx/Utilities/PlayReport/Value.cs | 130 +++++++++ 6 files changed, 343 insertions(+), 244 deletions(-) create mode 100644 src/Ryujinx/Utilities/PlayReport/Delegates.cs create mode 100644 src/Ryujinx/Utilities/PlayReport/Specs.cs create mode 100644 src/Ryujinx/Utilities/PlayReport/Value.cs diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index d95bb80dd..abdd9fed1 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -126,14 +126,16 @@ namespace Ryujinx.Ava if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - Analyzer.FormattedValue formattedValue = + 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/Utilities/PlayReport/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs index 84bdbf085..390e06d28 100644 --- a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs +++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs @@ -78,7 +78,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport return this; } - + /// /// Runs the configured for the specified game title ID. /// @@ -98,261 +98,48 @@ namespace Ryujinx.Ava.Utilities.PlayReport if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec)) return FormattedValue.Unhandled; - foreach (GameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) + foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) { if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) continue; - return formatSpec.ValueFormatter(new Value - { - Application = appMeta, PackedValue = valuePackObject - }); + return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject }); } - - foreach (GameSpec.MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority)) + + 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.ValueFormatter(packedObjects + + 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; } - - /// - /// 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 ValueFormatter 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 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. - /// - public class GameSpec - { - public required string[] TitleIds { get; init; } - public List SimpleValueFormatters { get; } = []; - public List MultiValueFormatters { 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) - { - 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 GameSpec AddValueFormatter(int priority, string reportKey, - ValueFormatter valueFormatter) - { - SimpleValueFormatters.Add(new FormatterSpec - { - Priority = priority, ReportKey = reportKey, ValueFormatter = 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) - { - 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. - /// - public struct FormatterSpec - { - public required int Priority { get; init; } - public required string ReportKey { 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 , - /// 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(); - - #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 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 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/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 index 25457744e..ae954c81c 100644 --- a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs @@ -1,6 +1,4 @@ -using static Ryujinx.Ava.Utilities.PlayReport.Analyzer; - -namespace Ryujinx.Ava.Utilities.PlayReport +namespace Ryujinx.Ava.Utilities.PlayReport { public static class PlayReports { @@ -10,7 +8,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport spec => spec .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) // reset to normal status when switching between normal & master mode in title screen - .AddValueFormatter("AoCVer", FormattedValue.AlwaysResets) + .AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets) ) .AddSpec( "0100f2c0115b6000", 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; + } +} From 5085af0050fde71db9fb0fc6d886e38ffdb60e25 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Fri, 7 Feb 2025 18:28:32 -0600 Subject: [PATCH 04/89] UI: Changed the color of "Ingame" from yellow to orange to stand out better in light mode --- src/Ryujinx/UI/Helpers/Converters/PlayabilityStatusConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }; From 4e8157688eb08ab651f641dc40723188cffdf820 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Fri, 7 Feb 2025 18:34:11 -0600 Subject: [PATCH 05/89] UI: See what games do/don't have an image & dynamic RPC support in the Game Info popup --- src/Ryujinx/Assets/locales.json | 50 +++++++++++++++++++ src/Ryujinx/DiscordIntegrationModule.cs | 4 ++ .../UI/Controls/ApplicationDataView.axaml | 44 ++++++++++++++++ .../Utilities/AppLibrary/ApplicationData.cs | 3 ++ src/Ryujinx/Utilities/PlayReport/Analyzer.cs | 5 ++ 5 files changed, 106 insertions(+) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index f30aac3bc..25a40b29e 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -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 abdd9fed1..229b6ee09 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -10,6 +10,7 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE; using Ryujinx.HLE.Loaders.Processes; using Ryujinx.Horizon; +using System.Linq; using System.Text; namespace Ryujinx.Ava @@ -37,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 diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml index a0b6ad7b3..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 @@ + + + + + + + + + + + + + + + + + + + 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/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs index 390e06d28..338c198a1 100644 --- a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs +++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs @@ -3,6 +3,7 @@ using MsgPack; using Ryujinx.Ava.Utilities.AppLibrary; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; @@ -15,6 +16,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport { 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. /// From 1d88771d1b2e90d78a938e5235d13b3a157dad5f Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 8 Feb 2025 00:22:34 -0600 Subject: [PATCH 06/89] Play Report Analyzer v4 You can now access the *entire* play report data in any given value formatter. The input types have been restructured and, notably, not every instance of Value has an ApplicationMetadata on it. It's now on the container type that also contains the matched values and the entire play report. --- src/Ryujinx.Horizon/HorizonStatic.cs | 5 +- src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs | 24 ++--- src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs | 24 +++++ src/Ryujinx/DiscordIntegrationModule.cs | 3 +- src/Ryujinx/Utilities/PlayReport/Analyzer.cs | 37 +++++--- src/Ryujinx/Utilities/PlayReport/Delegates.cs | 10 +-- .../Utilities/PlayReport/MatchedValues.cs | 87 +++++++++++++++++++ .../Utilities/PlayReport/PlayReports.cs | 32 +++---- src/Ryujinx/Utilities/PlayReport/Value.cs | 23 +++-- 9 files changed, 192 insertions(+), 53 deletions(-) create mode 100644 src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs create mode 100644 src/Ryujinx/Utilities/PlayReport/MatchedValues.cs diff --git a/src/Ryujinx.Horizon/HorizonStatic.cs b/src/Ryujinx.Horizon/HorizonStatic.cs index 15689f0c8..eb9dd4e31 100644 --- a/src/Ryujinx.Horizon/HorizonStatic.cs +++ b/src/Ryujinx.Horizon/HorizonStatic.cs @@ -1,5 +1,6 @@ using MsgPack; using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Prepo.Types; using Ryujinx.Memory; using System; using System.Threading; @@ -8,7 +9,7 @@ namespace Ryujinx.Horizon { public static class HorizonStatic { - internal static void HandlePlayReport(MessagePackObject report) => + internal static void HandlePlayReport(PlayReport report) => new Thread(() => PlayReport?.Invoke(report)) { Name = "HLE.PlayReportEvent", @@ -16,7 +17,7 @@ namespace Ryujinx.Horizon Priority = ThreadPriority.AboveNormal }.Start(); - public static event Action PlayReport; + public static event Action PlayReport; [field: ThreadStatic] public static HorizonOptions Options { get; private set; } diff --git a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs index 2f8657e0b..0ca851e6e 100644 --- a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs +++ b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs @@ -1,4 +1,3 @@ -using Gommon; using MsgPack; using MsgPack.Serialization; using Ryujinx.Common.Logging; @@ -12,19 +11,12 @@ using Ryujinx.Horizon.Sdk.Sf; using Ryujinx.Horizon.Sdk.Sf.Hipc; using System; using System.Text; -using System.Threading; using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId; namespace Ryujinx.Horizon.Prepo.Ipc { partial class PrepoService : IPrepoService { - enum PlayReportKind - { - Normal, - System, - } - private readonly ArpApi _arp; private readonly PrepoServicePermissionLevel _permissionLevel; private ulong _systemSessionId; @@ -196,10 +188,17 @@ namespace Ryujinx.Horizon.Prepo.Ipc { return PrepoResult.InvalidBufferSize; } - + StringBuilder builder = new(); MessagePackObject deserializedReport = MessagePackSerializer.UnpackMessagePackObject(reportBuffer.ToArray()); + PlayReport playReport = new() + { + Kind = playReportKind, + Room = gameRoom, + ReportData = deserializedReport + }; + builder.AppendLine(); builder.AppendLine("PlayReport log:"); builder.AppendLine($" Kind: {playReportKind}"); @@ -209,10 +208,12 @@ namespace Ryujinx.Horizon.Prepo.Ipc if (pid != 0) { builder.AppendLine($" Pid: {pid}"); + playReport.Pid = pid; } else { builder.AppendLine($" ApplicationId: {applicationId}"); + playReport.AppId = applicationId; } Result result = _arp.GetApplicationInstanceId(out ulong applicationInstanceId, pid); @@ -223,17 +224,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc _arp.GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, applicationInstanceId).AbortOnFailure(); + playReport.Version = applicationLaunchProperty.Version; + builder.AppendLine($" ApplicationVersion: {applicationLaunchProperty.Version}"); if (!userId.IsNull) { builder.AppendLine($" UserId: {userId}"); + playReport.UserId = userId; } builder.AppendLine($" Room: {gameRoom}"); builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}"); - HorizonStatic.HandlePlayReport(deserializedReport); + HorizonStatic.HandlePlayReport(playReport); Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString()); diff --git a/src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs b/src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs new file mode 100644 index 000000000..e896219d5 --- /dev/null +++ b/src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs @@ -0,0 +1,24 @@ +using MsgPack; +using Ryujinx.Horizon.Sdk.Account; +using Ryujinx.Horizon.Sdk.Ncm; + +namespace Ryujinx.Horizon.Prepo.Types +{ + public struct PlayReport + { + public PlayReportKind Kind { get; init; } + public string Room { get; init; } + public MessagePackObject ReportData { get; init; } + + public ApplicationId? AppId; + public ulong? Pid; + public uint Version; + public Uid? UserId; + } + + public enum PlayReportKind + { + Normal, + System, + } +} diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 229b6ee09..1f820a223 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -10,6 +10,7 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE; using Ryujinx.HLE.Loaders.Processes; using Ryujinx.Horizon; +using Ryujinx.Horizon.Prepo.Types; using System.Linq; using System.Text; @@ -124,7 +125,7 @@ namespace Ryujinx.Ava _currentApp = null; } - private static void HandlePlayReport(MessagePackObject playReport) + private static void HandlePlayReport(PlayReport playReport) { if (_discordClient is null) return; if (!TitleIDs.CurrentApplication.Value.HasValue) return; diff --git a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs index 338c198a1..0b4130da5 100644 --- a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs +++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs @@ -85,7 +85,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// - /// 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. @@ -94,10 +94,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport public FormattedValue Format( string runningGameId, ApplicationMetadata appMeta, - MessagePackObject playReport + Horizon.Prepo.Types.PlayReport playReport ) { - if (!playReport.IsDictionary) + if (!playReport.ReportData.IsDictionary) return FormattedValue.Unhandled; if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec)) @@ -105,10 +105,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) { - if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) + if (!playReport.ReportData.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) continue; - return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject }); + return formatSpec.Formatter(new SingleValue(valuePackObject) + { + Application = appMeta, + PlayReport = playReport + }); } foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority)) @@ -116,7 +120,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport List packedObjects = []; foreach (var reportKey in formatSpec.ReportKeys) { - if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) continue; packedObjects.Add(valuePackObject); @@ -125,23 +129,30 @@ namespace Ryujinx.Ava.Utilities.PlayReport if (packedObjects.Count != formatSpec.ReportKeys.Length) return FormattedValue.Unhandled; - return formatSpec.Formatter(packedObjects - .Select(packObject => new Value { Application = appMeta, PackedValue = packObject }) - .ToArray()); + return formatSpec.Formatter(new MultiValue(packedObjects) + { + Application = appMeta, + PlayReport = playReport + }); } foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority)) { - Dictionary packedObjects = []; + Dictionary packedObjects = []; foreach (var reportKey in formatSpec.ReportKeys) { - if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) continue; - packedObjects.Add(reportKey, new Value { Application = appMeta, PackedValue = valuePackObject }); + packedObjects.Add(reportKey, valuePackObject); } - return formatSpec.Formatter(packedObjects); + return formatSpec.Formatter( + new SparseMultiValue(packedObjects) + { + Application = appMeta, + PlayReport = playReport + }); } return FormattedValue.Unhandled; diff --git a/src/Ryujinx/Utilities/PlayReport/Delegates.cs b/src/Ryujinx/Utilities/PlayReport/Delegates.cs index 7c8952e18..789d408d7 100644 --- a/src/Ryujinx/Utilities/PlayReport/Delegates.cs +++ b/src/Ryujinx/Utilities/PlayReport/Delegates.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Ryujinx.Ava.Utilities.PlayReport +namespace Ryujinx.Ava.Utilities.PlayReport { /// /// The delegate type that powers single value formatters.
@@ -12,7 +10,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport ///
/// OR a signal to reset the value that the caller is using the for. ///
- public delegate FormattedValue ValueFormatter(Value value); + public delegate FormattedValue ValueFormatter(SingleValue value); /// /// The delegate type that powers multiple value formatters.
@@ -24,7 +22,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport ///
/// OR a signal to reset the value that the caller is using the for. ///
- public delegate FormattedValue MultiValueFormatter(Value[] value); + public delegate FormattedValue MultiValueFormatter(MultiValue value); /// /// The delegate type that powers multiple value formatters. @@ -38,5 +36,5 @@ namespace Ryujinx.Ava.Utilities.PlayReport ///
/// OR a signal to reset the value that the caller is using the for. ///
- public delegate FormattedValue SparseMultiValueFormatter(Dictionary values); + public delegate FormattedValue SparseMultiValueFormatter(SparseMultiValue value); } diff --git a/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs b/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs new file mode 100644 index 000000000..01c404c31 --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs @@ -0,0 +1,87 @@ +using MsgPack; +using Ryujinx.Ava.Utilities.AppLibrary; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Ava.Utilities.PlayReport +{ + public abstract class MatchedValue + { + public MatchedValue(T matched) + { + Matched = matched; + } + + /// + /// The currently running application's . + /// + public ApplicationMetadata Application { get; init; } + + /// + /// The entire play report. + /// + public Horizon.Prepo.Types.PlayReport PlayReport { get; init; } + + /// + /// The matched value from the Play Report. + /// + public T Matched { get; init; } + } + + /// + /// The input data to a , + /// containing the currently running application's , + /// and the matched from the Play Report. + /// + public class SingleValue : MatchedValue + { + public SingleValue(Value matched) : base(matched) + { + } + + public static implicit operator SingleValue(MessagePackObject mpo) => new(mpo); + } + + /// + /// The input data to a , + /// containing the currently running application's , + /// and the matched s from the Play Report. + /// + public class MultiValue : MatchedValue + { + public MultiValue(Value[] matched) : base(matched) + { + } + + public MultiValue(IEnumerable matched) : base(Value.ConvertPackedObjects(matched)) + { + } + + public static implicit operator MultiValue(List matched) + => new(matched.Select(x => new Value(x)).ToArray()); + } + + /// + /// The input data to a , + /// containing the currently running application's , + /// and the matched s from the Play Report. + /// + public class SparseMultiValue : MatchedValue> + { + public SparseMultiValue(Dictionary matched) : base(matched) + { + } + + public SparseMultiValue(Dictionary matched) : base(Value.ConvertPackedObjectMap(matched)) + { + } + + public static implicit operator SparseMultiValue(Dictionary matched) + => new(matched + .ToDictionary( + x => x.Key, + x => new Value(x.Value) + ) + ); + } +} diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs index ae954c81c..9e22cd6d2 100644 --- a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs @@ -39,28 +39,28 @@ .AddValueFormatter("team_circle", PokemonSVUnionCircle) ); - private static FormattedValue BreathOfTheWild_MasterMode(Value value) - => value.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset; + private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value) + => value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset; - private static FormattedValue TearsOfTheKingdom_CurrentField(Value value) => - value.DoubleValue switch + private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) => + value.Matched.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 SuperMarioOdyssey_AssistMode(SingleValue value) + => value.Matched.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 SuperMarioOdysseyChina_AssistMode(SingleValue value) + => value.Matched.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 SuperMario3DWorldOrBowsersFury(SingleValue value) + => value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; - private static FormattedValue MarioKart8Deluxe_Mode(Value value) - => value.StringValue switch + private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value) + => value.Matched.StringValue switch { // Single Player "Single" => "Single Player", @@ -87,11 +87,11 @@ _ => FormattedValue.ForceReset }; - private static FormattedValue PokemonSVUnionCircle(Value value) - => value.BoxedValue is 0 ? "Playing Alone" : "Playing in a group"; + private static FormattedValue PokemonSVUnionCircle(SingleValue value) + => value.Matched.BoxedValue is 0 ? "Playing Alone" : "Playing in a group"; - private static FormattedValue PokemonSVArea(Value value) - => value.StringValue switch + private static FormattedValue PokemonSVArea(SingleValue value) + => value.Matched.StringValue switch { // Base Game Locations "a_w01" => "South Area One", diff --git a/src/Ryujinx/Utilities/PlayReport/Value.cs b/src/Ryujinx/Utilities/PlayReport/Value.cs index 46d47366d..65d662ea0 100644 --- a/src/Ryujinx/Utilities/PlayReport/Value.cs +++ b/src/Ryujinx/Utilities/PlayReport/Value.cs @@ -1,6 +1,8 @@ using MsgPack; using Ryujinx.Ava.Utilities.AppLibrary; using System; +using System.Collections.Generic; +using System.Linq; namespace Ryujinx.Ava.Utilities.PlayReport { @@ -9,12 +11,12 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// containing the currently running application's , /// and the matched from the Play Report. /// - public class Value + public readonly struct Value { - /// - /// The currently running application's . - /// - public ApplicationMetadata Application { get; init; } + public Value(MessagePackObject packedValue) + { + PackedValue = packedValue; + } /// /// The matched value from the Play Report. @@ -37,6 +39,17 @@ namespace Ryujinx.Ava.Utilities.PlayReport : boxed.ToString(); } + public static implicit operator Value(MessagePackObject matched) => new(matched); + + public static Value[] ConvertPackedObjects(IEnumerable packObjects) + => packObjects.Select(packObject => new Value(packObject)).ToArray(); + + public static Dictionary ConvertPackedObjectMap(Dictionary packObjects) + => packObjects.ToDictionary( + x => x.Key, + x => new Value(x.Value) + ); + #region AsX accessors public bool BooleanValue => PackedValue.AsBoolean(); From 30a534edcdc7d72882a291448ac2a889848eb18f Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 8 Feb 2025 01:26:05 -0600 Subject: [PATCH 07/89] misc: chore: [ci skip] generify Formatter Specs to be able to run formatters of different types at interleaving priorities --- src/Ryujinx/Utilities/PlayReport/Analyzer.cs | 50 +------- src/Ryujinx/Utilities/PlayReport/Specs.cs | 124 +++++++++++++++---- 2 files changed, 105 insertions(+), 69 deletions(-) diff --git a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs index 0b4130da5..668eb526c 100644 --- a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs +++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs @@ -103,56 +103,12 @@ namespace Ryujinx.Ava.Utilities.PlayReport if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec)) return FormattedValue.Unhandled; - foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) + foreach (FormatterSpecBase formatSpec in spec.ValueFormatters.OrderBy(x => x.Priority)) { - if (!playReport.ReportData.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) + if (!formatSpec.Format(appMeta, playReport, out FormattedValue value)) continue; - return formatSpec.Formatter(new SingleValue(valuePackObject) - { - Application = appMeta, - PlayReport = playReport - }); - } - - foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority)) - { - List packedObjects = []; - foreach (var reportKey in formatSpec.ReportKeys) - { - if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) - continue; - - packedObjects.Add(valuePackObject); - } - - if (packedObjects.Count != formatSpec.ReportKeys.Length) - return FormattedValue.Unhandled; - - return formatSpec.Formatter(new MultiValue(packedObjects) - { - Application = appMeta, - PlayReport = playReport - }); - } - - foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority)) - { - Dictionary packedObjects = []; - foreach (var reportKey in formatSpec.ReportKeys) - { - if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) - continue; - - packedObjects.Add(reportKey, valuePackObject); - } - - return formatSpec.Formatter( - new SparseMultiValue(packedObjects) - { - Application = appMeta, - PlayReport = playReport - }); + return value; } return FormattedValue.Unhandled; diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs index 649813b7a..3c80198b9 100644 --- a/src/Ryujinx/Utilities/PlayReport/Specs.cs +++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs @@ -1,4 +1,7 @@ using FluentAvalonia.Core; +using MsgPack; +using Ryujinx.Ava.Utilities.AppLibrary; +using System; using System.Collections.Generic; using System.Linq; @@ -11,10 +14,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// public class GameSpec { + private int _lastPriority; + public required string[] TitleIds { get; init; } - public List SimpleValueFormatters { get; } = []; - public List MultiValueFormatters { get; } = []; - public List SparseMultiValueFormatters { get; } = []; + + public List ValueFormatters { get; } = []; /// @@ -25,7 +29,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// 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); + => AddValueFormatter(_lastPriority++, reportKey, valueFormatter); /// /// Add a value formatter at a specific priority to the current @@ -38,9 +42,9 @@ namespace Ryujinx.Ava.Utilities.PlayReport public GameSpec AddValueFormatter(int priority, string reportKey, ValueFormatter valueFormatter) { - SimpleValueFormatters.Add(new FormatterSpec + ValueFormatters.Add(new FormatterSpec { - Priority = priority, ReportKey = reportKey, Formatter = valueFormatter + Priority = priority, ReportKeys = [reportKey], Formatter = valueFormatter }); return this; } @@ -53,7 +57,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// The function which can format the values. /// The current , for chaining convenience. public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter) - => AddMultiValueFormatter(MultiValueFormatters.Count, reportKeys, valueFormatter); + => AddMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter); /// /// Add a multi-value formatter at a specific priority to the current @@ -66,7 +70,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys, MultiValueFormatter valueFormatter) { - MultiValueFormatters.Add(new MultiFormatterSpec + ValueFormatters.Add(new MultiFormatterSpec { Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter }); @@ -84,7 +88,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// The function which can format the values. /// The current , for chaining convenience. public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter) - => AddSparseMultiValueFormatter(SparseMultiValueFormatters.Count, reportKeys, valueFormatter); + => AddSparseMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter); /// /// Add a multi-value formatter at a specific priority to the current @@ -100,7 +104,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys, SparseMultiValueFormatter valueFormatter) { - SparseMultiValueFormatters.Add(new SparseMultiFormatterSpec + ValueFormatters.Add(new SparseMultiFormatterSpec { Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter }); @@ -111,30 +115,106 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// /// 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 class FormatterSpec : FormatterSpecBase { - public required int Priority { get; init; } - public required string ReportKey { get; init; } - public ValueFormatter Formatter { get; init; } + public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result) + { + if (!playReport.ReportData.AsDictionary().TryGetValue(ReportKeys[0], out MessagePackObject valuePackObject)) + { + result = null; + return false; + } + + result = valuePackObject; + return true; + } } /// /// 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 class MultiFormatterSpec : FormatterSpecBase { - public required int Priority { get; init; } - public required string[] ReportKeys { get; init; } - public MultiValueFormatter Formatter { get; init; } + public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result) + { + List packedObjects = []; + foreach (var reportKey in ReportKeys) + { + if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + { + result = null; + return false; + } + + packedObjects.Add(valuePackObject); + } + + result = packedObjects; + return true; + } } /// /// 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 class SparseMultiFormatterSpec : FormatterSpecBase { - public required int Priority { get; init; } - public required string[] ReportKeys { get; init; } - public SparseMultiValueFormatter Formatter { get; init; } + public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result) + { + Dictionary packedObjects = []; + foreach (var reportKey in ReportKeys) + { + if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + continue; + + packedObjects.Add(reportKey, valuePackObject); + } + + result = packedObjects; + return true; + } + } + + public abstract class FormatterSpecBase + { + public abstract bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object data); + + public int Priority { get; init; } + public string[] ReportKeys { get; init; } + public Delegate Formatter { get; init; } + + public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport, out FormattedValue formattedValue) + { + formattedValue = default; + if (!GetData(playReport, out object data)) + return false; + + if (data is FormattedValue fv) + { + formattedValue = fv; + return true; + } + + if (Formatter is ValueFormatter vf && data is MessagePackObject mpo) + { + formattedValue = vf(new SingleValue(mpo) { Application = appMeta, PlayReport = playReport }); + return true; + } + + if (Formatter is MultiValueFormatter mvf && data is List messagePackObjects) + { + formattedValue = mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport }); + return true; + } + + if (Formatter is SparseMultiValueFormatter smvf && + data is Dictionary sparseMessagePackObjects) + { + formattedValue = smvf(new SparseMultiValue(sparseMessagePackObjects) { Application = appMeta, PlayReport = playReport }); + return true; + } + + throw new InvalidOperationException("Formatter delegate is not of a known type!"); + } } } From 9c226dcc7a92d1068509db6edecc3a8f24cbd097 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 8 Feb 2025 01:34:44 -0600 Subject: [PATCH 08/89] misc: chore: [ci skip] rename ValueFormatter to SingleValueFormatter and some minor cleanups --- src/Ryujinx/Utilities/PlayReport/Delegates.cs | 2 +- .../Utilities/PlayReport/MatchedValues.cs | 17 +-------- src/Ryujinx/Utilities/PlayReport/Specs.cs | 35 ++++++++---------- src/Ryujinx/Utilities/PlayReport/Value.cs | 37 ++++++++++++++----- 4 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/Ryujinx/Utilities/PlayReport/Delegates.cs b/src/Ryujinx/Utilities/PlayReport/Delegates.cs index 789d408d7..92569d32e 100644 --- a/src/Ryujinx/Utilities/PlayReport/Delegates.cs +++ b/src/Ryujinx/Utilities/PlayReport/Delegates.cs @@ -10,7 +10,7 @@ ///
/// OR a signal to reset the value that the caller is using the for. ///
- public delegate FormattedValue ValueFormatter(SingleValue value); + public delegate FormattedValue SingleValueFormatter(SingleValue value); /// /// The delegate type that powers multiple value formatters.
diff --git a/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs b/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs index 01c404c31..3086a9d65 100644 --- a/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs +++ b/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs @@ -7,7 +7,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport { public abstract class MatchedValue { - public MatchedValue(T matched) + protected MatchedValue(T matched) { Matched = matched; } @@ -29,7 +29,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport } /// - /// The input data to a , + /// The input data to a , /// containing the currently running application's , /// and the matched from the Play Report. /// @@ -38,8 +38,6 @@ namespace Ryujinx.Ava.Utilities.PlayReport public SingleValue(Value matched) : base(matched) { } - - public static implicit operator SingleValue(MessagePackObject mpo) => new(mpo); } /// @@ -56,9 +54,6 @@ namespace Ryujinx.Ava.Utilities.PlayReport public MultiValue(IEnumerable matched) : base(Value.ConvertPackedObjects(matched)) { } - - public static implicit operator MultiValue(List matched) - => new(matched.Select(x => new Value(x)).ToArray()); } /// @@ -75,13 +70,5 @@ namespace Ryujinx.Ava.Utilities.PlayReport public SparseMultiValue(Dictionary matched) : base(Value.ConvertPackedObjectMap(matched)) { } - - public static implicit operator SparseMultiValue(Dictionary matched) - => new(matched - .ToDictionary( - x => x.Key, - x => new Value(x.Value) - ) - ); } } diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs index 3c80198b9..e7972fbb4 100644 --- a/src/Ryujinx/Utilities/PlayReport/Specs.cs +++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs @@ -28,7 +28,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// 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) + public GameSpec AddValueFormatter(string reportKey, SingleValueFormatter valueFormatter) => AddValueFormatter(_lastPriority++, reportKey, valueFormatter); /// @@ -40,7 +40,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// The function which can return a potential formatted value. /// The current , for chaining convenience. public GameSpec AddValueFormatter(int priority, string reportKey, - ValueFormatter valueFormatter) + SingleValueFormatter valueFormatter) { ValueFormatters.Add(new FormatterSpec { @@ -195,26 +195,21 @@ namespace Ryujinx.Ava.Utilities.PlayReport return true; } - if (Formatter is ValueFormatter vf && data is MessagePackObject mpo) + switch (Formatter) { - formattedValue = vf(new SingleValue(mpo) { Application = appMeta, PlayReport = playReport }); - return true; + case SingleValueFormatter svf when data is MessagePackObject mpo: + formattedValue = svf(new SingleValue(mpo) { Application = appMeta, PlayReport = playReport }); + return true; + case MultiValueFormatter mvf when data is List messagePackObjects: + formattedValue = mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport }); + return true; + case SparseMultiValueFormatter smvf when + data is Dictionary sparseMessagePackObjects: + formattedValue = smvf(new SparseMultiValue(sparseMessagePackObjects) { Application = appMeta, PlayReport = playReport }); + return true; + default: + throw new InvalidOperationException("Formatter delegate is not of a known type!"); } - - if (Formatter is MultiValueFormatter mvf && data is List messagePackObjects) - { - formattedValue = mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport }); - return true; - } - - if (Formatter is SparseMultiValueFormatter smvf && - data is Dictionary sparseMessagePackObjects) - { - formattedValue = smvf(new SparseMultiValue(sparseMessagePackObjects) { Application = appMeta, PlayReport = playReport }); - return true; - } - - throw new InvalidOperationException("Formatter delegate is not of a known type!"); } } } diff --git a/src/Ryujinx/Utilities/PlayReport/Value.cs b/src/Ryujinx/Utilities/PlayReport/Value.cs index 65d662ea0..b3108a41e 100644 --- a/src/Ryujinx/Utilities/PlayReport/Value.cs +++ b/src/Ryujinx/Utilities/PlayReport/Value.cs @@ -1,5 +1,4 @@ using MsgPack; -using Ryujinx.Ava.Utilities.AppLibrary; using System; using System.Collections.Generic; using System.Linq; @@ -7,8 +6,7 @@ using System.Linq; namespace Ryujinx.Ava.Utilities.PlayReport { /// - /// The input data to a , - /// containing the currently running application's , + /// The base input data to a ValueFormatter delegate, /// and the matched from the Play Report. /// public readonly struct Value @@ -70,7 +68,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport } /// - /// A potential formatted value returned by a . + /// A potential formatted value returned by a ValueFormatter delegate. /// public readonly struct FormattedValue { @@ -116,28 +114,47 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// /// Return this to tell the caller there is no value to return. /// - public static FormattedValue Unhandled => default; + public static readonly 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 }; + public static readonly 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 ValueFormatter SingleAlwaysResets = _ => ForceReset; + public static readonly SingleValueFormatter SingleAlwaysResets = _ => ForceReset; /// /// A delegate singleton you can use to always return in a . /// public static readonly MultiValueFormatter MultiAlwaysResets = _ => ForceReset; + + /// + /// A delegate singleton you can use to always return in a . + /// + public static readonly SparseMultiValueFormatter SparseMultiAlwaysResets = _ => 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 ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue; + public static SingleValueFormatter SingleAlwaysReturns(string formattedValue) => _ => formattedValue; + + /// + /// A delegate factory you can use to always return the specified + /// in a . + /// + /// The string to always return for this delegate instance. + public static MultiValueFormatter MultiAlwaysReturns(string formattedValue) => _ => formattedValue; + + /// + /// A delegate factory you can use to always return the specified + /// in a . + /// + /// The string to always return for this delegate instance. + public static SparseMultiValueFormatter SparseMultiAlwaysReturns(string formattedValue) => _ => formattedValue; } } From 253cbb28106704ca0f237de0aa0ab287d55f5162 Mon Sep 17 00:00:00 2001 From: FluffyOMC <45863583+FluffyOMC@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:23:24 -0500 Subject: [PATCH 09/89] Initial Implementation of SSBU PlayReport usage! (#638) Currently, this has as many game modes as I could find to implement, along with a list of all the characters in the game and their code ID. --- .../PlayReport/PlayReports.Formatters.cs | 296 ++++++++++++++++++ .../Utilities/PlayReport/PlayReports.cs | 111 ++----- 2 files changed, 320 insertions(+), 87 deletions(-) create mode 100644 src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs new file mode 100644 index 000000000..1cddee68b --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs @@ -0,0 +1,296 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Ava.Utilities.PlayReport +{ + public partial class PlayReports + { + private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value) + => value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset; + + private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) => + value.Matched.DoubleValue switch + { + > 800d => "Exploring the Sky Islands", + < -201d => "Exploring the Depths", + _ => "Roaming Hyrule" + }; + + private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value) + => value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; + + private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value) + => value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; + + private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value) + => value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; + + private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value) + => value.Matched.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(SingleValue value) + => value.Matched.BoxedValue is 0 ? "Playing Alone" : "Playing in a group"; + + private static FormattedValue PokemonSVArea(SingleValue value) + => value.Matched.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 + }; + + private static FormattedValue SuperSmashBrosUltimate_Mode(SparseMultiValue values) + { + // Check if the PlayReport is for a challenger approach or an achievement. + if (values.Matched.TryGetValue("fighter", out Value fighter) && values.Matched.ContainsKey("reason")) + { + return $"Challenger Approaches - {SuperSmashBrosUltimate_Character(fighter)}"; + } + + if (values.Matched.TryGetValue("fighter", out fighter) && values.Matched.ContainsKey("challenge_count")) + { + return $"Fighter Unlocked - {SuperSmashBrosUltimate_Character(fighter)}"; + } + + if (values.Matched.TryGetValue("anniversary", out Value anniversary)) + { + return $"Achievement Unlocked - ID: {anniversary}"; + } + + if (values.Matched.ContainsKey("adv_slot")) + { + return "Playing Adventure Mode"; // Doing this as it can be a placeholder until we can grab the character. + } + + // Check if we have a match_mode at this point, if not, go to default. + if (!values.Matched.TryGetValue("match_mode", out Value matchMode)) + { + return "Smashing"; + } + + return matchMode.BoxedValue switch + { + 0 when values.Matched.TryGetValue("player_1_fighter", out Value player) && + values.Matched.TryGetValue("player_2_fighter", out Value challenger) + => $"Last Smashed: {SuperSmashBrosUltimate_Character(challenger)}'s Fighter Challenge - {SuperSmashBrosUltimate_Character(player)}", + 1 => $"Last Smashed: Normal Battle - {SuperSmashBrosUltimate_PlayerListing(values)}", + 2 when values.Matched.TryGetValue("player_1_rank", out Value team) + => team.BoxedValue is 0 + ? "Last Smashed: Squad Strike - Red Team Wins" + : "Last Smashed: Squad Strike - Blue Team Wins", + 3 => $"Last Smashed: Custom Smash - {SuperSmashBrosUltimate_PlayerListing(values)}", + 4 => $"Last Smashed: Super Sudden Death - {SuperSmashBrosUltimate_PlayerListing(values)}", + 5 => $"Last Smashed: Smashdown - {SuperSmashBrosUltimate_PlayerListing(values)}", + 6 => $"Last Smashed: Tourney Battle - {SuperSmashBrosUltimate_PlayerListing(values)}", + 7 when values.Matched.TryGetValue("player_1_fighter", out Value player) + => $"Last Smashed: Spirit Board Battle as {SuperSmashBrosUltimate_Character(player)}", + 8 when values.Matched.TryGetValue("player_1_fighter", out Value player) + => $"Playing Adventure Mode as {SuperSmashBrosUltimate_Character(player)}", + 10 when values.Matched.TryGetValue("match_submode", out Value battle) && + values.Matched.TryGetValue("player_1_fighter", out Value player) + => $"Last Smashed: Classic Mode, Battle {(int)battle.BoxedValue + 1}/8 as {SuperSmashBrosUltimate_Character(player)}", + 12 => $"Last Smashed: Century Smash - {SuperSmashBrosUltimate_PlayerListing(values)}", + 13 => $"Last Smashed: All-Star Smash - {SuperSmashBrosUltimate_PlayerListing(values)}", + 14 => $"Last Smashed: Cruel Smash - {SuperSmashBrosUltimate_PlayerListing(values)}", + 15 when values.Matched.TryGetValue("player_1_fighter", out Value player) + => $"Last Smashed: Home-Run Contest - {SuperSmashBrosUltimate_Character(player)}", + 16 when values.Matched.TryGetValue("player_1_fighter", out Value player1) && + values.Matched.TryGetValue("player_2_fighter", out Value player2) + => $"Last Smashed: Home-Run Content (Co-op) - {SuperSmashBrosUltimate_Character(player1)} and {SuperSmashBrosUltimate_Character(player2)}", + 17 => $"Last Smashed: Home-Run Contest (Versus) - {SuperSmashBrosUltimate_PlayerListing(values)}", + 18 when values.Matched.TryGetValue("player_1_fighter", out Value player1) && + values.Matched.TryGetValue("player_2_fighter", out Value player2) + => $"Fresh out of Training mode - {SuperSmashBrosUltimate_Character(player1)} with {SuperSmashBrosUltimate_Character(player2)}", + 58 => $"Last Smashed: LDN Battle - {SuperSmashBrosUltimate_PlayerListing(values)}", + 63 when values.Matched.TryGetValue("player_1_fighter", out Value player) + => $"Last Smashed: DLC Spirit Board Battle as {SuperSmashBrosUltimate_Character(player)}", + _ => "Smashing" + }; + } + + private static string SuperSmashBrosUltimate_Character(Value value) => + BinaryPrimitives.ReverseEndianness( + BitConverter.ToInt64(((MsgPack.MessagePackExtendedTypeObject)value.BoxedValue).GetBody(), 0)) switch + { + 0x0 => "Mario", + 0x1 => "Donkey Kong", + 0x2 => "Link", + 0x3 => "Samus", + 0x4 => "Dark Samus", + 0x5 => "Yoshi", + 0x6 => "Kirby", + 0x7 => "Fox", + 0x8 => "Pikachu", + 0x9 => "Luigi", + 0xA => "Ness", + 0xB => "Captain Falcon", + 0xC => "Jigglypuff", + 0xD => "Peach", + 0xE => "Daisy", + 0xF => "Bowser", + 0x10 => "Ice Climbers", + 0x11 => "Sheik", + 0x12 => "Zelda", + 0x13 => "Dr. Mario", + 0x14 => "Pichu", + 0x15 => "Falco", + 0x16 => "Marth", + 0x17 => "Lucina", + 0x18 => "Young Link", + 0x19 => "Ganondorf", + 0x1A => "Mewtwo", + 0x1B => "Roy", + 0x1C => "Chrom", + 0x1D => "Mr Game & Watch", + 0x1E => "Meta Knight", + 0x1F => "Pit", + 0x20 => "Dark Pit", + 0x21 => "Zero Suit Samus", + 0x22 => "Wario", + 0x23 => "Snake", + 0x24 => "Ike", + 0x25 => "Pokémon Trainer", + 0x26 => "Diddy Kong", + 0x27 => "Lucas", + 0x28 => "Sonic", + 0x29 => "King Dedede", + 0x2A => "Olimar", + 0x2B => "Lucario", + 0x2C => "R.O.B.", + 0x2D => "Toon Link", + 0x2E => "Wolf", + 0x2F => "Villager", + 0x30 => "Mega Man", + 0x31 => "Wii Fit Trainer", + 0x32 => "Rosalina & Luma", + 0x33 => "Little Mac", + 0x34 => "Greninja", + 0x35 => "Palutena", + 0x36 => "Pac-Man", + 0x37 => "Robin", + 0x38 => "Shulk", + 0x39 => "Bowser Jr.", + 0x3A => "Duck Hunt", + 0x3B => "Ryu", + 0x3C => "Ken", + 0x3D => "Cloud", + 0x3E => "Corrin", + 0x3F => "Bayonetta", + 0x40 => "Richter", + 0x41 => "Inkling", + 0x42 => "Ridley", + 0x43 => "King K. Rool", + 0x44 => "Simon", + 0x45 => "Isabelle", + 0x46 => "Incineroar", + 0x47 => "Mii Brawler", + 0x48 => "Mii Swordfighter", + 0x49 => "Mii Gunner", + 0x4A => "Piranha Plant", + 0x4B => "Joker", + 0x4C => "Hero", + 0x4D => "Banjo", + 0x4E => "Terry", + 0x4F => "Byleth", + 0x50 => "Min Min", + 0x51 => "Steve", + 0x52 => "Sephiroth", + 0x53 => "Pyra/Mythra", + 0x54 => "Kazuya", + 0x55 => "Sora", + 0xFE => "Random", + 0xFF => "Scripted Entity", + _ => "Unknown" + }; + + private static string SuperSmashBrosUltimate_PlayerListing(SparseMultiValue values) + { + List<(string Character, int PlayerNumber, int? Rank)> players = []; + + foreach (KeyValuePair player in values.Matched) + { + if (player.Key.StartsWith("player_") && player.Key.EndsWith("_fighter") && + player.Value.BoxedValue is not null) + { + if (!int.TryParse(player.Key.Split('_')[1], out int playerNumber)) + continue; + + string character = SuperSmashBrosUltimate_Character(player.Value); + int? rank = values.Matched.TryGetValue($"player_{playerNumber}_rank", out Value rankValue) + ? rankValue.IntValue + : null; + + players.Add((character, playerNumber, rank)); + } + } + + players = players.OrderBy(p => p.Rank ?? int.MaxValue).ToList(); + + return players.Count > 4 + ? $"{players.Count} Players - " + string.Join(", ", + players.Take(3).Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")) + : string.Join(", ", players.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")); + + string RankMedal(int? rank) => rank switch + { + 0 => "🥇", + 1 => "🥈", + 2 => "🥉", + _ => "" + }; + } + } +} diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs index 9e22cd6d2..d4fb6cb53 100644 --- a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs @@ -1,6 +1,11 @@ -namespace Ryujinx.Ava.Utilities.PlayReport +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Ava.Utilities.PlayReport { - public static class PlayReports + public static partial class PlayReports { public static Analyzer Analyzer { get; } = new Analyzer() .AddSpec( @@ -37,91 +42,23 @@ spec => spec .AddValueFormatter("area_no", PokemonSVArea) .AddValueFormatter("team_circle", PokemonSVUnionCircle) + ).AddSpec( + "01006a800016e000", + spec => spec + .AddSparseMultiValueFormatter( + [ + // Metadata to figure out what PlayReport we have. + "match_mode", "match_submode", "anniversary", "fighter", "reason", "challenge_count", + "adv_slot", + // List of Fighters + "player_1_fighter", "player_2_fighter", "player_3_fighter", "player_4_fighter", + "player_5_fighter", "player_6_fighter", "player_7_fighter", "player_8_fighter", + // List of rankings/placements + "player_1_rank", "player_2_rank", "player_3_rank", "player_4_rank", "player_5_rank", + "player_6_rank", "player_7_rank", "player_8_rank" + ], + SuperSmashBrosUltimate_Mode + ) ); - - private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value) - => value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset; - - private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) => - value.Matched.DoubleValue switch - { - > 800d => "Exploring the Sky Islands", - < -201d => "Exploring the Depths", - _ => "Roaming Hyrule" - }; - - private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value) - => value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; - - private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value) - => value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; - - private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value) - => value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; - - private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value) - => value.Matched.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(SingleValue value) - => value.Matched.BoxedValue is 0 ? "Playing Alone" : "Playing in a group"; - - private static FormattedValue PokemonSVArea(SingleValue value) - => value.Matched.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 - }; } } From 93a298523f5ea87c27cd92cce52b3fffc3463c91 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 8 Feb 2025 16:52:07 -0600 Subject: [PATCH 10/89] Dynamic Presence support for every NSO emulator --- src/Ryujinx.Common/TitleIDs.cs | 5 +- .../PlayReport/PlayReports.Formatters.cs | 335 +++++++++++++++++- .../Utilities/PlayReport/PlayReports.cs | 26 +- 3 files changed, 358 insertions(+), 8 deletions(-) diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs index 42322c8a2..28d332a61 100644 --- a/src/Ryujinx.Common/TitleIDs.cs +++ b/src/Ryujinx.Common/TitleIDs.cs @@ -164,15 +164,16 @@ namespace Ryujinx.Common "0100ba0018500000", // Splatoon 3: Splatfest World Premiere //NSO Membership games - "0100ccf019c8c000", // F-ZERO 99 "0100c62011050000", // GB - Nintendo Switch Online "010012f017576000", // GBA - Nintendo Switch Online "0100c9a00ece6000", // N64 - Nintendo Switch Online "0100e0601c632000", // N64 - Nintendo Switch Online 18+ "0100d870045b6000", // NES - Nintendo Switch Online + "0100b3c014bda000", // SEGA Genesis - Nintendo Switch Online + "01008d300c50c000", // SNES - Nintendo Switch Online + "0100ccf019c8c000", // F-ZERO 99 "0100ad9012510000", // PAC-MAN 99 "010040600c5ce000", // Tetris 99 - "01008d300c50c000", // SNES - Nintendo Switch Online "0100277011f1a000", // Super Mario Bros. 35 //Misc Nintendo 1st party games diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs index 1cddee68b..3b7699191 100644 --- a/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs @@ -7,7 +7,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport { public partial class PlayReports { - private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value) + private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value) => value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset; private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) => @@ -112,7 +112,8 @@ namespace Ryujinx.Ava.Utilities.PlayReport if (values.Matched.ContainsKey("adv_slot")) { - return "Playing Adventure Mode"; // Doing this as it can be a placeholder until we can grab the character. + return + "Playing Adventure Mode"; // Doing this as it can be a placeholder until we can grab the character. } // Check if we have a match_mode at this point, if not, go to default. @@ -267,7 +268,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport { if (!int.TryParse(player.Key.Split('_')[1], out int playerNumber)) continue; - + string character = SuperSmashBrosUltimate_Character(player.Value); int? rank = values.Matched.TryGetValue($"player_{playerNumber}_rank", out Value rankValue) ? rankValue.IntValue @@ -278,12 +279,12 @@ namespace Ryujinx.Ava.Utilities.PlayReport } players = players.OrderBy(p => p.Rank ?? int.MaxValue).ToList(); - + return players.Count > 4 ? $"{players.Count} Players - " + string.Join(", ", players.Take(3).Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")) : string.Join(", ", players.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")); - + string RankMedal(int? rank) => rank switch { 0 => "🥇", @@ -292,5 +293,329 @@ namespace Ryujinx.Ava.Utilities.PlayReport _ => "" }; } + + private static FormattedValue N64_LaunchedGame(SingleValue value) => value.Matched.StringValue switch + { + "n_1653_e" or "n_1653_p" => Playing("1080º ™ Snowboarding"), + "n_4868_e" or "n_4868_p" => Playing("Banjo Kazooie™"), + "n_1226_e" or "n_1226_p" => Playing("Banjo-Tooie™"), + "n_3083_e" or "n_3083_p" => Playing("Blast Corps"), + "n_3007_e" => Playing("Dr. Mario™ 64"), + "n_4238_e" => Playing("Excitebike™ 64"), + "n_1870_e" => Playing("Extreme G"), + "n_2456_e" => Playing("F-Zero™ X"), + "n_4631_e" => Playing("GoldenEye 007"), + "n_1635_e" => Playing("Harvest Moon 64"), + "n_2225_e" => Playing("Iggy’s Reckin’ Balls"), + "n_1625_e" or "n_1625_p" => Playing("JET FORCE GEMINI™"), + "n_3052_e" => Playing("Kirby 64™: The Crystal Shards"), + "n_4371_e" => Playing("Mario Golf™"), + "n_3013_e" => Playing("Mario Kart™ 64"), + "n_1053_e" or "n_1053_p" => Playing("Mario Party™ 2"), + "n_2965_e" or "n_2965_p" => Playing("Mario Party™ 3"), + "n_4737_e" or "n_4737_p" => Playing("Mario Party™"), + "n_3017_e" => Playing("Mario Tennis™"), + "n_2992_e" or "n_2992_p" => Playing("Paper Mario™"), + "n_3783_e" or "n_3783_p" => Playing("Pilotwings™ 64"), + "n_1848_e" or "n_1848_pd" or "n_1848_pf" => Playing("Pokémon™ Puzzle League"), + "n_3240_e" or "n_3240_pd" or "n_3240_pf" or "n_3240_pi" or "n_3240_ps" => Playing("Pokémon Snap™"), + "n_4590_e" or "n_4590_pd" or "n_4590_pf" or "n_4590_pi" or "n_4590_ps" => Playing("Pokémon Stadium™"), + "n_3309_e" or "n_3309_pd" or "n_3309_pf" or "n_3309_pi" or "n_3309_ps" => Playing("Pokémon Stadium 2™"), + "n_3029_e" => Playing("Sin & Punishment™"), + "n_3030_e" => Playing("Star Fox™ 64"), + "n_3030_p" => Playing("Lylat Wars™"), + "n_3031_e" or "n_3031_p" => Playing("Super Mario 64™"), + "n_4813_e" or "n_4813_p" => Playing("Wave Race™ 64"), + "n_3034_e" => Playing("WIN BACK: COVERT OPERATIONS"), + "n_3034_p" => Playing("OPERATION: WIN BACK"), + "n_3036_e" or "n_3036_p" => Playing("Yoshi's Story™"), + "n_1407_e" or "n_1407_p" => Playing("The Legend of Zelda™: Majora's Mask™"), + "n_3038_e" or "n_3038_p" => Playing("The Legend of Zelda™: Ocarina of Time™"), + _ => FormattedValue.ForceReset + }; + + private static FormattedValue NES_LaunchedGame(SingleValue value) => value.Matched.StringValue switch + { + "clv_p_naaae" => Playing("Super Mario Bros.™"), + "clv_p_naabe" => Playing("Super Mario Bros.™: The Lost Levels"), + "clv_p_naace" or "clv_p_naace_sp1" => Playing("Super Mario Bros.™ 3"), + "clv_p_naade" => Playing("Super Mario Bros.™ 2"), + "clv_p_naaee" => Playing("Donkey Kong™"), + "clv_p_naafe" => Playing("Donkey Kong Jr.™"), + "clv_p_naage" => Playing("Donkey Kong™ 3"), + "clv_p_naahe" => Playing("Excitebike™"), + "clv_p_naaje" => Playing("EarthBound Beginnings"), + "clv_p_naame" => Playing("NES™ Open Tournament Golf"), + "clv_p_naane" or "clv_p_naane_sp1" => Playing("The Legend of Zelda™"), + "clv_p_naape" or "clv_p_naape_sp1" => Playing("Kirby's Adventure™"), + "clv_p_naaqe" or "clv_p_naaqe_sp1" or "clv_p_naaqe_sp2" => Playing("Metroid™"), + "clv_p_naare" => Playing("Balloon Fight™"), + "clv_p_naase" or "clv_p_naase_sp1" => Playing("Zelda II - The Adventure of Link™"), + "clv_p_naate" => Playing("Punch-Out!!™ Featuring Mr. Dream"), + "clv_p_naaue" => Playing("Ice Climber™"), + "clv_p_naave" or "clv_p_naave_sp1" => Playing("Kid Icarus™"), + "clv_p_naawe" => Playing("Mario Bros.™"), + "clv_p_naaxe" or "clv_p_naaxe_sp1" => Playing("Dr. Mario™"), + "clv_p_naaye" => Playing("Yoshi™"), + "clv_p_naaze" => Playing("StarTropics™"), + "clv_p_nabce" or "clv_p_nabce_sp1" => Playing("Ghosts'n Goblins™"), + "clv_p_nabre" or "clv_p_nabre_sp1" or "clv_p_nabre_sp2" => Playing("Gradius"), + "clv_p_nacbe" or "clv_p_nacbe_sp1" => Playing("Ninja Gaiden"), + "clv_p_nacce" => Playing("Solomon's Key"), + "clv_p_nacde" => Playing("Tecmo Bowl"), + "clv_p_nacfe" => Playing("Double Dragon"), + "clv_p_nache" => Playing("Double Dragon II: The Revenge"), + "clv_p_nacje" => Playing("River City Ransom"), + "clv_p_nacke" => Playing("Super Dodge Ball"), + "clv_p_nacle" => Playing("Downtown Nekketsu March Super-Awesome Field Day!"), + "clv_p_nacpe" => Playing("The Mystery of Atlantis"), + "clv_p_nacre" => Playing("Soccer"), + "clv_p_nacse" or "clv_p_nacse_sp1" => Playing("Ninja JaJaMaru-kun"), + "clv_p_nacte" => Playing("Ice Hockey"), + "clv_p_nacue" or "clv_p_nacue_sp1" => Playing("Blaster Master"), + "clv_p_nacwe" => Playing("ADVENTURES OF LOLO"), + "clv_p_nacxe" => Playing("Wario's Woods™"), + "clv_p_nacye" => Playing("Tennis"), + "clv_p_nacze" => Playing("Wrecking Crew™"), + "clv_p_nadbe" => Playing("Joy Mech Fight™"), + "clv_p_nadde" or "clv_p_nadde_sp1" => Playing("Star Soldier"), + "clv_p_nadke" => Playing("Tetris®"), + "clv_p_nadle" => Playing("Pro Wrestling"), + "clv_p_nadpe" => Playing("Baseball"), + "clv_p_nadte" or "clv_p_nadte_sp1" => Playing("TwinBee"), + "clv_p_nadue" or "clv_p_nadue_sp1" => Playing("Mighty Bomb Jack"), + "clv_p_nadve" => Playing("Kung-Fu Heroes"), + "clv_p_nadxe" => Playing("City Connection"), + "clv_p_nadye" => Playing("Rygar"), + "clv_p_naeae" => Playing("Crystalis"), + "clv_p_naece" => Playing("Vice: Project Doom"), + "clv_p_naehe" => Playing("Clu Clu Land™"), + "clv_p_naeie" => Playing("VS. Excitebike™"), + "clv_p_naeje" => Playing("Volleyball™"), + "clv_p_naeke" => Playing("JOURNEY TO SILIUS"), + "clv_p_naele" => Playing("S.C.A.T.: Special Cybernetic Attack Team"), + "clv_p_naeme" => Playing("Shadow of the Ninja"), + "clv_p_naene" => Playing("Nightshade"), + "clv_p_naepe" => Playing("The Immortal"), + "clv_p_naeqe" => Playing("Eliminator Boat Duel"), + "clv_p_naere" => Playing("Fire 'n Ice"), + "clv_p_nafce" => Playing("XEVIOUS"), + "clv_p_nagpe" => Playing("DAIVA STORY 6 IMPERIAL OF NIRSARTIA"), + "clv_p_nagqe" => Playing("DIG DUGⅡ"), + "clv_p_nague" => Playing("MAPPY-LAND"), + "clv_p_nahhe" => Playing("Mach Rider™"), + "clv_p_nahje" => Playing("Pinball"), + "clv_p_nahre" => Playing("Mystery Tower"), + "clv_p_nahte" => Playing("Urban Champion™"), + "clv_p_nahue" => Playing("Donkey Kong Jr.™ Math"), + "clv_p_nahve" => Playing("The Mysterious Murasame Castle"), + "clv_p_najae" => Playing("DEVIL WORLD™"), + "clv_p_najbe" => Playing("Golf"), + "clv_p_najpe" => Playing("R.C. PRO-AM™"), + "clv_p_najre" => Playing("COBRA TRIANGLE™"), + "clv_p_najse" => Playing("SNAKE RATTLE N ROLL™"), + "clv_p_najte" => Playing("SOLAR® JETMAN"), + _ => FormattedValue.ForceReset + }; + + private static FormattedValue SNES_LaunchedGame(SingleValue value) => value.Matched.StringValue switch + { + "s_2180_e" => Playing("BATTLETOADS™ DOUBLE DRAGON™"), + "s_2179_e" => Playing("BATTLETOADS™ IN BATTLEMANIACS"), + "s_2182_e" => Playing("BIG RUN"), + "s_2156_e" => Playing("Bombuzal"), + "s_2002_e" => Playing("BRAWL BROTHERS"), + "s_2025_e" => Playing("Breath of Fire II"), + "s_2003_e" => Playing("Breath Of Fire"), + "s_2163_e" => Playing("Claymates"), + "s_2150_e" => Playing("Congo's Caper"), + "s_2171_e" => Playing("COSMO GANG THE PUZZLE"), + "s_2004_e" => Playing("Demon's Crest"), + "s_2026_e" => Playing("Kunio-kun no Dodgeball da yo Zen'in Shūgō!"), + "s_2060_e" => Playing("Donkey Kong Country 2: Diddy's Kong Quest"), + "s_2061_e" => Playing("Donkey Kong Country 3: Dixie Kong's Double Trouble!"), + "s_2055_e" => Playing("Donkey Kong Country"), + "s_2139_e" => Playing("DOOMSDAY WARRIOR"), + "s_2051_e" => Playing("EarthBound"), + "s_2162_e" => Playing("Earthworm Jim™ 2"), + "s_2005_e" => Playing("F-ZERO™"), + "s_2183_e" => Playing("FATAL FURY 2"), + "s_2174_e" => Playing("Fighter's History"), + "s_2037_e" => Playing("Harvest Moon"), + "s_2161_e" => Playing("Jelly Boy"), + "s_2006_e" => Playing("Joe & Mac 2: Lost in the Tropics"), + "s_2169_e" => Playing("Caveman Ninja"), + "s_2181_e" => Playing("KILLER INSTINCT™"), + "s_2029_e" or "s_2029_e_sp1" => Playing("Kirby Super Star™"), + "s_2121_e" => Playing("Kirby's Avalanche™"), + "s_2007_e" or "s_2007_e_sp1" => Playing("Kirby's Dream Course™"), + "s_2008_e" or "s_2008_e_sp1" => Playing("Kirby's Dream Land™ 3"), + "s_2172_e" => Playing("Kirby’s Star Stacker™"), + "s_2151_e" => Playing("Magical Drop2"), + "s_2044_e" => Playing("Mario's Super Picross"), + "s_2038_e" => Playing("Natsume Championship Wrestling"), + "s_2140_e" => Playing("Operation Logic Bomb"), + "s_2034_e" => Playing("Panel de Pon"), + "s_2009_e" => Playing("Pilotwings™"), + "s_2010_e" => Playing("Pop'n TwinBee"), + "s_2157_e" => Playing("Prehistorik Man"), + "s_2145_e" => Playing("Psycho Dream"), + "s_2141_e" => Playing("Rival Turf!"), + "s_2152_e" => Playing("SIDE POCKET"), + "s_2158_e" => Playing("Spanky’s™ Quest"), + "s_2031_e" => Playing("Star Fox™ 2"), + "s_2011_e" => Playing("Star Fox™"), + "s_2012_e" => Playing("Stunt Race FX™"), + "s_2032_e" => Playing("Amazing Hebereke"), + "s_2159_e" => Playing("Super Baseball Simulator 1.000"), + "s_2013_e" => Playing("SUPER E.D.F. EARTH DEFENSE FORCE"), + "s_2014_e" => Playing("Smash Tennis"), + "s_2015_e" => Playing("Super Ghouls'n Ghosts™"), + "s_2033_e" => Playing("Super Mario All-Stars™"), + "s_2016_e" or "s_2016_e_sp1" => Playing("Super Mario Kart™"), + "s_2017_e" or "s_2017_e_sp1" => Playing("Super Mario World™"), + "s_2018_e" or "s_2018_e_sp1" => Playing("Super Metroid™"), + "s_2184_e" => Playing("Super Ninja Boy"), + "s_2019_e" or "s_2019_e_sp1" => Playing("Super Punch-Out!!™"), + "s_2020_e" => Playing("Super Puyo Puyo 2"), + "s_2133_e" => Playing("SUPER R-TYPE"), + "s_2021_e" => Playing("Super Soccer"), + "s_2022_e" => Playing("Super Tennis"), + "s_2136_e" => Playing("Sutte Hakkun"), + "s_2142_e" => Playing("The Ignition Factor"), + "s_2143_e" => Playing("The Peace Keepers"), + "s_2146_e" => Playing("Tuff E Nuff"), + "s_2144_e" => Playing("SUPER VALIS Ⅳ"), + "s_2049_e" => Playing("Wild Guns"), + "s_2096_e" => Playing("Wrecking Crew™ '98"), + "s_2023_e" => Playing("Super Mario World™ 2: Yoshi's Island™"), + "s_2024_e" => Playing("The Legend of Zelda™: A Link to the Past™"), + _ => FormattedValue.ForceReset + }; + + private static FormattedValue Genesis_LaunchedGame(SingleValue value) => value.Matched.StringValue switch + { + "m_0054_e" => Playing("Alien Soldier"), + "m_3978_e" => Playing("Alien Storm"), + "m_5234_e" => Playing("ALISIA DRAGOON"), + "m_5003_e" => Playing("Streets of Rage 2"), + "m_4843_e" => Playing("Kid Chameleon"), + "m_2874_e" => Playing("Columns"), + "m_3167_e" => Playing("Comix Zone"), + "m_5007_e" => Playing("Contra: Hard Corps"), + "m_0865_e" => Playing("Ghouls 'n Ghosts"), + "m_0935_e" => Playing("Dynamite Headdy"), + "m_8314_e" => Playing("Earthworm Jim"), + "m_5012_e" => Playing("Ecco the Dolphin"), + "m_2207_e" => Playing("Flicky"), + "m_9432_e" => Playing("Golden Axe II"), + "m_5015_e" => Playing("Golden Axe"), + "m_5017_e" => Playing("Gunstar Heroes"), + "m_0732_e" => Playing("Altered Beast"), + "m_2245_e" or "m_2245_pd" or "m_2245_pf" => Playing("Landstalker"), + "m_1654_e" => Playing("Target Earth"), + "m_7050_e" => Playing("Light Crusader"), + "m_5027_e" => Playing("M.U.S.H.A."), + "m_5028_e" => Playing("Phantasy Star IV"), + "m_9155_e" => Playing("Pulseman"), + "m_5030_e" => Playing("Dr. Robotnik's Mean Bean Machine"), + "m_0098_e" => Playing("Crusader of Centy"), + "m_0098_k" => Playing("신창세기 라그나센티"), + "m_0098_pd" or "m_0098_pf" or "m_0098_ps" => Playing("Soleil"), + "m_5033_e" => Playing("Ristar"), + "m_1987_e" => Playing("MEGA MAN: THE WILY WARS"), + "m_2609_e" => Playing("WOLF OF THE BATTLEFIELD: MERCS"), + "m_3353_e" => Playing("Shining Force II"), + "m_5036_e" => Playing("Shining Force"), + "m_9866_e" => Playing("Sonic The Hedgehog Spinball"), + "m_5041_e" => Playing("Sonic The Hedgehog 2"), + "m_5523_e" => Playing("Space Harrier II"), + "m_0041_e" => Playing("STREET FIGHTER II' : SPECIAL CHAMPION EDITION"), + "m_5044_e" => Playing("STRIDER"), + "m_6353_e" => Playing("Super Fantasy Zone"), + "m_9569_e" => Playing("Beyond Oasis"), + "m_9569_k" => Playing("스토리 오브 도어"), + "m_9569_pd" or "m_9569_ps" => Playing("The Story of Thor"), + "m_9569_pf" => Playing("La Légende de Thor"), + "m_5049_e" => Playing("Shinobi III: Return of the Ninja Master"), + "m_6811_e" => Playing("The Revenge of Shinobi"), + "m_4372_e" => Playing("Thunder Force II"), + "m_1535_e" => Playing("ToeJam & Earl in Panic on Funkotron"), + "m_0432_e" => Playing("ToeJam & Earl"), + "m_5052_e" => Playing("Castlevania: BLOODLINES"), + "m_3626_e" => Playing("VectorMan"), + "m_7955_e" => Playing("Sword of Vermilion"), + "m_0394_e" => Playing("Virtua Fighter 2"), + "m_9417_e" => Playing("Zero Wing"), + _ => FormattedValue.ForceReset + }; + + private static FormattedValue GBA_LaunchedGame(SingleValue value) => value.Matched.StringValue switch + { + "a_9694_e" => Playing("Densetsu no Starfy 1"), + "a_5600_e" => Playing("Densetsu no Starfy 2"), + "a_7565_e" => Playing("Densetsu no Starfy 3"), + "a_6553_e" => Playing("F-ZERO CLIMAX"), + "a_7842_e" or "a_7842_p" => Playing("F-Zero™- GP Legend"), + "a_9283_e" => Playing("F-Zero™ Maximum Velocity"), + "a_3744_e" or "a_3744_x" or "a_3744_y" => Playing("Fire Emblem™"), + "a_8978_d" or "a_8978_e" or "a_8978_f" or "a_8978_i" or "a_8978_s" => Playing("Golden Sun™: The Lost Age"), + "a_3108_d" or "a_3108_e" or "a_3108_f" or "a_3108_i" or "a_3108_s" => Playing("Golden Sun™"), + "a_3654_e" or "a_3654_p" => Playing("Kirby™ & The Amazing Mirror"), + "a_7279_p" => Playing("Kuru Kuru Kururin™"), + "a_7311_e" or "a_7311_p" => Playing("Mario & Luigi™: Superstar Saga"), + "a_6845_e" => Playing("Mario Kart™: Super Circuit™"), + "a_4139_e" or "a_4139_p" => Playing("Metroid™ Fusion"), + "a_6834_e" or "a_6834_p" => Playing("Metroid™: Zero Mission"), + "a_8989_e" or "a_8989_p" => Playing("Pokémon™ Mystery Dungeon: Red Rescue Team"), + "a_9444_e" => Playing("Super Mario™ Advance"), + "a_9901_e" or "a_9901_p" => Playing("Super Mario™ Advance 4: Super Mario Bros.™ 3"), + "a_2939_e" => Playing("Super Mario World™: Super Mario Advance 2"), + "a_2939_p" => Playing("Super Mario World™: Super Mario Advance 2™"), + "a_1302_e" => Playing("WarioWare™, Inc.: Mega Microgame$!"), + "a_1302_p" => Playing("WarioWare™, Inc.: Minigame Mania."), + "a_6960_e" or "a_6960_p" => Playing("Yoshi's Island™: Super Mario™ Advance 3"), + "a_5190_e" or "a_5190_p" => Playing("The Legend of Zelda™: A Link to the Past™ Four Swords"), + "a_8665_e" or "a_8665_p" => Playing("The Legend of Zelda™: The Minish Cap"), + _ => FormattedValue.ForceReset + }; + + private static FormattedValue GB_LaunchedGame(SingleValue value) => value.Matched.StringValue switch + { + "c_7224_e" or "c_7224_p" => Playing("Alone in the Dark: The New Nightmare"), + "c_5022_e" => Playing("Blaster Master: Enemy Below"), + "c_3381_e" => Playing("Game & Watch™ Gallery 3"), + "c_0282_e" => Playing("Kirby Tilt ‘n’ Tumble™"), + "c_4471_e" or "c_4471_p" => Playing("Mario Golf™"), + "c_9947_e" => Playing("Mario Tennis™"), + "c_3191_e" or "c_3191_p" or "c_3191_x" => Playing("Pokémon™ Trading Card Game"), + "c_8914_e" or "c_8914_p" => Playing("Quest for Camelot™"), + "c_2648_e" => Playing("Tetris® DX"), + "c_5928_e" => Playing("Wario Land™ 3"), + "c_3996_e" or "c_3996_pd" or "c_3996_pf" => Playing("The Legend of Zelda™: Link's Awakening DX™"), + "c_8852_e" or "c_8852_p" => Playing("The Legend of Zelda™: Oracle of Ages™"), + "c_9130_e" or "c_9130_p" => Playing("The Legend of Zelda™: Oracle of Seasons™"), + "d_6879_e" => Playing("Alleyway™"), + "d_7618_e" => Playing("Baseball"), + "d_6005_e" => Playing("BurgerTime Deluxe"), + "d_7120_e" => Playing("Castlevania Legends"), + "d_2744_e" => Playing("Dr. Mario™"), + "d_1593_e" => Playing("Donkey Kong Land 2™"), + "d_7216_e" => Playing("Donkey Kong Land III™"), + "d_4971_e" => Playing("Donkey Kong Land™"), + "d_7984_e" => Playing("GARGOYLE'S QUEST"), + "d_8212_e" => Playing("Kirby's Dream Land™ 2"), + "d_5661_e" => Playing("Kirby's Dream Land™"), + "d_3837_e" => Playing("MEGA MAN II"), + "d_1965_e" => Playing("MEGA MAN III"), + "d_0194_e" => Playing("MEGA MAN IV"), + "d_1425_e" => Playing("MEGA MAN V"), + "d_9324_e" => Playing("MEGA MAN: DR. WILY'S REVENGE"), + "d_1577_e" => Playing("Metroid™ II - Return of Samus™"), + "d_5124_e" => Playing("Super Mario Land™ 2 - 6 Golden Coins™"), + "d_7970_e" => Playing("Super Mario Land™"), + "d_8484_e" => Playing("Tetris®"), + _ => FormattedValue.ForceReset + }; } } diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs index d4fb6cb53..e012a92ad 100644 --- a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs @@ -42,7 +42,8 @@ namespace Ryujinx.Ava.Utilities.PlayReport spec => spec .AddValueFormatter("area_no", PokemonSVArea) .AddValueFormatter("team_circle", PokemonSVUnionCircle) - ).AddSpec( + ) + .AddSpec( "01006a800016e000", spec => spec .AddSparseMultiValueFormatter( @@ -59,6 +60,29 @@ namespace Ryujinx.Ava.Utilities.PlayReport ], SuperSmashBrosUltimate_Mode ) + ) + .AddSpec( + "0100c9a00ece6000", + spec => spec.AddValueFormatter("launch_title_id", N64_LaunchedGame) + ) + .AddSpec( + "01008d300c50c000", + spec => spec.AddValueFormatter("launch_title_id", SNES_LaunchedGame) + ) + .AddSpec( + "0100d870045b6000", + spec => spec.AddValueFormatter("launch_title_id", NES_LaunchedGame) + ).AddSpec( + "010012f017576000", + spec => spec.AddValueFormatter("launch_title_id", GBA_LaunchedGame) + ).AddSpec( + "0100c62011050000", + spec => spec.AddValueFormatter("launch_title_id", GB_LaunchedGame) + ).AddSpec( + "0100b3c014bda000", + spec => spec.AddValueFormatter("launch_title_id", Genesis_LaunchedGame) ); + + private static string Playing(string game) => $"Playing {game}"; } } From 2cd876b1cbed7544a7edade0f69ae402d066431a Mon Sep 17 00:00:00 2001 From: shinyoyo Date: Sun, 9 Feb 2025 11:33:20 +0800 Subject: [PATCH 11/89] Update Zh-CN Simplified Chinese (#642) --- src/Ryujinx/Assets/locales.json | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 25a40b29e..04ecfe8da 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -1543,7 +1543,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "由 {0} 开发", "zh_TW": "" } }, @@ -1843,7 +1843,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "兼容性:", "zh_TW": "" } }, @@ -1868,7 +1868,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "标题 ID:", "zh_TW": "" } }, @@ -1893,7 +1893,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "服务的游戏: {0}", "zh_TW": "" } }, @@ -1918,7 +1918,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "在线玩家: {0}", "zh_TW": "" } }, @@ -2268,7 +2268,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "清理 PPTC 缓存", "zh_TW": "" } }, @@ -2293,7 +2293,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "删除应用程序的所有 PPTC 缓存", "zh_TW": "" } }, @@ -2768,7 +2768,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "显示兼容性项目", "zh_TW": "" } }, @@ -2793,7 +2793,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "在兼容性列表中显示选定的游戏,您通常可以通过帮助菜单访问。", "zh_TW": "" } }, @@ -2818,7 +2818,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "显示游戏信息", "zh_TW": "" } }, @@ -2843,7 +2843,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "显示当前选定游戏的状态与详细信息。", "zh_TW": "" } }, @@ -4493,7 +4493,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "与系统时间同步", "zh_TW": "" } }, @@ -6143,7 +6143,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "重置设置", "zh_TW": "" } }, @@ -6168,7 +6168,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "我要重置我的设置。", "zh_TW": "" } }, @@ -8143,7 +8143,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "彩虹滚动速度", "zh_TW": "" } }, @@ -13418,7 +13418,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "您正要清理 PPTC 数据:\n\n{0}\n\n您确实要继续吗?", "zh_TW": "" } }, @@ -23568,7 +23568,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "启动和游戏时不会出现任何崩溃或任何类型的 GPU bug 且速度足够快可以在一般 PC 上尽情游玩。", "zh_TW": "" } }, @@ -23593,7 +23593,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "可以成功启动并进入游戏但可能会遇到以下一种或多种问题: 崩溃、卡死、GPU bug、令人无法接受的音频,或者只是太慢。仍然可以继续进行游戏,但是可能无法达到预期。", "zh_TW": "" } }, @@ -23618,7 +23618,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "可以启动并通过标题画面但是无法进入到主要的游戏流程。", "zh_TW": "" } }, @@ -23643,7 +23643,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "可以启动但是无法通过标题画面。", "zh_TW": "" } }, @@ -23668,7 +23668,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "无法启动或显示无任何动静。", "zh_TW": "" } }, @@ -23718,7 +23718,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "Rich Presence 图像", "zh_TW": "" } }, @@ -23743,7 +23743,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "动态 Rich Presence", "zh_TW": "" } } From 05e991db87185b1a2e256c7f05bc9f0560e25ce4 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 9 Feb 2025 00:37:35 -0600 Subject: [PATCH 12/89] misc: chore: collapse all NSO emulator formatters into one method --- .../PlayReport/PlayReports.Formatters.cs | 205 +++++++++--------- .../Utilities/PlayReport/PlayReports.cs | 23 +- 2 files changed, 109 insertions(+), 119 deletions(-) diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs index 3b7699191..f0d7f87ba 100644 --- a/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.Formatters.cs @@ -294,8 +294,67 @@ namespace Ryujinx.Ava.Utilities.PlayReport }; } - private static FormattedValue N64_LaunchedGame(SingleValue value) => value.Matched.StringValue switch + private static FormattedValue NsoEmulator_LaunchedGame(SingleValue value) => value.Matched.StringValue switch { + #region SEGA Genesis + + "m_0054_e" => Playing("Alien Soldier"), + "m_3978_e" => Playing("Alien Storm"), + "m_5234_e" => Playing("ALISIA DRAGOON"), + "m_5003_e" => Playing("Streets of Rage 2"), + "m_4843_e" => Playing("Kid Chameleon"), + "m_2874_e" => Playing("Columns"), + "m_3167_e" => Playing("Comix Zone"), + "m_5007_e" => Playing("Contra: Hard Corps"), + "m_0865_e" => Playing("Ghouls 'n Ghosts"), + "m_0935_e" => Playing("Dynamite Headdy"), + "m_8314_e" => Playing("Earthworm Jim"), + "m_5012_e" => Playing("Ecco the Dolphin"), + "m_2207_e" => Playing("Flicky"), + "m_9432_e" => Playing("Golden Axe II"), + "m_5015_e" => Playing("Golden Axe"), + "m_5017_e" => Playing("Gunstar Heroes"), + "m_0732_e" => Playing("Altered Beast"), + "m_2245_e" or "m_2245_pd" or "m_2245_pf" => Playing("Landstalker"), + "m_1654_e" => Playing("Target Earth"), + "m_7050_e" => Playing("Light Crusader"), + "m_5027_e" => Playing("M.U.S.H.A."), + "m_5028_e" => Playing("Phantasy Star IV"), + "m_9155_e" => Playing("Pulseman"), + "m_5030_e" => Playing("Dr. Robotnik's Mean Bean Machine"), + "m_0098_e" => Playing("Crusader of Centy"), + "m_0098_k" => Playing("신창세기 라그나센티"), + "m_0098_pd" or "m_0098_pf" or "m_0098_ps" => Playing("Soleil"), + "m_5033_e" => Playing("Ristar"), + "m_1987_e" => Playing("MEGA MAN: THE WILY WARS"), + "m_2609_e" => Playing("WOLF OF THE BATTLEFIELD: MERCS"), + "m_3353_e" => Playing("Shining Force II"), + "m_5036_e" => Playing("Shining Force"), + "m_9866_e" => Playing("Sonic The Hedgehog Spinball"), + "m_5041_e" => Playing("Sonic The Hedgehog 2"), + "m_5523_e" => Playing("Space Harrier II"), + "m_0041_e" => Playing("STREET FIGHTER II' : SPECIAL CHAMPION EDITION"), + "m_5044_e" => Playing("STRIDER"), + "m_6353_e" => Playing("Super Fantasy Zone"), + "m_9569_e" => Playing("Beyond Oasis"), + "m_9569_k" => Playing("스토리 오브 도어"), + "m_9569_pd" or "m_9569_ps" => Playing("The Story of Thor"), + "m_9569_pf" => Playing("La Légende de Thor"), + "m_5049_e" => Playing("Shinobi III: Return of the Ninja Master"), + "m_6811_e" => Playing("The Revenge of Shinobi"), + "m_4372_e" => Playing("Thunder Force II"), + "m_1535_e" => Playing("ToeJam & Earl in Panic on Funkotron"), + "m_0432_e" => Playing("ToeJam & Earl"), + "m_5052_e" => Playing("Castlevania: BLOODLINES"), + "m_3626_e" => Playing("VectorMan"), + "m_7955_e" => Playing("Sword of Vermilion"), + "m_0394_e" => Playing("Virtua Fighter 2"), + "m_9417_e" => Playing("Zero Wing"), + + #endregion + + #region Nintendo 64 + "n_1653_e" or "n_1653_p" => Playing("1080º ™ Snowboarding"), "n_4868_e" or "n_4868_p" => Playing("Banjo Kazooie™"), "n_1226_e" or "n_1226_p" => Playing("Banjo-Tooie™"), @@ -331,11 +390,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport "n_3036_e" or "n_3036_p" => Playing("Yoshi's Story™"), "n_1407_e" or "n_1407_p" => Playing("The Legend of Zelda™: Majora's Mask™"), "n_3038_e" or "n_3038_p" => Playing("The Legend of Zelda™: Ocarina of Time™"), - _ => FormattedValue.ForceReset - }; - private static FormattedValue NES_LaunchedGame(SingleValue value) => value.Matched.StringValue switch - { + #endregion + + #region NES + "clv_p_naaae" => Playing("Super Mario Bros.™"), "clv_p_naabe" => Playing("Super Mario Bros.™: The Lost Levels"), "clv_p_naace" or "clv_p_naace_sp1" => Playing("Super Mario Bros.™ 3"), @@ -415,11 +474,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport "clv_p_najre" => Playing("COBRA TRIANGLE™"), "clv_p_najse" => Playing("SNAKE RATTLE N ROLL™"), "clv_p_najte" => Playing("SOLAR® JETMAN"), - _ => FormattedValue.ForceReset - }; - private static FormattedValue SNES_LaunchedGame(SingleValue value) => value.Matched.StringValue switch - { + #endregion + + #region SNES + "s_2180_e" => Playing("BATTLETOADS™ DOUBLE DRAGON™"), "s_2179_e" => Playing("BATTLETOADS™ IN BATTLEMANIACS"), "s_2182_e" => Playing("BIG RUN"), @@ -490,98 +549,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport "s_2096_e" => Playing("Wrecking Crew™ '98"), "s_2023_e" => Playing("Super Mario World™ 2: Yoshi's Island™"), "s_2024_e" => Playing("The Legend of Zelda™: A Link to the Past™"), - _ => FormattedValue.ForceReset - }; - private static FormattedValue Genesis_LaunchedGame(SingleValue value) => value.Matched.StringValue switch - { - "m_0054_e" => Playing("Alien Soldier"), - "m_3978_e" => Playing("Alien Storm"), - "m_5234_e" => Playing("ALISIA DRAGOON"), - "m_5003_e" => Playing("Streets of Rage 2"), - "m_4843_e" => Playing("Kid Chameleon"), - "m_2874_e" => Playing("Columns"), - "m_3167_e" => Playing("Comix Zone"), - "m_5007_e" => Playing("Contra: Hard Corps"), - "m_0865_e" => Playing("Ghouls 'n Ghosts"), - "m_0935_e" => Playing("Dynamite Headdy"), - "m_8314_e" => Playing("Earthworm Jim"), - "m_5012_e" => Playing("Ecco the Dolphin"), - "m_2207_e" => Playing("Flicky"), - "m_9432_e" => Playing("Golden Axe II"), - "m_5015_e" => Playing("Golden Axe"), - "m_5017_e" => Playing("Gunstar Heroes"), - "m_0732_e" => Playing("Altered Beast"), - "m_2245_e" or "m_2245_pd" or "m_2245_pf" => Playing("Landstalker"), - "m_1654_e" => Playing("Target Earth"), - "m_7050_e" => Playing("Light Crusader"), - "m_5027_e" => Playing("M.U.S.H.A."), - "m_5028_e" => Playing("Phantasy Star IV"), - "m_9155_e" => Playing("Pulseman"), - "m_5030_e" => Playing("Dr. Robotnik's Mean Bean Machine"), - "m_0098_e" => Playing("Crusader of Centy"), - "m_0098_k" => Playing("신창세기 라그나센티"), - "m_0098_pd" or "m_0098_pf" or "m_0098_ps" => Playing("Soleil"), - "m_5033_e" => Playing("Ristar"), - "m_1987_e" => Playing("MEGA MAN: THE WILY WARS"), - "m_2609_e" => Playing("WOLF OF THE BATTLEFIELD: MERCS"), - "m_3353_e" => Playing("Shining Force II"), - "m_5036_e" => Playing("Shining Force"), - "m_9866_e" => Playing("Sonic The Hedgehog Spinball"), - "m_5041_e" => Playing("Sonic The Hedgehog 2"), - "m_5523_e" => Playing("Space Harrier II"), - "m_0041_e" => Playing("STREET FIGHTER II' : SPECIAL CHAMPION EDITION"), - "m_5044_e" => Playing("STRIDER"), - "m_6353_e" => Playing("Super Fantasy Zone"), - "m_9569_e" => Playing("Beyond Oasis"), - "m_9569_k" => Playing("스토리 오브 도어"), - "m_9569_pd" or "m_9569_ps" => Playing("The Story of Thor"), - "m_9569_pf" => Playing("La Légende de Thor"), - "m_5049_e" => Playing("Shinobi III: Return of the Ninja Master"), - "m_6811_e" => Playing("The Revenge of Shinobi"), - "m_4372_e" => Playing("Thunder Force II"), - "m_1535_e" => Playing("ToeJam & Earl in Panic on Funkotron"), - "m_0432_e" => Playing("ToeJam & Earl"), - "m_5052_e" => Playing("Castlevania: BLOODLINES"), - "m_3626_e" => Playing("VectorMan"), - "m_7955_e" => Playing("Sword of Vermilion"), - "m_0394_e" => Playing("Virtua Fighter 2"), - "m_9417_e" => Playing("Zero Wing"), - _ => FormattedValue.ForceReset - }; - - private static FormattedValue GBA_LaunchedGame(SingleValue value) => value.Matched.StringValue switch - { - "a_9694_e" => Playing("Densetsu no Starfy 1"), - "a_5600_e" => Playing("Densetsu no Starfy 2"), - "a_7565_e" => Playing("Densetsu no Starfy 3"), - "a_6553_e" => Playing("F-ZERO CLIMAX"), - "a_7842_e" or "a_7842_p" => Playing("F-Zero™- GP Legend"), - "a_9283_e" => Playing("F-Zero™ Maximum Velocity"), - "a_3744_e" or "a_3744_x" or "a_3744_y" => Playing("Fire Emblem™"), - "a_8978_d" or "a_8978_e" or "a_8978_f" or "a_8978_i" or "a_8978_s" => Playing("Golden Sun™: The Lost Age"), - "a_3108_d" or "a_3108_e" or "a_3108_f" or "a_3108_i" or "a_3108_s" => Playing("Golden Sun™"), - "a_3654_e" or "a_3654_p" => Playing("Kirby™ & The Amazing Mirror"), - "a_7279_p" => Playing("Kuru Kuru Kururin™"), - "a_7311_e" or "a_7311_p" => Playing("Mario & Luigi™: Superstar Saga"), - "a_6845_e" => Playing("Mario Kart™: Super Circuit™"), - "a_4139_e" or "a_4139_p" => Playing("Metroid™ Fusion"), - "a_6834_e" or "a_6834_p" => Playing("Metroid™: Zero Mission"), - "a_8989_e" or "a_8989_p" => Playing("Pokémon™ Mystery Dungeon: Red Rescue Team"), - "a_9444_e" => Playing("Super Mario™ Advance"), - "a_9901_e" or "a_9901_p" => Playing("Super Mario™ Advance 4: Super Mario Bros.™ 3"), - "a_2939_e" => Playing("Super Mario World™: Super Mario Advance 2"), - "a_2939_p" => Playing("Super Mario World™: Super Mario Advance 2™"), - "a_1302_e" => Playing("WarioWare™, Inc.: Mega Microgame$!"), - "a_1302_p" => Playing("WarioWare™, Inc.: Minigame Mania."), - "a_6960_e" or "a_6960_p" => Playing("Yoshi's Island™: Super Mario™ Advance 3"), - "a_5190_e" or "a_5190_p" => Playing("The Legend of Zelda™: A Link to the Past™ Four Swords"), - "a_8665_e" or "a_8665_p" => Playing("The Legend of Zelda™: The Minish Cap"), - _ => FormattedValue.ForceReset - }; - - private static FormattedValue GB_LaunchedGame(SingleValue value) => value.Matched.StringValue switch - { + #endregion + + #region GameBoy + "c_7224_e" or "c_7224_p" => Playing("Alone in the Dark: The New Nightmare"), "c_5022_e" => Playing("Blaster Master: Enemy Below"), "c_3381_e" => Playing("Game & Watch™ Gallery 3"), @@ -615,6 +587,39 @@ namespace Ryujinx.Ava.Utilities.PlayReport "d_5124_e" => Playing("Super Mario Land™ 2 - 6 Golden Coins™"), "d_7970_e" => Playing("Super Mario Land™"), "d_8484_e" => Playing("Tetris®"), + + #endregion + + #region GameBoy Advance + + "a_9694_e" => Playing("Densetsu no Starfy 1"), + "a_5600_e" => Playing("Densetsu no Starfy 2"), + "a_7565_e" => Playing("Densetsu no Starfy 3"), + "a_6553_e" => Playing("F-ZERO CLIMAX"), + "a_7842_e" or "a_7842_p" => Playing("F-Zero™- GP Legend"), + "a_9283_e" => Playing("F-Zero™ Maximum Velocity"), + "a_3744_e" or "a_3744_x" or "a_3744_y" => Playing("Fire Emblem™"), + "a_8978_d" or "a_8978_e" or "a_8978_f" or "a_8978_i" or "a_8978_s" => Playing("Golden Sun™: The Lost Age"), + "a_3108_d" or "a_3108_e" or "a_3108_f" or "a_3108_i" or "a_3108_s" => Playing("Golden Sun™"), + "a_3654_e" or "a_3654_p" => Playing("Kirby™ & The Amazing Mirror"), + "a_7279_p" => Playing("Kuru Kuru Kururin™"), + "a_7311_e" or "a_7311_p" => Playing("Mario & Luigi™: Superstar Saga"), + "a_6845_e" => Playing("Mario Kart™: Super Circuit™"), + "a_4139_e" or "a_4139_p" => Playing("Metroid™ Fusion"), + "a_6834_e" or "a_6834_p" => Playing("Metroid™: Zero Mission"), + "a_8989_e" or "a_8989_p" => Playing("Pokémon™ Mystery Dungeon: Red Rescue Team"), + "a_9444_e" => Playing("Super Mario™ Advance"), + "a_9901_e" or "a_9901_p" => Playing("Super Mario™ Advance 4: Super Mario Bros.™ 3"), + "a_2939_e" => Playing("Super Mario World™: Super Mario Advance 2"), + "a_2939_p" => Playing("Super Mario World™: Super Mario Advance 2™"), + "a_1302_e" => Playing("WarioWare™, Inc.: Mega Microgame$!"), + "a_1302_p" => Playing("WarioWare™, Inc.: Minigame Mania."), + "a_6960_e" or "a_6960_p" => Playing("Yoshi's Island™: Super Mario™ Advance 3"), + "a_5190_e" or "a_5190_p" => Playing("The Legend of Zelda™: A Link to the Past™ Four Swords"), + "a_8665_e" or "a_8665_p" => Playing("The Legend of Zelda™: The Minish Cap"), + + #endregion + _ => FormattedValue.ForceReset }; } diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs index e012a92ad..5f6ba3446 100644 --- a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs @@ -62,25 +62,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport ) ) .AddSpec( - "0100c9a00ece6000", - spec => spec.AddValueFormatter("launch_title_id", N64_LaunchedGame) - ) - .AddSpec( - "01008d300c50c000", - spec => spec.AddValueFormatter("launch_title_id", SNES_LaunchedGame) - ) - .AddSpec( - "0100d870045b6000", - spec => spec.AddValueFormatter("launch_title_id", NES_LaunchedGame) - ).AddSpec( - "010012f017576000", - spec => spec.AddValueFormatter("launch_title_id", GBA_LaunchedGame) - ).AddSpec( - "0100c62011050000", - spec => spec.AddValueFormatter("launch_title_id", GB_LaunchedGame) - ).AddSpec( - "0100b3c014bda000", - spec => spec.AddValueFormatter("launch_title_id", Genesis_LaunchedGame) + [ + "0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000", + "010012f017576000", "0100c62011050000", "0100b3c014bda000"], + spec => spec.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame) ); private static string Playing(string game) => $"Playing {game}"; From 764c9e9d4e39753fa5353c5c9fbb48a8dd2bce68 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 9 Feb 2025 00:37:54 -0600 Subject: [PATCH 13/89] misc: chore: unify GameSpec creation --- src/Ryujinx/Utilities/PlayReport/Analyzer.cs | 21 +++++++++++++------- src/Ryujinx/Utilities/PlayReport/Specs.cs | 6 ++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs index 668eb526c..0b5284673 100644 --- a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs +++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs @@ -31,8 +31,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport 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; + return AddSpec(transform(GameSpec.Create(titleId))); } /// @@ -46,8 +45,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport 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; + return AddSpec(GameSpec.Create(titleId).Apply(transform)); } /// @@ -63,8 +61,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport 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; + return AddSpec(transform(GameSpec.Create(tids))); } /// @@ -79,7 +76,17 @@ namespace Ryujinx.Ava.Utilities.PlayReport 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 AddSpec(GameSpec.Create(tids).Apply(transform)); + } + + /// + /// Add an analysis spec matching a specific game by title ID, with the provided pre-configured spec. + /// + /// The to add. + /// The current , for chaining convenience. + public Analyzer AddSpec(GameSpec spec) + { + _specs.Add(spec); return this; } diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs index e7972fbb4..837c37e76 100644 --- a/src/Ryujinx/Utilities/PlayReport/Specs.cs +++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs @@ -14,6 +14,12 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// public class GameSpec { + public static GameSpec Create(string requiredTitleId, params IEnumerable otherTitleIds) + => new() { TitleIds = otherTitleIds.Prepend(requiredTitleId).ToArray() }; + + public static GameSpec Create(IEnumerable titleIds) + => new() { TitleIds = titleIds.ToArray() }; + private int _lastPriority; public required string[] TitleIds { get; init; } From e4b4e94b56e3c122733d67dfb968aa74f3144ab8 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 9 Feb 2025 00:45:46 -0600 Subject: [PATCH 14/89] misc: chore: cleanup Specs.cs --- src/Ryujinx/Utilities/PlayReport/Specs.cs | 87 +++++++++++++---------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs index 837c37e76..be9232dc8 100644 --- a/src/Ryujinx/Utilities/PlayReport/Specs.cs +++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs @@ -16,12 +16,12 @@ namespace Ryujinx.Ava.Utilities.PlayReport { public static GameSpec Create(string requiredTitleId, params IEnumerable otherTitleIds) => new() { TitleIds = otherTitleIds.Prepend(requiredTitleId).ToArray() }; - + public static GameSpec Create(IEnumerable titleIds) => new() { TitleIds = titleIds.ToArray() }; - + private int _lastPriority; - + public required string[] TitleIds { get; init; } public List ValueFormatters { get; } = []; @@ -34,8 +34,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// The key name to match. /// The function which can return a potential formatted value. /// The current , for chaining convenience. - public GameSpec AddValueFormatter(string reportKey, SingleValueFormatter valueFormatter) - => AddValueFormatter(_lastPriority++, reportKey, valueFormatter); + public GameSpec AddValueFormatter( + string reportKey, + SingleValueFormatter valueFormatter + ) => AddValueFormatter(_lastPriority++, reportKey, valueFormatter); /// /// Add a value formatter at a specific priority to the current @@ -45,15 +47,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// 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, - SingleValueFormatter valueFormatter) + public GameSpec AddValueFormatter( + int priority, + string reportKey, + SingleValueFormatter valueFormatter + ) => AddValueFormatter(new FormatterSpec { - ValueFormatters.Add(new FormatterSpec - { - Priority = priority, ReportKeys = [reportKey], Formatter = valueFormatter - }); - return this; - } + Priority = priority, ReportKeys = [reportKey], Formatter = valueFormatter + }); /// /// Add a multi-value formatter to the current @@ -62,8 +63,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// 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(_lastPriority++, reportKeys, valueFormatter); + public GameSpec AddMultiValueFormatter( + string[] reportKeys, + MultiValueFormatter valueFormatter + ) => AddMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter); /// /// Add a multi-value formatter at a specific priority to the current @@ -73,15 +76,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// 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) + public GameSpec AddMultiValueFormatter( + int priority, + string[] reportKeys, + MultiValueFormatter valueFormatter + ) => AddValueFormatter(new MultiFormatterSpec { - ValueFormatters.Add(new MultiFormatterSpec - { - Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter - }); - return this; - } + Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter + }); /// /// Add a multi-value formatter to the current @@ -93,8 +95,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// 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(_lastPriority++, reportKeys, valueFormatter); + public GameSpec AddSparseMultiValueFormatter( + string[] reportKeys, + SparseMultiValueFormatter valueFormatter + ) => AddSparseMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter); /// /// Add a multi-value formatter at a specific priority to the current @@ -107,13 +111,18 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// 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) + public GameSpec AddSparseMultiValueFormatter( + int priority, + string[] reportKeys, + SparseMultiValueFormatter valueFormatter + ) => AddValueFormatter(new SparseMultiFormatterSpec { - ValueFormatters.Add(new SparseMultiFormatterSpec - { - Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter - }); + Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter + }); + + private GameSpec AddValueFormatter(T formatterSpec) where T : FormatterSpecBase + { + ValueFormatters.Add(formatterSpec); return this; } } @@ -180,16 +189,17 @@ namespace Ryujinx.Ava.Utilities.PlayReport return true; } } - + public abstract class FormatterSpecBase { public abstract bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object data); - + public int Priority { get; init; } public string[] ReportKeys { get; init; } public Delegate Formatter { get; init; } - public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport, out FormattedValue formattedValue) + public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport, + out FormattedValue formattedValue) { formattedValue = default; if (!GetData(playReport, out object data)) @@ -207,11 +217,16 @@ namespace Ryujinx.Ava.Utilities.PlayReport formattedValue = svf(new SingleValue(mpo) { Application = appMeta, PlayReport = playReport }); return true; case MultiValueFormatter mvf when data is List messagePackObjects: - formattedValue = mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport }); + formattedValue = + mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport }); return true; case SparseMultiValueFormatter smvf when data is Dictionary sparseMessagePackObjects: - formattedValue = smvf(new SparseMultiValue(sparseMessagePackObjects) { Application = appMeta, PlayReport = playReport }); + formattedValue = + smvf(new SparseMultiValue(sparseMessagePackObjects) + { + Application = appMeta, PlayReport = playReport + }); return true; default: throw new InvalidOperationException("Formatter delegate is not of a known type!"); From 1c0813d09d99ebe39ef0a47557304fc8470fb859 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 9 Feb 2025 00:50:11 -0600 Subject: [PATCH 15/89] misc: chore: [ci skip] shorten lines in FormatterSpecBase.Format & consistently format them --- src/Ryujinx/Utilities/PlayReport/Specs.cs | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs index be9232dc8..b81a599a2 100644 --- a/src/Ryujinx/Utilities/PlayReport/Specs.cs +++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs @@ -213,20 +213,20 @@ namespace Ryujinx.Ava.Utilities.PlayReport switch (Formatter) { - case SingleValueFormatter svf when data is MessagePackObject mpo: - formattedValue = svf(new SingleValue(mpo) { Application = appMeta, PlayReport = playReport }); + case SingleValueFormatter svf when data is MessagePackObject match: + formattedValue = svf( + new SingleValue(match) { Application = appMeta, PlayReport = playReport } + ); return true; - case MultiValueFormatter mvf when data is List messagePackObjects: - formattedValue = - mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport }); + case MultiValueFormatter mvf when data is List matches: + formattedValue = mvf( + new MultiValue(matches) { Application = appMeta, PlayReport = playReport } + ); return true; - case SparseMultiValueFormatter smvf when - data is Dictionary sparseMessagePackObjects: - formattedValue = - smvf(new SparseMultiValue(sparseMessagePackObjects) - { - Application = appMeta, PlayReport = playReport - }); + case SparseMultiValueFormatter smvf when data is Dictionary sparseMatches: + formattedValue = smvf( + new SparseMultiValue(sparseMatches) { Application = appMeta, PlayReport = playReport } + ); return true; default: throw new InvalidOperationException("Formatter delegate is not of a known type!"); From efa0cc7554d781eb943eba8c6b221993db7c27ca Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 9 Feb 2025 21:15:38 -0600 Subject: [PATCH 16/89] UI: Show issue labels of games in the tooltip on playability status --- .../UI/Controls/ApplicationDataView.axaml | 16 +++- .../UI/Controls/ApplicationListView.axaml | 15 +++- .../Utilities/AppLibrary/ApplicationData.cs | 74 +++++++++++++------ .../Utilities/Compat/CompatibilityCsv.cs | 13 +++- 4 files changed, 90 insertions(+), 28 deletions(-) diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml index c40b6e192..aee8f7b36 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml @@ -55,9 +55,21 @@ Tag="{Binding AppData.IdString}" Text="{Binding AppData.LocalizedStatus}" Foreground="{Binding AppData.PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}" - ToolTip.Tip="{Binding AppData.LocalizedStatusTooltip}" TextAlignment="Start" - TextWrapping="Wrap" /> + TextWrapping="Wrap"> + + + + + + + +