Compare commits

...

7 Commits

11 changed files with 214 additions and 163 deletions

View File

@@ -47,7 +47,8 @@ namespace Ryujinx.Common
"01006f8002326000", // Animal Crossings: New Horizons "01006f8002326000", // Animal Crossings: New Horizons
"01009bf0072d4000", // Captain Toad: Treasure Tracker "01009bf0072d4000", // Captain Toad: Treasure Tracker
"01009510001ca000", // Fast RMX "01009510001ca000", // Fast RMX
"01005CA01580E000", // Persona 5 Royale "01005CA01580E000", // Persona 5 Royal
"0100b880154fc000", // Persona 5 The Royal (Japan)
"010015100b514000", // Super Mario Bros. Wonder "010015100b514000", // Super Mario Bros. Wonder
"0100000000010000", // Super Mario Odyssey "0100000000010000", // Super Mario Odyssey

View File

@@ -2,35 +2,36 @@ using MsgPack;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Threading;
namespace Ryujinx.Horizon 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) =>
new Thread(() => PlayReport?.Invoke(report))
{
Name = "HLE.PlayReportEvent",
IsBackground = true,
Priority = ThreadPriority.AboveNormal
}.Start();
public static event Action<MessagePackObject> PlayReportPrinted; public static event Action<MessagePackObject> PlayReport;
[ThreadStatic]
private static HorizonOptions _options;
[ThreadStatic] [field: ThreadStatic]
private static ISyscallApi _syscall; public static HorizonOptions Options { get; private set; }
[ThreadStatic] [field: ThreadStatic]
private static IVirtualMemoryManager _addressSpace; public static ISyscallApi Syscall { get; private set; }
[ThreadStatic] [field: ThreadStatic]
private static IThreadContext _threadContext; public static IVirtualMemoryManager AddressSpace { get; private set; }
[ThreadStatic] [field: ThreadStatic]
private static int _threadHandle; public static IThreadContext ThreadContext { get; private set; }
public static HorizonOptions Options => _options; [field: ThreadStatic]
public static ISyscallApi Syscall => _syscall; public static int CurrentThreadHandle { get; private set; }
public static IVirtualMemoryManager AddressSpace => _addressSpace;
public static IThreadContext ThreadContext => _threadContext;
public static int CurrentThreadHandle => _threadHandle;
public static void Register( public static void Register(
HorizonOptions options, HorizonOptions options,
@@ -39,11 +40,11 @@ namespace Ryujinx.Horizon
IThreadContext threadContext, IThreadContext threadContext,
int threadHandle) int threadHandle)
{ {
_options = options; Options = options;
_syscall = syscallApi; Syscall = syscallApi;
_addressSpace = addressSpace; AddressSpace = addressSpace;
_threadContext = threadContext; ThreadContext = threadContext;
_threadHandle = threadHandle; CurrentThreadHandle = threadHandle;
} }
} }
} }

View File

@@ -1,3 +1,4 @@
using Gommon;
using MsgPack; using MsgPack;
using MsgPack.Serialization; using MsgPack.Serialization;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
@@ -11,6 +12,7 @@ using Ryujinx.Horizon.Sdk.Sf;
using Ryujinx.Horizon.Sdk.Sf.Hipc; using Ryujinx.Horizon.Sdk.Sf.Hipc;
using System; using System;
using System.Text; using System.Text;
using System.Threading;
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId; using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
namespace Ryujinx.Horizon.Prepo.Ipc namespace Ryujinx.Horizon.Prepo.Ipc

View File

@@ -23069,7 +23069,7 @@
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "可游玩", "zh_CN": "可游玩",
"zh_TW": "可暢順遊玩 (Playable)" "zh_TW": "可暢順遊玩"
} }
}, },
{ {
@@ -23094,7 +23094,7 @@
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "进入游戏", "zh_CN": "进入游戏",
"zh_TW": "大致可遊玩 (Ingame)" "zh_TW": "大致可遊玩"
} }
}, },
{ {
@@ -23119,7 +23119,7 @@
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "菜单", "zh_CN": "菜单",
"zh_TW": "只開啟至遊戲開始功能表 (Menus)" "zh_TW": "只開啟至遊戲開始功能表"
} }
}, },
{ {
@@ -23144,7 +23144,7 @@
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "启动", "zh_CN": "启动",
"zh_TW": "只能啟動 (Boots)" "zh_TW": "只能啟動"
} }
}, },
{ {
@@ -23169,7 +23169,7 @@
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "什么都没有", "zh_CN": "什么都没有",
"zh_TW": "無法啟動 (Nothing)" "zh_TW": "無法啟動"
} }
}, },
{ {

View File

@@ -56,7 +56,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update; ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue); TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
HorizonStatic.PlayReportPrinted += HandlePlayReport; HorizonStatic.PlayReport += HandlePlayReport;
} }
private static void Update(object sender, ReactiveEventArgs<bool> evnt) private static void Update(object sender, ReactiveEventArgs<bool> evnt)

