Merge branch 'master' into master
This commit is contained in:
commit
4efd20c54b
@ -2483,7 +2483,7 @@
|
|||||||
0100A5200C2E0000,"Safety First!",,playable,2021-01-06 09:05:23
|
0100A5200C2E0000,"Safety First!",,playable,2021-01-06 09:05:23
|
||||||
0100A51013530000,"SaGa Frontier Remastered",nvdec,playable,2022-11-03 13:54:56
|
0100A51013530000,"SaGa Frontier Remastered",nvdec,playable,2022-11-03 13:54:56
|
||||||
010003A00D0B4000,"SaGa SCARLET GRACE: AMBITIONS™",,playable,2022-10-06 13:20:31
|
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
|
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
|
01007F000EB36000,"Sakai and...",nvdec,playable,2022-12-15 13:53:19
|
||||||
0100B1400E8FE000,"Sakuna: Of Rice and Ruin",,playable,2023-07-24 13:47:13
|
0100B1400E8FE000,"Sakuna: Of Rice and Ruin",,playable,2023-07-24 13:47:13
|
||||||
|
|
@ -7,7 +7,7 @@ namespace Ryujinx.Horizon
|
|||||||
{
|
{
|
||||||
public static class HorizonStatic
|
public static class HorizonStatic
|
||||||
{
|
{
|
||||||
internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted.Invoke(report);
|
internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted?.Invoke(report);
|
||||||
|
|
||||||
public static event Action<MessagePackObject> PlayReportPrinted;
|
public static event Action<MessagePackObject> PlayReportPrinted;
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ using Ryujinx.Ava.Utilities;
|
|||||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
using Ryujinx.Ava.Utilities.Configuration;
|
using Ryujinx.Ava.Utilities.Configuration;
|
||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
|
using Ryujinx.Common.Helper;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
using Ryujinx.HLE.Loaders.Processes;
|
using Ryujinx.HLE.Loaders.Processes;
|
||||||
@ -38,6 +39,7 @@ namespace Ryujinx.Ava
|
|||||||
private static DiscordRpcClient _discordClient;
|
private static DiscordRpcClient _discordClient;
|
||||||
private static RichPresence _discordPresenceMain;
|
private static RichPresence _discordPresenceMain;
|
||||||
private static RichPresence _discordPresencePlaying;
|
private static RichPresence _discordPresencePlaying;
|
||||||
|
private static ApplicationMetadata _currentApp;
|
||||||
|
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
{
|
{
|
||||||
@ -45,8 +47,7 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
Assets = new Assets
|
Assets = new Assets
|
||||||
{
|
{
|
||||||
LargeImageKey = "ryujinx",
|
LargeImageKey = "ryujinx", LargeImageText = TruncateToByteLength(_description)
|
||||||
LargeImageText = TruncateToByteLength(_description)
|
|
||||||
},
|
},
|
||||||
Details = "Main Menu",
|
Details = "Main Menu",
|
||||||
State = "Idling",
|
State = "Idling",
|
||||||
@ -113,6 +114,7 @@ namespace Ryujinx.Ava
|
|||||||
private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes)
|
private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes)
|
||||||
{
|
{
|
||||||
_discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes));
|
_discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes));
|
||||||
|
_currentApp = appMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdatePlayingState()
|
private static void UpdatePlayingState()
|
||||||
@ -124,38 +126,31 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
_discordClient?.SetPresence(_discordPresenceMain);
|
_discordClient?.SetPresence(_discordPresenceMain);
|
||||||
_discordPresencePlaying = null;
|
_discordPresencePlaying = null;
|
||||||
|
_currentApp = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void HandlePlayReport(MessagePackObject playReport)
|
private static void HandlePlayReport(MessagePackObject playReport)
|
||||||
{
|
{
|
||||||
|
if (_discordClient is null) return;
|
||||||
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
|
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
|
||||||
if (_discordPresencePlaying is null) return;
|
if (_discordPresencePlaying is null) return;
|
||||||
if (!playReport.IsDictionary) return;
|
|
||||||
|
|
||||||
_playReportValues
|
PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
||||||
.FindFirst(x => x.Key.EqualsIgnoreCase(TitleIDs.CurrentApplication.Value))
|
|
||||||
.Convert(x => x.Value)
|
if (!value.Handled) return;
|
||||||
.IfPresent(x =>
|
|
||||||
|
if (value.Reset)
|
||||||
{
|
{
|
||||||
if (!playReport.AsDictionary().TryGetValue(x.ReportKey, out MessagePackObject valuePackObject))
|
_discordPresencePlaying.Details = $"Playing {_currentApp.Title}";
|
||||||
return;
|
Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter.");
|
||||||
|
}
|
||||||
_discordPresencePlaying.Details = x.Formatter(valuePackObject.ToObject());
|
else
|
||||||
UpdatePlayingState();
|
{
|
||||||
|
_discordPresencePlaying.Details = value.FormattedString;
|
||||||
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
|
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
UpdatePlayingState();
|
||||||
// title ID -> Play Report key & value formatter
|
|
||||||
private static readonly ReadOnlyDictionary<string, (string ReportKey, Func<object, string> Formatter)>
|
|
||||||
_playReportValues = new(new Dictionary<string, (string ReportKey, Func<object, string> Formatter)>
|
|
||||||
{
|
|
||||||
{
|
|
||||||
// Breath of the Wild Master Mode display
|
|
||||||
"01007ef00011e000",
|
|
||||||
("IsHardMode", val => val is 1 ? "Playing Master Mode" : "Playing Normal Mode")
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
private static string TruncateToByteLength(string input)
|
private static string TruncateToByteLength(string input)
|
||||||
{
|
{
|
||||||
|
198
src/Ryujinx/Utilities/PlayReport.cs
Normal file
198
src/Ryujinx/Utilities/PlayReport.cs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
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, Mario Kart 8 Deluxe (China)
|
||||||
|
["0100152000022000", "010075100E8EC000"],
|
||||||
|
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
|
||||||
|
);
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref PlayReportValue value)
|
||||||
|
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref PlayReportValue value)
|
||||||
|
=> value.BoxedValue switch
|
||||||
|
{
|
||||||
|
// Single Player
|
||||||
|
"Single" => "Single Player",
|
||||||
|
// Multiplayer
|
||||||
|
"Multi-2players" => "Multiplayer 2 Players",
|
||||||
|
"Multi-3players" => "Multiplayer 3 Players",
|
||||||
|
"Multi-4players" => "Multiplayer 4 Players",
|
||||||
|
// Wireless/LAN Play
|
||||||
|
"Local-Single" => "Wireless/LAN Play",
|
||||||
|
"Local-2players" => "Wireless/LAN Play 2 Players",
|
||||||
|
// CC Classes
|
||||||
|
"50cc" => "50cc",
|
||||||
|
"100cc" => "100cc",
|
||||||
|
"150cc" => "150cc",
|
||||||
|
"Mirror" => "Mirror (150cc)",
|
||||||
|
"200cc" => "200cc",
|
||||||
|
// Modes
|
||||||
|
"GrandPrix" => "Grand Prix",
|
||||||
|
"TimeAttack" => "Time Trials",
|
||||||
|
"VS" => "VS Races",
|
||||||
|
"Battle" => "Battle Mode",
|
||||||
|
"RaceStart" => "Selecting a Course",
|
||||||
|
"Race" => "Racing",
|
||||||
|
_ => PlayReportFormattedValue.ForceReset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Analyzer implementation
|
||||||
|
|
||||||
|
public class PlayReportAnalyzer
|
||||||
|
{
|
||||||
|
private readonly List<PlayReportGameSpec> _specs = [];
|
||||||
|
|
||||||
|
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] }));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
_specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport)
|
||||||
|
{
|
||||||
|
if (!playReport.IsDictionary)
|
||||||
|
return PlayReportFormattedValue.Unhandled;
|
||||||
|
|
||||||
|
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
|
||||||
|
return PlayReportFormattedValue.Unhandled;
|
||||||
|
|
||||||
|
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 PlayReportFormattedValue.Unhandled;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayReportGameSpec
|
||||||
|
{
|
||||||
|
public required string[] TitleIds { get; init; }
|
||||||
|
public List<PlayReportValueFormatterSpec> 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 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 PlayReportValueFormatter ValueFormatter { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate PlayReportFormattedValue PlayReportValueFormatter(ref PlayReportValue value);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user