From b38b5a1e709c797b3e409c8d34775284f35bff96 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 16:59:06 -0600 Subject: [PATCH 1/7] docs: compat: Saints Row IV: Playable -> Ingame Deadlock label added. Game sometimes just stops loading in loading screens. Game continues like its doing something but you'll be sitting there for minutes wondering why nothing is happening. Considering the game isn't crashing, this might be an emulator-side mutex issue. I've seen that before. --- docs/compatibility.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compatibility.csv b/docs/compatibility.csv index 570c93618..53ad389b6 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -2483,7 +2483,7 @@ 0100A5200C2E0000,"Safety First!",,playable,2021-01-06 09:05:23 0100A51013530000,"SaGa Frontier Remastered",nvdec,playable,2022-11-03 13:54:56 010003A00D0B4000,"SaGa SCARLET GRACE: AMBITIONS™",,playable,2022-10-06 13:20:31 -01008D100D43E000,"Saints Row IV®: Re-Elected™",ldn-untested;LAN,playable,2023-12-04 18:33:37 +01008D100D43E000,"Saints Row IV®: Re-Elected™",ldn-untested;LAN;deadlock,ingame,2025-02-02 16:57:53 0100DE600BEEE000,"SAINTS ROW®: THE THIRD™ - THE FULL PACKAGE",slow;LAN,playable,2023-08-24 02:40:58 01007F000EB36000,"Sakai and...",nvdec,playable,2022-12-15 13:53:19 0100B1400E8FE000,"Sakuna: Of Rice and Ruin",,playable,2023-07-24 13:47:13 From bf713a80d66c14dc2b045fced8d1cfafbce91875 Mon Sep 17 00:00:00 2001 From: Piplup <100526773+piplup55@users.noreply.github.com> Date: Mon, 3 Feb 2025 02:29:00 +0000 Subject: [PATCH 2/7] PlayReportAnalyzer: Added Games (#614) Added Super Mario Odyssey, Super Mario Odyssey (China), Super Mario 3D World + Bowser's Fury, Mario Kart 8 Deluxe and Mario Kart 8 Deluxe (China) --- src/Ryujinx/DiscordIntegrationModule.cs | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index add46bda4..7f48089b9 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -82,6 +82,36 @@ namespace Ryujinx.Ava } } + public static string MarioKart8(object obj) + { + return obj 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", + _ => "Playing Mario Kart 8 Deluxe" + }; + } + public static void Use(Optional titleId) { if (titleId.TryGet(out string tid)) @@ -131,6 +161,31 @@ namespace Ryujinx.Ava "01007ef00011e000", gameSpec => gameSpec.AddValueFormatter("IsHardMode", val => val is 1 ? "Playing Master Mode" : "Playing Normal Mode") + ) + .AddSpec( // Super Mario Odyssey + "0100000000010000", + gameSpec => + gameSpec.AddValueFormatter("is_kids_mode", val => val is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode") + ) + .AddSpec( // Super Mario Odyssey (China) + "010075000ECBE000", + gameSpec => + gameSpec.AddValueFormatter("is_kids_mode", val => val is 1 ? "Playing in 帮助模式" : "Playing in 普通模式") + ) + .AddSpec( // Super Mario 3D World + Bowser's Fury + "010028600EBDA000", + gameSpec => + gameSpec.AddValueFormatter("mode", val => val is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury") + ) + .AddSpec( // Mario Kart 8 Deluxe + "0100152000022000", + gameSpec => + gameSpec.AddValueFormatter("To", MarioKart8) + ) + .AddSpec( // Mario Kart 8 Deluxe (China) + "010075100E8EC000", + gameSpec => + gameSpec.AddValueFormatter("To", MarioKart8) ); private static void HandlePlayReport(MessagePackObject playReport) From 8117e160c2e82ffacea312bb4b9c41171b4396d5 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 20:32:01 -0600 Subject: [PATCH 3/7] misc: chore: [ci skip] Move the play report analyzer definition into a PlayReport static class to avoid polluting the Discord integration module --- src/Ryujinx/DiscordIntegrationModule.cs | 64 +-------------------- src/Ryujinx/Utilities/PlayReport.cs | 76 +++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 src/Ryujinx/Utilities/PlayReport.cs diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 7f48089b9..c9fa1f732 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -82,36 +82,6 @@ namespace Ryujinx.Ava } } - public static string MarioKart8(object obj) - { - return obj 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", - _ => "Playing Mario Kart 8 Deluxe" - }; - } - public static void Use(Optional titleId) { if (titleId.TryGet(out string tid)) @@ -155,45 +125,13 @@ namespace Ryujinx.Ava _discordClient?.SetPresence(_discordPresenceMain); _discordPresencePlaying = null; } - - private static readonly PlayReportAnalyzer _playReportAnalyzer = new PlayReportAnalyzer() - .AddSpec( // Breath of the Wild - "01007ef00011e000", - gameSpec => - gameSpec.AddValueFormatter("IsHardMode", val => val is 1 ? "Playing Master Mode" : "Playing Normal Mode") - ) - .AddSpec( // Super Mario Odyssey - "0100000000010000", - gameSpec => - gameSpec.AddValueFormatter("is_kids_mode", val => val is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode") - ) - .AddSpec( // Super Mario Odyssey (China) - "010075000ECBE000", - gameSpec => - gameSpec.AddValueFormatter("is_kids_mode", val => val is 1 ? "Playing in 帮助模式" : "Playing in 普通模式") - ) - .AddSpec( // Super Mario 3D World + Bowser's Fury - "010028600EBDA000", - gameSpec => - gameSpec.AddValueFormatter("mode", val => val is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury") - ) - .AddSpec( // Mario Kart 8 Deluxe - "0100152000022000", - gameSpec => - gameSpec.AddValueFormatter("To", MarioKart8) - ) - .AddSpec( // Mario Kart 8 Deluxe (China) - "010075100E8EC000", - gameSpec => - gameSpec.AddValueFormatter("To", MarioKart8) - ); private static void HandlePlayReport(MessagePackObject playReport) { if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - Optional details = _playReportAnalyzer.Run(TitleIDs.CurrentApplication.Value, playReport); + Optional details = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, playReport); if (!details.HasValue) return; diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs new file mode 100644 index 000000000..e913ffa13 --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport.cs @@ -0,0 +1,76 @@ +using Ryujinx.Common.Helper; + +namespace Ryujinx.Ava.Utilities +{ + public static class PlayReport + { + public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer() + .AddSpec( + "01007ef00011e000", + spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) + ) + .AddSpec( // Super Mario Odyssey + "0100000000010000", + spec => + spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode) + ) + .AddSpec( // Super Mario Odyssey (China) + "010075000ECBE000", + spec => + spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode) + ) + .AddSpec( // Super Mario 3D World + Bowser's Fury + "010028600EBDA000", + spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) + ) + .AddSpec( // Mario Kart 8 Deluxe + "0100152000022000", + spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) + ) + .AddSpec( // Mario Kart 8 Deluxe (China) + "010075100E8EC000", + spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) + ); + + private static string BreathOfTheWild_MasterMode(object val) + => val is 1 ? "Playing Master Mode" : "Playing Normal Mode"; + + private static string SuperMarioOdyssey_AssistMode(object val) + => val is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; + + private static string SuperMarioOdysseyChina_AssistMode(object val) + => val is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; + + private static string SuperMario3DWorldOrBowsersFury(object val) + => val is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; + + private static string MarioKart8Deluxe_Mode(object obj) + => obj 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", + //TODO: refactor value formatting system to pass in the name from the content archive so this can be localized properly + _ => "Playing Mario Kart 8 Deluxe" + }; + } +} From fe43c32e60008fa39dd6ab6216de8df97d13f98e Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 20:47:42 -0600 Subject: [PATCH 4/7] UI: The argument to Play Report value formatters is now a struct containing the current ApplicationMetadata & the BoxedValue that was the only argument previously. This allows for the title of Mario Kart to be localized when one of the value checkers doesn't match. --- .../Helpers/PlayReportAnalyzer.cs | 80 ------------ src/Ryujinx/DiscordIntegrationModule.cs | 5 +- src/Ryujinx/Utilities/PlayReport.cs | 121 ++++++++++++++++-- 3 files changed, 112 insertions(+), 94 deletions(-) delete mode 100644 src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs diff --git a/src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs b/src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs deleted file mode 100644 index b69b18f57..000000000 --- a/src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Gommon; -using MsgPack; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Ryujinx.Common.Helper -{ - public class PlayReportAnalyzer - { - private readonly List _specs = []; - - public PlayReportAnalyzer AddSpec(string titleId, Func transform) - { - _specs.Add(transform(new PlayReportGameSpec { TitleIdStr = titleId })); - return this; - } - - public PlayReportAnalyzer AddSpec(string titleId, Action transform) - { - _specs.Add(new PlayReportGameSpec { TitleIdStr = titleId }.Apply(transform)); - return this; - } - - public Optional Run(string runningGameId, MessagePackObject playReport) - { - if (!playReport.IsDictionary) - return Optional.None; - - if (!_specs.TryGetFirst(s => s.TitleIdStr.EqualsIgnoreCase(runningGameId), out PlayReportGameSpec spec)) - return Optional.None; - - foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority)) - { - if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) - continue; - - return formatSpec.ValueFormatter(valuePackObject.ToObject()); - } - - return Optional.None; - } - - } - - public class PlayReportGameSpec - { - public required string TitleIdStr { get; init; } - public List Analyses { get; } = []; - - public PlayReportGameSpec AddValueFormatter(string reportKey, Func valueFormatter) - { - Analyses.Add(new PlayReportValueFormatterSpec - { - Priority = Analyses.Count, - ReportKey = reportKey, - ValueFormatter = valueFormatter - }); - return this; - } - - public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, Func valueFormatter) - { - Analyses.Add(new PlayReportValueFormatterSpec - { - Priority = priority, - ReportKey = reportKey, - ValueFormatter = valueFormatter - }); - return this; - } - } - - public struct PlayReportValueFormatterSpec - { - public required int Priority { get; init; } - public required string ReportKey { get; init; } - public required Func ValueFormatter { get; init; } - } -} diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index c9fa1f732..5561c1562 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -39,6 +39,7 @@ namespace Ryujinx.Ava private static DiscordRpcClient _discordClient; private static RichPresence _discordPresenceMain; private static RichPresence _discordPresencePlaying; + private static ApplicationMetadata _currentApp; public static void Initialize() { @@ -113,6 +114,7 @@ namespace Ryujinx.Ava private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes) { _discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes)); + _currentApp = appMeta; } private static void UpdatePlayingState() @@ -124,6 +126,7 @@ namespace Ryujinx.Ava { _discordClient?.SetPresence(_discordPresenceMain); _discordPresencePlaying = null; + _currentApp = null; } private static void HandlePlayReport(MessagePackObject playReport) @@ -131,7 +134,7 @@ namespace Ryujinx.Ava if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - Optional details = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, playReport); + Optional details = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport); if (!details.HasValue) return; diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs index e913ffa13..9665a1628 100644 --- a/src/Ryujinx/Utilities/PlayReport.cs +++ b/src/Ryujinx/Utilities/PlayReport.cs @@ -1,4 +1,10 @@ -using Ryujinx.Common.Helper; +using Gommon; +using MsgPack; +using Ryujinx.Ava.Utilities.AppLibrary; +using Ryujinx.Common.Helper; +using System; +using System.Collections.Generic; +using System.Linq; namespace Ryujinx.Ava.Utilities { @@ -32,20 +38,20 @@ namespace Ryujinx.Ava.Utilities spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) ); - private static string BreathOfTheWild_MasterMode(object val) - => val is 1 ? "Playing Master Mode" : "Playing Normal Mode"; + private static string BreathOfTheWild_MasterMode(ref PlayReportValue value) + => value.BoxedValue is 1 ? "Playing Master Mode" : "Playing Normal Mode"; - private static string SuperMarioOdyssey_AssistMode(object val) - => val is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; + private static string SuperMarioOdyssey_AssistMode(ref PlayReportValue value) + => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; - private static string SuperMarioOdysseyChina_AssistMode(object val) - => val is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; + private static string SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value) + => value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; - private static string SuperMario3DWorldOrBowsersFury(object val) - => val is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; + private static string SuperMario3DWorldOrBowsersFury(ref PlayReportValue value) + => value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; - private static string MarioKart8Deluxe_Mode(object obj) - => obj switch + private static string MarioKart8Deluxe_Mode(ref PlayReportValue value) + => value.BoxedValue switch { // Single Player "Single" => "Single Player", @@ -69,8 +75,97 @@ namespace Ryujinx.Ava.Utilities "Battle" => "Battle Mode", "RaceStart" => "Selecting a Course", "Race" => "Racing", - //TODO: refactor value formatting system to pass in the name from the content archive so this can be localized properly - _ => "Playing Mario Kart 8 Deluxe" + _ => $"Playing {value.Application.Title}" }; } + + #region Analyzer implementation + + public class PlayReportAnalyzer + { + private readonly List _specs = []; + + public PlayReportAnalyzer AddSpec(string titleId, Func transform) + { + _specs.Add(transform(new PlayReportGameSpec { TitleIdStr = titleId })); + return this; + } + + public PlayReportAnalyzer AddSpec(string titleId, Action transform) + { + _specs.Add(new PlayReportGameSpec { TitleIdStr = titleId }.Apply(transform)); + return this; + } + + public Optional Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport) + { + if (!playReport.IsDictionary) + return Optional.None; + + if (!_specs.TryGetFirst(s => s.TitleIdStr.EqualsIgnoreCase(runningGameId), out PlayReportGameSpec spec)) + return Optional.None; + + foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority)) + { + if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) + continue; + + PlayReportValue value = new() + { + Application = appMeta, + BoxedValue = valuePackObject.ToObject() + }; + + return formatSpec.ValueFormatter(ref value); + } + + return Optional.None; + } + + } + + public class PlayReportGameSpec + { + public required string TitleIdStr { get; init; } + public List Analyses { get; } = []; + + public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter) + { + Analyses.Add(new PlayReportValueFormatterSpec + { + Priority = Analyses.Count, + ReportKey = reportKey, + ValueFormatter = valueFormatter + }); + return this; + } + + public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, PlayReportValueFormatter valueFormatter) + { + Analyses.Add(new PlayReportValueFormatterSpec + { + Priority = priority, + ReportKey = reportKey, + ValueFormatter = valueFormatter + }); + return this; + } + } + + public struct PlayReportValue + { + public ApplicationMetadata Application { get; init; } + public object BoxedValue { get; init; } + } + + public struct PlayReportValueFormatterSpec + { + public required int Priority { get; init; } + public required string ReportKey { get; init; } + public required PlayReportValueFormatter ValueFormatter { get; init; } + } + + public delegate string PlayReportValueFormatter(ref PlayReportValue value); + + #endregion } From b2eecd28cea1d50dfc18e2cbfac0db80f4c11678 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 22:10:49 -0600 Subject: [PATCH 5/7] UI: RPC: Value Formatter V3 - Allows the ability to bind a single PlayReportGameSpec to multiple title IDs, like for MK8D - Allows the ability for the value formatters to tell the caller of the analyzer that they should reset the value, and also added the ability to explicitly not handle a value format. --- src/Ryujinx/DiscordIntegrationModule.cs | 18 ++++-- src/Ryujinx/Utilities/PlayReport.cs | 75 +++++++++++++++++-------- 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 5561c1562..0e1f91869 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -134,13 +134,21 @@ namespace Ryujinx.Ava if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - Optional details = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport); + PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport); - if (!details.HasValue) return; - - _discordPresencePlaying.Details = details; + if (!value.Handled) return; + + if (value.Reset) + { + _discordPresencePlaying.Details = $"Playing {_currentApp.Title}"; + Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter."); + } + else + { + _discordPresencePlaying.Details = value.FormattedString; + Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); + } UpdatePlayingState(); - Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); } private static string TruncateToByteLength(string input) diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs index 9665a1628..af56bae12 100644 --- a/src/Ryujinx/Utilities/PlayReport.cs +++ b/src/Ryujinx/Utilities/PlayReport.cs @@ -29,28 +29,24 @@ namespace Ryujinx.Ava.Utilities "010028600EBDA000", spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) ) - .AddSpec( // Mario Kart 8 Deluxe - "0100152000022000", - spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) - ) - .AddSpec( // Mario Kart 8 Deluxe (China) - "010075100E8EC000", + .AddSpec( // Mario Kart 8 Deluxe, Mario Kart 8 Deluxe (China) + ["0100152000022000", "010075100E8EC000"], spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) ); - private static string BreathOfTheWild_MasterMode(ref PlayReportValue value) - => value.BoxedValue is 1 ? "Playing Master Mode" : "Playing Normal Mode"; + private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value) + => value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset; - private static string SuperMarioOdyssey_AssistMode(ref PlayReportValue value) + private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value) => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; - private static string SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value) + private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value) => value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; - private static string SuperMario3DWorldOrBowsersFury(ref PlayReportValue value) + private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref PlayReportValue value) => value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; - private static string MarioKart8Deluxe_Mode(ref PlayReportValue value) + private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref PlayReportValue value) => value.BoxedValue switch { // Single Player @@ -75,7 +71,7 @@ namespace Ryujinx.Ava.Utilities "Battle" => "Battle Mode", "RaceStart" => "Selecting a Course", "Race" => "Racing", - _ => $"Playing {value.Application.Title}" + _ => PlayReportFormattedValue.ForceReset }; } @@ -87,23 +83,35 @@ namespace Ryujinx.Ava.Utilities public PlayReportAnalyzer AddSpec(string titleId, Func transform) { - _specs.Add(transform(new PlayReportGameSpec { TitleIdStr = titleId })); + _specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] })); return this; } public PlayReportAnalyzer AddSpec(string titleId, Action transform) { - _specs.Add(new PlayReportGameSpec { TitleIdStr = titleId }.Apply(transform)); + _specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform)); + return this; + } + + public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Func transform) + { + _specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] })); + return this; + } + + public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Action transform) + { + _specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform)); return this; } - public Optional Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport) + public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport) { if (!playReport.IsDictionary) - return Optional.None; + return PlayReportFormattedValue.Unhandled; - if (!_specs.TryGetFirst(s => s.TitleIdStr.EqualsIgnoreCase(runningGameId), out PlayReportGameSpec spec)) - return Optional.None; + if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec)) + return PlayReportFormattedValue.Unhandled; foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority)) { @@ -119,14 +127,14 @@ namespace Ryujinx.Ava.Utilities return formatSpec.ValueFormatter(ref value); } - return Optional.None; + return PlayReportFormattedValue.Unhandled; } } public class PlayReportGameSpec { - public required string TitleIdStr { get; init; } + public required string[] TitleIds { get; init; } public List Analyses { get; } = []; public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter) @@ -158,14 +166,33 @@ namespace Ryujinx.Ava.Utilities public object BoxedValue { get; init; } } + public struct PlayReportFormattedValue + { + public bool Handled { get; private init; } + + public bool Reset { get; private init; } + + public string FormattedString { get; private init; } + + public static implicit operator PlayReportFormattedValue(string formattedValue) + => new() { Handled = true, FormattedString = formattedValue }; + + public static PlayReportFormattedValue Unhandled => default; + public static PlayReportFormattedValue ForceReset => new() { Handled = true, Reset = true }; + + public static PlayReportValueFormatter AlwaysResets = AlwaysResetsImpl; + + private static PlayReportFormattedValue AlwaysResetsImpl(ref PlayReportValue _) => ForceReset; + } + public struct PlayReportValueFormatterSpec { public required int Priority { get; init; } public required string ReportKey { get; init; } - public required PlayReportValueFormatter ValueFormatter { get; init; } + public PlayReportValueFormatter ValueFormatter { get; init; } } - public delegate string PlayReportValueFormatter(ref PlayReportValue value); - + public delegate PlayReportFormattedValue PlayReportValueFormatter(ref PlayReportValue value); + #endregion } From 55536f5d7815e82aa2032bf63878497d87c7e70c Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 22:14:43 -0600 Subject: [PATCH 6/7] misc: chore: Early exit HandlePlayReport if RPC is not enabled --- src/Ryujinx/DiscordIntegrationModule.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 0e1f91869..f55eb8d66 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -131,6 +131,7 @@ namespace Ryujinx.Ava private static void HandlePlayReport(MessagePackObject playReport) { + if (_discordClient is null) return; if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; From 774edb7b29bf8ba44b0cea331977b37017ac6499 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Feb 2025 23:46:55 -0600 Subject: [PATCH 7/7] UI: Match System Time is now an active setting which you can toggle on/off. --- src/Ryujinx/AppHost.cs | 4 +- src/Ryujinx/Assets/locales.json | 40 +++++++++---------- .../UI/ViewModels/SettingsViewModel.cs | 24 ++++------- .../UI/Views/Settings/SettingsCPUView.axaml | 3 +- .../Views/Settings/SettingsGraphicsView.axaml | 3 +- .../Views/Settings/SettingsSystemView.axaml | 21 ++++++---- .../Settings/SettingsSystemView.axaml.cs | 2 - src/Ryujinx/UI/Windows/SettingsWindow.axaml | 3 +- .../Configuration/ConfigurationFileFormat.cs | 7 +++- .../ConfigurationState.Migration.cs | 3 +- .../Configuration/ConfigurationState.Model.cs | 7 ++++ 11 files changed, 64 insertions(+), 53 deletions(-) diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index d12795963..25f451858 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -938,7 +938,9 @@ namespace Ryujinx.Ava ConfigurationState.Instance.System.EnableInternetAccess, ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None, ConfigurationState.Instance.System.FsGlobalAccessLogMode, - ConfigurationState.Instance.System.SystemTimeOffset, + ConfigurationState.Instance.System.MatchSystemTime + ? 0 + : ConfigurationState.Instance.System.SystemTimeOffset, ConfigurationState.Instance.System.TimeZone, ConfigurationState.Instance.System.MemoryManagerMode, ConfigurationState.Instance.System.IgnoreMissingServices, diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index ec8a2aaac..3db41b963 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -4153,23 +4153,23 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Resync to PC Date & Time", + "en_US": "Match System Time", "es_ES": "", - "fr_FR": "Resynchronier la Date à celle du PC", + "fr_FR": "", "he_IL": "", - "it_IT": "Sincronizza data e ora con il PC", + "it_IT": "", "ja_JP": "", - "ko_KR": "PC 날짜와 시간에 동기화", - "no_NO": "Resynkroniser til PC-dato og -klokkeslett", + "ko_KR": "", + "no_NO": "", "pl_PL": "", "pt_BR": "", - "ru_RU": "Повторная синхронизация с датой и временем на компьютере", - "sv_SE": "Återsynka till datorns datum och tid", + "ru_RU": "", + "sv_SE": "", "th_TH": "", "tr_TR": "", - "uk_UA": "Синхронізувати з датою та часом ПК", - "zh_CN": "与 PC 日期和时间重新同步", - "zh_TW": "重新同步至 PC 的日期和時間" + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" } }, { @@ -15553,23 +15553,23 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Resync System Time to match your PC's current date & time.\n\nThis is not an active setting, it can still fall out of sync; in which case just click this button again.", + "en_US": "Sync System Time to match your PC's current date & time.", "es_ES": "", - "fr_FR": "Resynchronise la Date du Système pour qu'elle soit la même que celle du PC.\n\nCeci n'est pas un paramètrage automatique, la date peut se désynchroniser; dans ce cas là, rappuyer sur le boutton.", + "fr_FR": "Resynchronise la Date du Système pour qu'elle soit la même que celle du PC.", "he_IL": "", - "it_IT": "Sincronizza data e ora del sistema con quelle del PC.\n\nQuesta non è un'opzione attiva, perciò data e ora potrebbero tornare a non essere sincronizzate: in tal caso basterà cliccare nuovamente questo pulsante.", + "it_IT": "Sincronizza data e ora del sistema con quelle del PC.", "ja_JP": "", - "ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.\n\n이 설정은 활성 설정이 아니므로 여전히 동기화되지 않을 수 있으며, 이 경우 이 버튼을 다시 클릭하면 됩니다.", - "no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett. \\Dette er ikke en aktiv innstilling, men den kan likevel komme ut av synkronisering; i så fall er det bare å klikke på denne knappen igjen.", + "ko_KR": "시스템 시간을 PC의 현재 날짜 및 시간과 일치하도록 다시 동기화합니다.", + "no_NO": "Resynkroniser systemtiden slik at den samsvarer med PC-ens gjeldende dato og klokkeslett.", "pl_PL": "", "pt_BR": "", - "ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.\n\nЭто не активная настройка, она все еще может рассинхронизироваться; в этом случае просто нажмите эту кнопку еще раз.", - "sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.\n\nDetta är inte en aktiv inställning och den kan tappa synken och om det händer så kan du klicka på denna knapp igen.", + "ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.", + "sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.", "th_TH": "", "tr_TR": "", - "uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.\n\nЦе не активне налаштування, тому синхронізація може збитися; у такому разі просто натискайте цю кнопку знову.", - "zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。\n\n这个操作不会实时同步系统时间与电脑时间,时间仍然可能不同步;在这种情况下,只需再次单击此按钮即可。", - "zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。\n\n這不是一個主動設定,它仍然可能會失去同步;在這種情況下,只需再次點擊此按鈕。" + "uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.", + "zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。", + "zh_TW": "重新同步系統韌體時間至 PC 目前的日期和時間。" } }, { diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 488828482..d54313e76 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -116,10 +116,6 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS(); - public bool IsAppleSiliconMac => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; - - public bool IsMacOS => OperatingSystem.IsMacOS(); - public bool EnableDiscordIntegration { get; set; } public bool CheckUpdatesOnStart { get; set; } public bool ShowConfirmExit { get; set; } @@ -201,7 +197,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableTextureRecompression { get; set; } public bool EnableMacroHLE { get; set; } public bool EnableColorSpacePassthrough { get; set; } - public bool ColorSpacePassthroughAvailable => IsMacOS; + public bool ColorSpacePassthroughAvailable => RunningPlatform.IsMacOS; public bool EnableFileLog { get; set; } public bool EnableStub { get; set; } public bool EnableInfo { get; set; } @@ -297,6 +293,8 @@ namespace Ryujinx.Ava.UI.ViewModels } } + [ObservableProperty] private bool _matchSystemTime; + public DateTimeOffset CurrentDate { get; set; } public TimeSpan CurrentTime { get; set; } @@ -412,17 +410,6 @@ namespace Ryujinx.Ava.UI.ViewModels Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(PreferredGpuIndex))); } - public void MatchSystemTime() - { - (DateTimeOffset dto, TimeSpan timeOfDay) = DateTimeOffset.Now.Extract(); - - CurrentDate = dto; - CurrentTime = timeOfDay; - - OnPropertyChanged(nameof(CurrentDate)); - OnPropertyChanged(nameof(CurrentTime)); - } - public async Task LoadTimeZones() { _timeZoneContentManager = new TimeZoneContentManager(); @@ -524,7 +511,9 @@ namespace Ryujinx.Ava.UI.ViewModels CurrentDate = currentDateTime.Date; CurrentTime = currentDateTime.TimeOfDay; - EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval.Value; + MatchSystemTime = config.System.MatchSystemTime; + + EnableCustomVSyncInterval = config.Graphics.EnableCustomVSyncInterval; CustomVSyncInterval = config.Graphics.CustomVSyncInterval; VSyncMode = config.Graphics.VSyncMode; EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; @@ -629,6 +618,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.System.TimeZone.Value = TimeZone; } + config.System.MatchSystemTime.Value = MatchSystemTime; config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; config.System.DramSize.Value = DramSize; diff --git a/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml index 83f908a9c..62f087510 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml @@ -6,6 +6,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common" mc:Ignorable="d" x:DataType="viewModels:SettingsViewModel"> @@ -69,7 +70,7 @@ diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml index a2559f393..42515a4e9 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml @@ -8,6 +8,7 @@ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common" Design.Width="1000" mc:Ignorable="d" x:DataType="viewModels:SettingsViewModel"> @@ -48,7 +49,7 @@ - + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml index d05f3b7bf..aa7144cf1 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml @@ -170,7 +170,8 @@ ToolTip.Tip="{ext:Locale TimeTooltip}" Width="250"/> @@ -181,17 +182,21 @@ - + Text="{ext:Locale SettingsTabSystemSystemTimeMatch}" + ToolTip.Tip="{ext:Locale MatchTimeTooltip}" + Width="250"/> + ViewModel.MatchSystemTime(); } } diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml index 59302b6fc..7abf044ee 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -10,6 +10,7 @@ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" + xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common" Width="1100" Height="768" MinWidth="800" @@ -113,7 +114,7 @@ Spacing="10" Orientation="Horizontal" HorizontalAlignment="Right" - ReverseOrder="{Binding IsMacOS}"> + ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">