View File

@@ -86,6 +86,13 @@
Text="{Binding Version}" Text="{Binding Version}"
TextAlignment="Start" TextAlignment="Start"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock
IsVisible="{Binding HasPlayabilityInfo}"
HorizontalAlignment="Stretch"
Text="{Binding LocalizedStatus}"
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextAlignment="Start"
TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</Border> </Border>
<StackPanel <StackPanel

View File

@@ -7,6 +7,8 @@ using LibHac.Ns;
using LibHac.Tools.Fs; using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.Compat;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Loaders.Processes.Extensions;
@@ -21,9 +23,30 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public bool Favorite { get; set; } public bool Favorite { get; set; }
public byte[] Icon { get; set; } public byte[] Icon { get; set; }
public string Name { get; set; } = "Unknown"; public string Name { get; set; } = "Unknown";
public ulong Id { get; set; }
private ulong _id;
public ulong Id
{
get => _id;
set
{
_id = value;
PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
}
}
public string Developer { get; set; } = "Unknown"; public string Developer { get; set; } = "Unknown";
public string Version { get; set; } = "0"; public string Version { get; set; } = "0";
public bool HasPlayabilityInfo => PlayabilityStatus != null;
public string LocalizedStatus =>
PlayabilityStatus.HasValue
? LocaleManager.Instance[PlayabilityStatus!.Value]
: string.Empty;
public LocaleKeys? PlayabilityStatus { get; set; }
public int PlayerCount { get; set; } public int PlayerCount { get; set; }
public int GameCount { get; set; } public int GameCount { get; set; }
public TimeSpan TimePlayed { get; set; } public TimeSpan TimePlayed { get; set; }

View File

@@ -47,11 +47,6 @@ namespace Ryujinx.Ava.Utilities.Compat
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility"); Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
} }
public static void Unload()
{
_entries = null;
}
private static CompatibilityEntry[] _entries; private static CompatibilityEntry[] _entries;
public static CompatibilityEntry[] Entries public static CompatibilityEntry[] Entries
@@ -64,6 +59,11 @@ namespace Ryujinx.Ava.Utilities.Compat
return _entries; return _entries;
} }
} }
public static LocaleKeys? GetStatus(string titleId)
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId))?.Status;
public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16"));
} }
public class CompatibilityEntry public class CompatibilityEntry

View File

@@ -32,8 +32,6 @@ namespace Ryujinx.Ava.Utilities.Compat
contentDialog.Styles.Add(closeButtonParent); contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog); await ContentDialogHelper.ShowAsync(contentDialog);
CompatibilityCsv.Unload();
} }
public CompatibilityList() public CompatibilityList()

View File

@@ -15,21 +15,24 @@ namespace Ryujinx.Ava.Utilities
"01007ef00011e000", "01007ef00011e000",
spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
) )
.AddSpec( // Super Mario Odyssey .AddSpec(
"0100f2c0115b6000",
spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
"0100000000010000", "0100000000010000",
spec => spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode) spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
) )
.AddSpec( // Super Mario Odyssey (China) .AddSpec(
"010075000ECBE000", "010075000ECBE000",
spec => spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode) spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
) )
.AddSpec( // Super Mario 3D World + Bowser's Fury .AddSpec(
"010028600EBDA000", "010028600EBDA000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
) )
.AddSpec( // Mario Kart 8 Deluxe, Mario Kart 8 Deluxe (China) .AddSpec( // Global & China IDs
["0100152000022000", "010075100E8EC000"], ["0100152000022000", "010075100E8EC000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
); );
@@ -37,6 +40,14 @@ namespace Ryujinx.Ava.Utilities
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value) private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset; => value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(ref PlayReportValue value) =>
value.PackedValue.AsDouble() switch
{
> 800d => "Exploring the Sky Islands",
< -201d => "Exploring the Depths",
_ => "Roaming Hyrule"
};
private static PlayReportFormattedValue 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"; => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
@@ -74,125 +85,4 @@ namespace Ryujinx.Ava.Utilities
_ => PlayReportFormattedValue.ForceReset _ => 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
} }

View File

@@ -0,0 +1,129 @@
using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities
{
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,
PackedValue = valuePackObject
};
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 readonly struct PlayReportValue
{
public ApplicationMetadata Application { get; init; }
public MessagePackObject PackedValue { get; init; }
public object BoxedValue => PackedValue.ToObject();
}
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);
}