Compare commits

...

12 Commits

Author SHA1 Message Date
Evan Husted
d8549f687b misc: chore: convert split ThreadStatic fields & property getter/setters into a single property with [field: ThreadStatic] 2025-02-03 17:04:11 -06:00
Evan Husted
5ab50680b4 HLE: Run Play Report event handlers in a dedicated .NET thread 2025-02-03 17:01:44 -06:00
Evan Husted
a0edc5c2b0 UI: RPC: Small change to how values are passed to hopefully detect the player pos better 2025-02-03 15:46:39 -06:00
Evan Husted
158ea7b4d6 misc: chore: logged coordinate is a decimal not a whole number 2025-02-03 15:33:12 -06:00
Evan Husted
8bc3de8303 UI: RPC: Add TOTK current world layer to RPC based on logged player y pos 2025-02-03 14:54:34 -06:00
Evan Husted
c812106611 UI: Show playability information under game version in List view 2025-02-03 13:49:56 -06:00
shinra-electric
11e4d8f970 Add the JP version of P5R to the list of Metal games (#612) 2025-02-03 01:36:58 -06:00
Evan Husted
774edb7b29 UI: Match System Time is now an active setting which you can toggle on/off. 2025-02-02 23:46:55 -06:00
Evan Husted
55536f5d78 misc: chore: Early exit HandlePlayReport if RPC is not enabled 2025-02-02 22:14:43 -06:00
Evan Husted
b2eecd28ce 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.
2025-02-02 22:10:49 -06:00
Evan Husted
fe43c32e60 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.
2025-02-02 20:47:42 -06:00
Evan Husted
8117e160c2 misc: chore: [ci skip] Move the play report analyzer definition into a PlayReport static class to avoid polluting the Discord integration module 2025-02-02 20:32:01 -06:00
22 changed files with 368 additions and 238 deletions

View File

@@ -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<PlayReportGameSpec> _specs = [];
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
{
_specs.Add(transform(new PlayReportGameSpec { TitleIdStr = titleId }));
return this;
}
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
{
_specs.Add(new PlayReportGameSpec { TitleIdStr = titleId }.Apply(transform));
return this;
}
public Optional<string> Run(string runningGameId, MessagePackObject playReport)
{
if (!playReport.IsDictionary)
return Optional<string>.None;
if (!_specs.TryGetFirst(s => s.TitleIdStr.EqualsIgnoreCase(runningGameId), out PlayReportGameSpec spec))
return Optional<string>.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<string>.None;
}
}
public class PlayReportGameSpec
{
public required string TitleIdStr { get; init; }
public List<PlayReportValueFormatterSpec> Analyses { get; } = [];
public PlayReportGameSpec AddValueFormatter(string reportKey, Func<object, string> valueFormatter)
{
Analyses.Add(new PlayReportValueFormatterSpec
{
Priority = Analyses.Count,
ReportKey = reportKey,
ValueFormatter = valueFormatter
});
return this;
}
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, Func<object, string> 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<object, string> ValueFormatter { get; init; }
}
}

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
using Gommon;
using MsgPack;
using MsgPack.Serialization;
using Ryujinx.Common.Logging;
@@ -11,6 +12,7 @@ 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

View File

@@ -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,

View File

@@ -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 目前的日期和時間。"
}
},
{
@@ -23069,7 +23069,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "可游玩",
"zh_TW": "可暢順遊玩 (Playable)"
"zh_TW": "可暢順遊玩"
}
},
{
@@ -23094,7 +23094,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "进入游戏",
"zh_TW": "大致可遊玩 (Ingame)"
"zh_TW": "大致可遊玩"
}
},
{
@@ -23119,7 +23119,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "菜单",
"zh_TW": "只開啟至遊戲開始功能表 (Menus)"
"zh_TW": "只開啟至遊戲開始功能表"
}
},
{
@@ -23144,7 +23144,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "启动",
"zh_TW": "只能啟動 (Boots)"
"zh_TW": "只能啟動"
}
},
{
@@ -23169,7 +23169,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "什么都没有",
"zh_TW": "無法啟動 (Nothing)"
"zh_TW": "無法啟動"
}
},
{

View File

@@ -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()
{
@@ -55,7 +56,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
HorizonStatic.PlayReportPrinted += HandlePlayReport;
HorizonStatic.PlayReport += HandlePlayReport;
}
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
@@ -82,36 +83,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<string> titleId)
{
if (titleId.TryGet(out string tid))
@@ -143,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()
@@ -154,52 +126,30 @@ namespace Ryujinx.Ava
{
_discordClient?.SetPresence(_discordPresenceMain);
_discordPresencePlaying = null;
_currentApp = 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 (_discordClient is null) return;
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
if (_discordPresencePlaying is null) return;
Optional<string> details = _playReportAnalyzer.Run(TitleIDs.CurrentApplication.Value, 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)

View File

@@ -86,6 +86,13 @@
Text="{Binding Version}"
TextAlignment="Start"
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>
</Border>
<StackPanel

View File

@@ -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;

View File

@@ -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">
<Design.DataContext>
@@ -69,7 +70,7 @@
</ComboBox>
</StackPanel>
<CheckBox IsChecked="{Binding UseHypervisor}"
IsVisible="{Binding IsAppleSiliconMac}"
IsVisible="{x:Static helper:RunningPlatform.IsArmMac}"
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemUseHypervisor}"
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}" />

