Merge branch 'master' into master

This commit is contained in:
Evan Husted 2025-02-15 21:06:46 -06:00 committed by GitHub
commit 0106a6aceb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 201 additions and 74 deletions

View File

@ -447,6 +447,31 @@
"zh_TW": "開啟 Ryujinx 資料夾" "zh_TW": "開啟 Ryujinx 資料夾"
} }
}, },
{
"ID": "MenuBarFileOpenScreenshotsFolder",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Open Screenshots Folder",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{ {
"ID": "MenuBarFileOpenLogsFolder", "ID": "MenuBarFileOpenLogsFolder",
"Translations": { "Translations": {
@ -17197,6 +17222,31 @@
"zh_TW": "開啟 Ryujinx 檔案系統資料夾" "zh_TW": "開啟 Ryujinx 檔案系統資料夾"
} }
}, },
{
"ID": "OpenScreenshotFolderTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Open Ryujinx screenshots folder",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{ {
"ID": "OpenRyujinxLogsTooltip", "ID": "OpenRyujinxLogsTooltip",
"Translations": { "Translations": {

View File

@ -119,17 +119,23 @@
TextWrapping="Wrap" > TextWrapping="Wrap" >
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5"> <StackPanel Orientation="Horizontal" Spacing="5" ToolTip.Tip="{Binding DynamicRichPresenceDescription}">
<ui:SymbolIcon Foreground="ForestGreen" Symbol="Checkmark" IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"/> <ui:SymbolIcon
Foreground="ForestGreen"
Symbol="Checkmark"
IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"/>
<TextBlock <TextBlock
Foreground="ForestGreen" Foreground="ForestGreen"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}" IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"
Text="{ext:Locale GameInfoRpcDynamic}" Text="{ext:Locale GameInfoRpcDynamic}"
TextAlignment="Start" TextAlignment="Start"
TextWrapping="Wrap" > TextWrapping="Wrap">
</TextBlock> </TextBlock>
<ui:SymbolIcon Foreground="Red" Symbol="Cancel" IsVisible="{Binding !AppData.HasDynamicRichPresenceSupport}"/> <ui:SymbolIcon
Foreground="Red"
Symbol="Cancel"
IsVisible="{Binding !AppData.HasDynamicRichPresenceSupport}"/>
<TextBlock <TextBlock
Foreground="Red" Foreground="Red"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"

View File

@ -1,6 +1,7 @@
using Gommon; using Gommon;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.PlayReport;
namespace Ryujinx.Ava.UI.ViewModels namespace Ryujinx.Ava.UI.ViewModels
{ {
@ -10,6 +11,11 @@ namespace Ryujinx.Ava.UI.ViewModels
public ApplicationDataViewModel(ApplicationData appData) => AppData = appData; public ApplicationDataViewModel(ApplicationData appData) => AppData = appData;
public string DynamicRichPresenceDescription =>
AppData.HasDynamicRichPresenceSupport
? AppData.RichPresenceSpec.Value.Description
: GameSpec.DefaultDescription;
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version); public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer); public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension); public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);

View File

@ -1347,6 +1347,25 @@ namespace Ryujinx.Ava.UI.ViewModels
OpenHelper.OpenFolder(AppDataManager.BaseDirPath); OpenHelper.OpenFolder(AppDataManager.BaseDirPath);
} }
public void OpenScreenshotsFolder()
{
string screenshotsDir = Path.Combine(AppDataManager.BaseDirPath, "screenshots");
try
{
if (!Directory.Exists(screenshotsDir))
Directory.CreateDirectory(screenshotsDir);
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {screenshotsDir}. Error : {ex.GetType().Name}", "Screenshot");
return;
}
OpenHelper.OpenFolder(screenshotsDir);
}
public void OpenLogsFolder() public void OpenLogsFolder()
{ {
string logPath = AppDataManager.GetOrCreateLogsDir(); string logPath = AppDataManager.GetOrCreateLogsDir();

View File

@ -66,6 +66,10 @@
Command="{Binding OpenRyujinxFolder}" Command="{Binding OpenRyujinxFolder}"
Header="{ext:Locale MenuBarFileOpenEmuFolder}" Header="{ext:Locale MenuBarFileOpenEmuFolder}"
ToolTip.Tip="{ext:Locale OpenRyujinxFolderTooltip}" /> ToolTip.Tip="{ext:Locale OpenRyujinxFolderTooltip}" />
<MenuItem
Command="{Binding OpenScreenshotsFolder}"
Header="{ext:Locale MenuBarFileOpenScreenshotsFolder}"
ToolTip.Tip="{ext:Locale OpenScreenshotFolderTooltip}"/>
<MenuItem <MenuItem
Command="{Binding OpenLogsFolder}" Command="{Binding OpenLogsFolder}"
Header="{ext:Locale MenuBarFileOpenLogsFolder}" Header="{ext:Locale MenuBarFileOpenLogsFolder}"

View File

@ -10,6 +10,7 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.Compat; using Ryujinx.Ava.Utilities.Compat;
using Ryujinx.Ava.Utilities.PlayReport;
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;
@ -35,9 +36,14 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
{ {
_id = value; _id = value;
Compatibility = CompatibilityCsv.Find(Id); Compatibility = CompatibilityCsv.Find(value);
RichPresenceSpec = PlayReports.Analyzer.TryGetSpec(IdString, out GameSpec gameSpec)
? gameSpec
: default(Optional<GameSpec>);
} }
} }
public Optional<GameSpec> RichPresenceSpec { get; set; }
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 int PlayerCount { get; set; } public int PlayerCount { get; set; }
@ -46,7 +52,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0; public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
public bool HasRichPresenceAsset => DiscordIntegrationModule.HasAssetImage(IdString); public bool HasRichPresenceAsset => DiscordIntegrationModule.HasAssetImage(IdString);
public bool HasDynamicRichPresenceSupport => DiscordIntegrationModule.HasAnalyzer(IdString); public bool HasDynamicRichPresenceSupport => RichPresenceSpec.HasValue;
public TimeSpan TimePlayed { get; set; } public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; } public DateTime? LastPlayed { get; set; }