View File

@@ -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 @@
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
<TextBlock Text="OpenGL" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsAppleSiliconMac}">
<ComboBoxItem IsEnabled="{x:Static helper:RunningPlatform.IsArmMac}">
<TextBlock Text="Metal (ARM Mac only, Experimental)" />
</ComboBoxItem>
</ComboBox>

View File

@@ -170,7 +170,8 @@
ToolTip.Tip="{ext:Locale TimeTooltip}"
Width="250"/>
<DatePicker
VerticalAlignment="Center"
VerticalAlignment="Center"
IsEnabled="{Binding !MatchSystemTime}"
SelectedDate="{Binding CurrentDate}"
ToolTip.Tip="{ext:Locale TimeTooltip}"
Width="350" />
@@ -181,17 +182,21 @@
<TimePicker
VerticalAlignment="Center"
ClockIdentifier="24HourClock"
IsEnabled="{Binding !MatchSystemTime}"
SelectedTime="{Binding CurrentTime}"
Width="350"
ToolTip.Tip="{ext:Locale TimeTooltip}" />
<Button
Margin="10, 0, 0, 0"
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
Click="MatchSystemTime_OnClick"
Background="{DynamicResource SystemAccentColor}"
ToolTip.Tip="{ext:Locale MatchTimeTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemSystemTimeMatch}" />
</Button>
Text="{ext:Locale SettingsTabSystemSystemTimeMatch}"
ToolTip.Tip="{ext:Locale MatchTimeTooltip}"
Width="250"/>
<CheckBox
VerticalAlignment="Center"
IsChecked="{Binding MatchSystemTime}"
ToolTip.Tip="{ext:Locale MatchTimeTooltip}"/>
</StackPanel>
<Separator />
<StackPanel Margin="0,10,0,10"

View File

@@ -34,7 +34,5 @@ namespace Ryujinx.Ava.UI.Views.Settings
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
}
}
private void MatchSystemTime_OnClick(object sender, RoutedEventArgs e) => ViewModel.MatchSystemTime();
}
}

View File

@@ -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}">
<Button
Classes="accent"
Content="{ext:Locale SettingsButtonOk}"

View File

@@ -7,6 +7,8 @@ using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.Compat;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
@@ -21,9 +23,30 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public bool Favorite { get; set; }
public byte[] Icon { get; set; }
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 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 GameCount { 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");
}
public static void Unload()
{
_entries = null;
}
private static CompatibilityEntry[] _entries;
public static CompatibilityEntry[] Entries
@@ -64,6 +59,11 @@ namespace Ryujinx.Ava.Utilities.Compat
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

View File

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

View File

@@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 62;
public const int CurrentVersion = 63;
/// <summary>
/// Version of the configuration file format
@@ -141,6 +141,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// Change System Time Offset in seconds
/// </summary>
public long SystemTimeOffset { get; set; }
/// <summary>
/// Instead of setting the time via configuration, use the values provided by the system.
/// </summary>
public bool MatchSystemTime { get; set; }
/// <summary>
/// Enables or disables Docked Mode

View File

@@ -429,7 +429,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
};
}
}),
(62, static cff => cff.RainbowSpeed = 1f)
(62, static cff => cff.RainbowSpeed = 1f),
(63, static cff => cff.MatchSystemTime = false)
);
}
}

View File

@@ -312,6 +312,11 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// System Time Offset in Seconds
/// </summary>
public ReactiveObject<long> SystemTimeOffset { get; private set; }
/// <summary>
/// Instead of setting the time via configuration, use the values provided by the system.
/// </summary>
public ReactiveObject<bool> MatchSystemTime { get; private set; }
/// <summary>
/// Enables or disables Docked Mode
@@ -388,6 +393,8 @@ namespace Ryujinx.Ava.Utilities.Configuration
TimeZone.LogChangesToValue(nameof(TimeZone));
SystemTimeOffset = new ReactiveObject<long>();
SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset));
MatchSystemTime = new ReactiveObject<bool>();
MatchSystemTime.LogChangesToValue(nameof(MatchSystemTime));
EnableDockedMode = new ReactiveObject<bool>();
EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode));
EnablePtc = new ReactiveObject<bool>();

View File

@@ -0,0 +1,88 @@
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(
"0100f2c0115b6000",
spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
"0100000000010000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
)
.AddSpec(
"010075000ECBE000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
)
.AddSpec(
"010028600EBDA000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec( // Global & China IDs
["0100152000022000", "010075100E8EC000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
);
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value)
=> 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)
=> 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
};
}
}

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