View File

@ -20,6 +20,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public IReadOnlyList<GameSpec> Specs => new ReadOnlyCollection<GameSpec>(_specs); public IReadOnlyList<GameSpec> Specs => new ReadOnlyCollection<GameSpec>(_specs);
public GameSpec GetSpec(string titleId) => _specs.First(x => x.TitleIds.ContainsIgnoreCase(titleId));
public bool TryGetSpec(string titleId, out GameSpec gameSpec)
=> (gameSpec = _specs.FirstOrDefault(x => x.TitleIds.ContainsIgnoreCase(titleId))) != null;
/// <summary> /// <summary>
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration. /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
/// </summary> /// </summary>
@ -128,13 +133,13 @@ namespace Ryujinx.Ava.Utilities.PlayReport
{ {
if (!playReport.ReportData.IsDictionary) if (!playReport.ReportData.IsDictionary)
return FormattedValue.Unhandled; return FormattedValue.Unhandled;
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec)) if (!TryGetSpec(runningGameId, out GameSpec spec))
return FormattedValue.Unhandled; return FormattedValue.Unhandled;
foreach (FormatterSpecBase formatSpec in spec.ValueFormatters.OrderBy(x => x.Priority)) foreach (FormatterSpecBase formatSpec in spec.ValueFormatters.OrderBy(x => x.Priority))
{ {
if (!formatSpec.Format(appMeta, playReport, out FormattedValue value)) if (!formatSpec.TryFormat(appMeta, playReport, out FormattedValue value))
continue; continue;
return value; return value;

View File

@ -9,72 +9,89 @@ namespace Ryujinx.Ava.Utilities.PlayReport
// init lazy value // init lazy value
_ = Analyzer; _ = Analyzer;
} }
public static Analyzer Analyzer => _analyzerLazy.Value; public static Analyzer Analyzer => _analyzerLazy.Value;
private static readonly Lazy<Analyzer> _analyzerLazy = new(() => private static readonly Lazy<Analyzer> _analyzerLazy = new(() => new Analyzer()
new Analyzer() .AddSpec(
.AddSpec( "01007ef00011e000",
"01007ef00011e000", spec => spec
spec => spec .WithDescription("based on being in Master Mode.")
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
// reset to normal status when switching between normal & master mode in title screen // reset to normal status when switching between normal & master mode in title screen
.AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets) .AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
) )
.AddSpec( .AddSpec(
"0100f2c0115b6000", "0100f2c0115b6000",
spec => spec spec => spec
.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField)) .WithDescription("based on where you are in Hyrule (Depths, Surface, Sky).")
.AddSpec( .AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
"0100000000010000", .AddSpec(
spec => "01002da013484000",
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode) spec => spec
) .WithDescription("based on how many Rupees you have.")
.AddSpec( .AddValueFormatter("rupees", SkywardSwordHD_Rupees))
"010075000ecbe000", .AddSpec(
spec => "0100000000010000",
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode) spec => spec
) .WithDescription("based on if you're playing with Assist Mode.")
.AddSpec( .AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
"010028600ebda000", )
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) .AddSpec(
) "010075000ecbe000",
.AddSpec( // Global & China IDs spec => spec
["0100152000022000", "010075100e8ec000"], .WithDescription("based on if you're playing with Assist Mode.")
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) .AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
) )
.AddSpec( .AddSpec(
["0100a3d008c5c000", "01008f6008c5e000"], "010028600ebda000",
spec => spec spec => spec
.AddValueFormatter("area_no", PokemonSVArea) .WithDescription("based on being in either Super Mario 3D World or Bowser's Fury.")
.AddValueFormatter("team_circle", PokemonSVUnionCircle) .AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
) )
.AddSpec( .AddSpec( // Global & China IDs
"01006a800016e000", ["0100152000022000", "010075100e8ec000"],
spec => spec spec => spec
.AddSparseMultiValueFormatter( .WithDescription(
[ "based on what modes you're selecting in the menu & whether or not you're in a race.")
// Metadata to figure out what PlayReport we have. .AddValueFormatter("To", MarioKart8Deluxe_Mode)
"match_mode", "match_submode", "anniversary", "fighter", "reason", "challenge_count", )
"adv_slot", .AddSpec(
// List of Fighters ["0100a3d008c5c000", "01008f6008c5e000"],
"player_1_fighter", "player_2_fighter", "player_3_fighter", "player_4_fighter", spec => spec
"player_5_fighter", "player_6_fighter", "player_7_fighter", "player_8_fighter", .WithDescription("based on what area of Paldea you're exploring.")
// List of rankings/placements .AddValueFormatter("area_no", PokemonSVArea)
"player_1_rank", "player_2_rank", "player_3_rank", "player_4_rank", "player_5_rank", .AddValueFormatter("team_circle", PokemonSVUnionCircle)
"player_6_rank", "player_7_rank", "player_8_rank" )
], .AddSpec(
SuperSmashBrosUltimate_Mode "01006a800016e000",
) spec => spec
) .WithDescription("based on what mode you're playing, who won, and what characters were present.")
.AddSpec( .AddSparseMultiValueFormatter(
[ [
"0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000", // Metadata to figure out what PlayReport we have.
"010012f017576000", "0100c62011050000", "0100b3c014bda000" "match_mode", "match_submode", "anniversary", "fighter", "reason", "challenge_count",
], "adv_slot",
spec => spec.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame) // List of Fighters
) "player_1_fighter", "player_2_fighter", "player_3_fighter", "player_4_fighter",
.AddSpec("01002da013484000", spec => spec.AddValueFormatter("rupees", SkywardSwordHD_Rupees)) "player_5_fighter", "player_6_fighter", "player_7_fighter", "player_8_fighter",
// List of rankings/placements
"player_1_rank", "player_2_rank", "player_3_rank", "player_4_rank", "player_5_rank",
"player_6_rank", "player_7_rank", "player_8_rank"
],
SuperSmashBrosUltimate_Mode
)
)
.AddSpec(
[
"0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000",
"010012f017576000", "0100c62011050000", "0100b3c014bda000"
],
spec => spec
.WithDescription(
"based on what game you first launch.\n\nNSO emulators do not print any Play Report information past the first game launch so it's all we got.")
.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame)
)
); );
private static string Playing(string game) => $"Playing {game}"; private static string Playing(string game) => $"Playing {game}";

View File

@ -23,6 +23,20 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public required string[] TitleIds { get; init; } public required string[] TitleIds { get; init; }
public const string DefaultDescription = "Formats the details on your Discord presence based on logged data from the game.";
private string _valueDescription;
public string Description => _valueDescription ?? DefaultDescription;
public GameSpec WithDescription(string description)
{
_valueDescription = description != null
? $"Formats the details on your Discord presence {description}"
: null;
return this;
}
public List<FormatterSpecBase> ValueFormatters { get; } = []; public List<FormatterSpecBase> ValueFormatters { get; } = [];
@ -197,7 +211,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public string[] ReportKeys { get; init; } public string[] ReportKeys { get; init; }
public Delegate Formatter { get; init; } public Delegate Formatter { get; init; }
public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport, public bool TryFormat(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport,
out FormattedValue formattedValue) out FormattedValue formattedValue)
{ {
formattedValue = default; formattedValue = default;