mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-07-26 23:27:11 +02:00
misc: small Avalonia project restructure
Moved AppLibrary, Configuration, and PlayReport namespaces to Ryujinx.Systems, add the compat list stuff in the base Ryujinx.Systems namespace. Moved the compatibility UI stuff to the proper UI view/viewmodel folders.
This commit is contained in:
parent
c12a59ecd6
commit
2317c06364
84 changed files with 128 additions and 129 deletions
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.AppLibrary
|
||||
{
|
||||
public class ApplicationCountUpdatedEventArgs : EventArgs
|
||||
{
|
||||
public int NumAppsFound { get; set; }
|
||||
public int NumAppsLoaded { get; set; }
|
||||
}
|
||||
}
|
227
src/Ryujinx/Systems/AppLibrary/ApplicationData.cs
Normal file
227
src/Ryujinx/Systems/AppLibrary/ApplicationData.cs
Normal file
|
@ -0,0 +1,227 @@
|
|||
using Gommon;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Loader;
|
||||
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;
|
||||
using Ryujinx.Ava.Systems.PlayReport;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.AppLibrary
|
||||
{
|
||||
public class ApplicationData
|
||||
{
|
||||
public bool Favorite { get; set; }
|
||||
public bool HasIndependentConfiguration { get; set; }
|
||||
public byte[] Icon { get; set; }
|
||||
public string Name { get; set; } = "Unknown";
|
||||
|
||||
private ulong _id;
|
||||
|
||||
public ulong Id
|
||||
{
|
||||
get => _id;
|
||||
set
|
||||
{
|
||||
_id = value;
|
||||
|
||||
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 Version { get; set; } = "0";
|
||||
public int PlayerCount { get; set; }
|
||||
public int GameCount { get; set; }
|
||||
|
||||
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
|
||||
|
||||
public bool HasRichPresenceAsset => DiscordIntegrationModule.HasAssetImage(IdString);
|
||||
public bool HasDynamicRichPresenceSupport => RichPresenceSpec.HasValue;
|
||||
|
||||
public TimeSpan TimePlayed { get; set; }
|
||||
public DateTime? LastPlayed { get; set; }
|
||||
public string FileExtension { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
public string Path { get; set; }
|
||||
public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
|
||||
|
||||
public bool HasControlHolder => ControlHolder.ByteSpan.Length > 0 && !ControlHolder.ByteSpan.IsZeros();
|
||||
|
||||
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
|
||||
|
||||
public bool HasPlayedPreviously => TimePlayed.TotalSeconds > 1;
|
||||
|
||||
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
|
||||
|
||||
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
|
||||
|
||||
public Optional<CompatibilityEntry> Compatibility { get; private set; }
|
||||
|
||||
public bool HasPlayabilityInfo => Compatibility.HasValue;
|
||||
|
||||
public string LocalizedStatus => Compatibility.Convert(x => x.LocalizedStatus);
|
||||
|
||||
public bool HasCompatibilityLabels => !FormattedCompatibilityLabels.Equals(string.Empty);
|
||||
|
||||
public string FormattedCompatibilityLabels
|
||||
=> Compatibility.Convert(x => x.FormattedIssueLabels).OrElse(string.Empty);
|
||||
|
||||
public LocaleKeys? PlayabilityStatus => Compatibility.Convert(x => x.Status).OrElse(null);
|
||||
|
||||
public string LocalizedStatusTooltip =>
|
||||
Compatibility.Convert(x =>
|
||||
#pragma warning disable CS8509 // It is exhaustive for all possible values this can contain.
|
||||
LocaleManager.Instance[x.Status switch
|
||||
#pragma warning restore CS8509
|
||||
{
|
||||
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
|
||||
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
|
||||
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
|
||||
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
|
||||
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
|
||||
}]
|
||||
).OrElse(string.Empty);
|
||||
|
||||
|
||||
[JsonIgnore] public string IdString => Id.ToString("x16");
|
||||
|
||||
[JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL;
|
||||
|
||||
[JsonIgnore] public string IdBaseString => IdBase.ToString("x16");
|
||||
|
||||
public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
|
||||
{
|
||||
if (!System.IO.Path.Exists(titleFilePath))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist.");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
|
||||
|
||||
Nca mainNca = null;
|
||||
Nca patchNca = null;
|
||||
|
||||
string extension = System.IO.Path.GetExtension(titleFilePath).ToLower();
|
||||
|
||||
if (extension is ".nsp" or ".xci")
|
||||
{
|
||||
IFileSystem pfs;
|
||||
|
||||
if (extension == ".xci")
|
||||
{
|
||||
Xci xci = new(virtualFileSystem.KeySet, file.AsStorage());
|
||||
|
||||
pfs = xci.OpenPartition(XciPartitionType.Secure);
|
||||
}
|
||||
else
|
||||
{
|
||||
PartitionFileSystem pfsTemp = new();
|
||||
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||
pfs = pfsTemp;
|
||||
}
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
using UniqueRef<IFile> ncaFile = new();
|
||||
|
||||
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = new(virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
||||
|
||||
if (nca.Header.ContentType != NcaContentType.Program)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||
|
||||
if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
|
||||
{
|
||||
patchNca = nca;
|
||||
}
|
||||
else
|
||||
{
|
||||
mainNca = nca;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (extension == ".nca")
|
||||
{
|
||||
mainNca = new Nca(virtualFileSystem.KeySet, file.AsStorage());
|
||||
}
|
||||
|
||||
if (mainNca == null)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, "Extraction failure. The main NCA was not present in the selected file");
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
(Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _);
|
||||
|
||||
if (updatePatchNca != null)
|
||||
{
|
||||
patchNca = updatePatchNca;
|
||||
}
|
||||
|
||||
IFileSystem codeFs = null;
|
||||
|
||||
if (patchNca == null)
|
||||
{
|
||||
if (mainNca.CanOpenSection(NcaSectionType.Code))
|
||||
{
|
||||
codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (patchNca.CanOpenSection(NcaSectionType.Code))
|
||||
{
|
||||
codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
if (codeFs == null)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Loader, "No ExeFS found in NCA");
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
const string MainExeFs = "main";
|
||||
|
||||
if (!codeFs.FileExists($"/{MainExeFs}"))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Loader, "No main binary ExeFS found in ExeFS");
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
using UniqueRef<IFile> nsoFile = new();
|
||||
|
||||
codeFs.OpenFile(ref nsoFile.Ref, $"/{MainExeFs}".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
NsoReader reader = new();
|
||||
reader.Initialize(nsoFile.Release().AsStorage().AsFile(OpenMode.Read)).ThrowIfFailure();
|
||||
|
||||
return Convert.ToHexString(reader.Header.ModuleId.ItemsRo.ToArray()).Replace("-", string.Empty).ToUpper()[..16];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.AppLibrary
|
||||
{
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(ApplicationMetadata))]
|
||||
internal partial class ApplicationJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
1580
src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs
Normal file
1580
src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs
Normal file
File diff suppressed because it is too large
Load diff
51
src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs
Normal file
51
src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.AppLibrary
|
||||
{
|
||||
public class ApplicationMetadata
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
|
||||
[JsonPropertyName("timespan_played")]
|
||||
public TimeSpan TimePlayed { get; set; } = TimeSpan.Zero;
|
||||
|
||||
[JsonPropertyName("last_played_utc")]
|
||||
public DateTime? LastPlayed { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("time_played")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public double TimePlayedOld { get; set; }
|
||||
|
||||
[JsonPropertyName("last_played")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public string LastPlayedOld { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/>. Call this before launching a game.
|
||||
/// </summary>
|
||||
public void UpdatePreGame()
|
||||
{
|
||||
LastPlayed = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/> and <see cref="TimePlayed"/>. Call this after a game ends.
|
||||
/// </summary>
|
||||
public void UpdatePostGame()
|
||||
{
|
||||
DateTime? prevLastPlayed = LastPlayed;
|
||||
UpdatePreGame();
|
||||
|
||||
if (!prevLastPlayed.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value;
|
||||
double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds;
|
||||
TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
}
|
||||
}
|
49
src/Ryujinx/Systems/AppLibrary/LdnGameData.cs
Normal file
49
src/Ryujinx/Systems/AppLibrary/LdnGameData.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using Gommon;
|
||||
using LibHac.Ns;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.AppLibrary
|
||||
{
|
||||
public struct LdnGameData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public int PlayerCount { get; set; }
|
||||
public int MaxPlayerCount { get; set; }
|
||||
public string GameName { get; set; }
|
||||
public string TitleId { get; set; }
|
||||
public string Mode { get; set; }
|
||||
public string Status { get; set; }
|
||||
public IEnumerable<string> Players { get; set; }
|
||||
|
||||
public static Array GetArrayForApp(
|
||||
LdnGameData[] receivedData, ref ApplicationControlProperty acp)
|
||||
{
|
||||
LibHac.Common.FixedArrays.Array8<ulong> communicationId = acp.LocalCommunicationId;
|
||||
|
||||
return new Array(receivedData.Where(game =>
|
||||
communicationId.Items.Contains(game.TitleId.ToULong())
|
||||
));
|
||||
}
|
||||
|
||||
public class Array
|
||||
{
|
||||
private readonly LdnGameData[] _ldnDatas;
|
||||
|
||||
internal Array(IEnumerable<LdnGameData> receivedData)
|
||||
{
|
||||
_ldnDatas = receivedData.ToArray();
|
||||
}
|
||||
|
||||
public int PlayerCount => _ldnDatas.Sum(it => it.PlayerCount);
|
||||
public int GameCount => _ldnDatas.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LdnGameDataHelper
|
||||
{
|
||||
public static LdnGameData.Array Where(this LdnGameData[] unfilteredDatas, ref ApplicationControlProperty acp)
|
||||
=> LdnGameData.GetArrayForApp(unfilteredDatas, ref acp);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.AppLibrary
|
||||
{
|
||||
public class LdnGameDataReceivedEventArgs : EventArgs
|
||||
{
|
||||
public static new readonly LdnGameDataReceivedEventArgs Empty = new(null);
|
||||
|
||||
public LdnGameDataReceivedEventArgs(LdnGameData[] ldnData)
|
||||
{
|
||||
LdnData = ldnData ?? [];
|
||||
}
|
||||
|
||||
|
||||
public LdnGameData[] LdnData { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.AppLibrary
|
||||
{
|
||||
[JsonSerializable(typeof(IEnumerable<LdnGameData>))]
|
||||
internal partial class LdnGameDataSerializerContext : JsonSerializerContext;
|
||||
}
|
208
src/Ryujinx/Systems/CompatibilityCsv.cs
Normal file
208
src/Ryujinx/Systems/CompatibilityCsv.cs
Normal file
|
@ -0,0 +1,208 @@
|
|||
using Gommon;
|
||||
using Humanizer;
|
||||
using nietras.SeparatedValues;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ava.Systems
|
||||
{
|
||||
public struct ColumnIndices(Func<ReadOnlySpan<char>, int> getIndex)
|
||||
{
|
||||
public const string TitleIdCol = "\"title_id\"";
|
||||
public const string GameNameCol = "\"game_name\"";
|
||||
public const string LabelsCol = "\"labels\"";
|
||||
public const string StatusCol = "\"status\"";
|
||||
public const string LastUpdatedCol = "\"last_updated\"";
|
||||
|
||||
public readonly int TitleId = getIndex(TitleIdCol);
|
||||
public readonly int GameName = getIndex(GameNameCol);
|
||||
public readonly int Labels = getIndex(LabelsCol);
|
||||
public readonly int Status = getIndex(StatusCol);
|
||||
public readonly int LastUpdated = getIndex(LastUpdatedCol);
|
||||
}
|
||||
|
||||
public class CompatibilityCsv
|
||||
{
|
||||
static CompatibilityCsv() => Load();
|
||||
|
||||
public static void Load()
|
||||
{
|
||||
using Stream csvStream = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("RyujinxGameCompatibilityList")!;
|
||||
csvStream.Position = 0;
|
||||
|
||||
using SepReader reader = Sep.Reader().From(csvStream);
|
||||
ColumnIndices columnIndices = new(reader.Header.IndexOf);
|
||||
|
||||
_entries = reader
|
||||
.Enumerate(row => new CompatibilityEntry(ref columnIndices, row))
|
||||
.OrderBy(it => it.GameName)
|
||||
.ToArray();
|
||||
|
||||
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
|
||||
}
|
||||
|
||||
private static CompatibilityEntry[] _entries;
|
||||
|
||||
public static CompatibilityEntry[] Entries
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_entries == null)
|
||||
Load();
|
||||
|
||||
return _entries;
|
||||
}
|
||||
}
|
||||
|
||||
public static CompatibilityEntry Find(string titleId)
|
||||
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId));
|
||||
|
||||
public static CompatibilityEntry Find(ulong titleId)
|
||||
=> Find(titleId.ToString("X16"));
|
||||
|
||||
public static LocaleKeys? GetStatus(string titleId)
|
||||
=> Find(titleId)?.Status;
|
||||
|
||||
public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16"));
|
||||
|
||||
public static string GetLabels(string titleId)
|
||||
=> Find(titleId)?.FormattedIssueLabels;
|
||||
|
||||
public static string GetLabels(ulong titleId) => GetLabels(titleId.ToString("X16"));
|
||||
}
|
||||
|
||||
public class CompatibilityEntry
|
||||
{
|
||||
public CompatibilityEntry(ref ColumnIndices indices, SepReader.Row row)
|
||||
{
|
||||
string titleIdRow = ColStr(row[indices.TitleId]);
|
||||
TitleId = !string.IsNullOrEmpty(titleIdRow)
|
||||
? titleIdRow
|
||||
: default(Optional<string>);
|
||||
|
||||
GameName = ColStr(row[indices.GameName]);
|
||||
|
||||
Labels = ColStr(row[indices.Labels]).Split(';');
|
||||
Status = ColStr(row[indices.Status]).ToLower() switch
|
||||
{
|
||||
"playable" => LocaleKeys.CompatibilityListPlayable,
|
||||
"ingame" => LocaleKeys.CompatibilityListIngame,
|
||||
"menus" => LocaleKeys.CompatibilityListMenus,
|
||||
"boots" => LocaleKeys.CompatibilityListBoots,
|
||||
"nothing" => LocaleKeys.CompatibilityListNothing,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (DateTime.TryParse(ColStr(row[indices.LastUpdated]), out DateTime dt))
|
||||
LastUpdated = dt;
|
||||
|
||||
return;
|
||||
|
||||
string ColStr(SepReader.Col col) => col.ToString().Trim('"');
|
||||
}
|
||||
|
||||
public string GameName { get; }
|
||||
public Optional<string> TitleId { get; }
|
||||
public string[] Labels { get; }
|
||||
public LocaleKeys? Status { get; }
|
||||
|
||||
public LocaleKeys? StatusDescription
|
||||
=> Status switch
|
||||
{
|
||||
LocaleKeys.CompatibilityListPlayable => LocaleKeys.CompatibilityListPlayableTooltip,
|
||||
LocaleKeys.CompatibilityListIngame => LocaleKeys.CompatibilityListIngameTooltip,
|
||||
LocaleKeys.CompatibilityListMenus => LocaleKeys.CompatibilityListMenusTooltip,
|
||||
LocaleKeys.CompatibilityListBoots => LocaleKeys.CompatibilityListBootsTooltip,
|
||||
LocaleKeys.CompatibilityListNothing => LocaleKeys.CompatibilityListNothingTooltip,
|
||||
_ => null
|
||||
};
|
||||
|
||||
public DateTime LastUpdated { get; }
|
||||
|
||||
public string LocalizedLastUpdated =>
|
||||
LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize());
|
||||
|
||||
public string LocalizedStatus => LocaleManager.Instance[Status!.Value];
|
||||
public string LocalizedStatusDescription => LocaleManager.Instance[StatusDescription!.Value];
|
||||
public string FormattedTitleId => TitleId
|
||||
.OrElse(new string(' ', 16));
|
||||
|
||||
public string FormattedIssueLabels => Labels
|
||||
.Select(FormatLabelName)
|
||||
.JoinToString(", ");
|
||||
|
||||
public override string ToString() =>
|
||||
new StringBuilder("CompatibilityEntry: {")
|
||||
.Append($"{nameof(GameName)}=\"{GameName}\", ")
|
||||
.Append($"{nameof(TitleId)}={TitleId}, ")
|
||||
.Append($"{nameof(Labels)}={
|
||||
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
|
||||
}, ")
|
||||
.Append($"{nameof(Status)}=\"{Status}\", ")
|
||||
.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"")
|
||||
.Append('}')
|
||||
.ToString();
|
||||
|
||||
public static string FormatLabelName(string labelName) => labelName.ToLower() switch
|
||||
{
|
||||
"audio" => "Audio",
|
||||
"bug" => "Bug",
|
||||
"cpu" => "CPU",
|
||||
"gpu" => "GPU",
|
||||
"gui" => "GUI",
|
||||
"help wanted" => "Help Wanted",
|
||||
"horizon" => "Horizon",
|
||||
"infra" => "Project Infra",
|
||||
"invalid" => "Invalid",
|
||||
"kernel" => "Kernel",
|
||||
"ldn" => "LDN",
|
||||
"linux" => "Linux",
|
||||
"macos" => "macOS",
|
||||
"question" => "Question",
|
||||
"windows" => "Windows",
|
||||
"graphics-backend:opengl" => "Graphics: OpenGL",
|
||||
"graphics-backend:vulkan" => "Graphics: Vulkan",
|
||||
"ldn-works" => "LDN Works",
|
||||
"ldn-untested" => "LDN Untested",
|
||||
"ldn-broken" => "LDN Broken",
|
||||
"ldn-partial" => "Partial LDN",
|
||||
"nvdec" => "NVDEC",
|
||||
"services" => "NX Services",
|
||||
"services-horizon" => "Horizon OS Services",
|
||||
"slow" => "Runs Slow",
|
||||
"crash" => "Crashes",
|
||||
"deadlock" => "Deadlock",
|
||||
"regression" => "Regression",
|
||||
"opengl" => "OpenGL",
|
||||
"opengl-backend-bug" => "OpenGL Backend Bug",
|
||||
"vulkan-backend-bug" => "Vulkan Backend Bug",
|
||||
"mac-bug" => "Mac-specific Bug(s)",
|
||||
"amd-vendor-bug" => "AMD GPU Bug",
|
||||
"intel-vendor-bug" => "Intel GPU Bug",
|
||||
"loader-allocator" => "Loader Allocator",
|
||||
"audout" => "AudOut",
|
||||
"32-bit" => "32-bit Game",
|
||||
"UE4" => "Unreal Engine 4",
|
||||
"homebrew" => "Homebrew Content",
|
||||
"online-broken" => "Online Broken",
|
||||
_ => Capitalize(labelName)
|
||||
};
|
||||
|
||||
public static string Capitalize(string value)
|
||||
{
|
||||
if (value == string.Empty)
|
||||
return string.Empty;
|
||||
|
||||
char firstChar = value[0];
|
||||
string rest = value[1..];
|
||||
|
||||
return $"{char.ToUpper(firstChar)}{rest}";
|
||||
}
|
||||
}
|
||||
}
|
14
src/Ryujinx/Systems/Configuration/AudioBackend.cs
Normal file
14
src/Ryujinx/Systems/Configuration/AudioBackend.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<AudioBackend>))]
|
||||
public enum AudioBackend
|
||||
{
|
||||
Dummy,
|
||||
OpenAl,
|
||||
SoundIo,
|
||||
SDL2,
|
||||
}
|
||||
}
|
492
src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs
Normal file
492
src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs
Normal file
|
@ -0,0 +1,492 @@
|
|||
using Ryujinx.Ava.Systems.Configuration.System;
|
||||
using Ryujinx.Ava.Systems.Configuration.UI;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
public class ConfigurationFileFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// The current version of the file format
|
||||
/// </summary>
|
||||
public const int CurrentVersion = 67;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the configuration file format
|
||||
/// </summary>
|
||||
public int Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables logging to a file on disk
|
||||
/// </summary>
|
||||
public bool EnableFileLog { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime.
|
||||
/// </summary>
|
||||
public BackendThreading BackendThreading { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead.
|
||||
/// </summary>
|
||||
public int ResScale { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1.
|
||||
/// </summary>
|
||||
public float ResScaleCustom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide.
|
||||
/// </summary>
|
||||
public float MaxAnisotropy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Aspect Ratio applied to the renderer window.
|
||||
/// </summary>
|
||||
public AspectRatio AspectRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies anti-aliasing to the renderer.
|
||||
/// </summary>
|
||||
public AntiAliasing AntiAliasing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the framebuffer upscaling type.
|
||||
/// </summary>
|
||||
public ScalingFilter ScalingFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the framebuffer upscaling level.
|
||||
/// </summary>
|
||||
public int ScalingFilterLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dumps shaders in this local directory
|
||||
/// </summary>
|
||||
public string GraphicsShadersDumpPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing debug log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableDebug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing stub log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableStub { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing info log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing warning log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableWarn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing error log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing trace log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableTrace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing guest log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableGuest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing FS access log messages
|
||||
/// </summary>
|
||||
public bool LoggingEnableFsAccessLog { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables log messages from Avalonia
|
||||
/// </summary>
|
||||
public bool LoggingEnableAvalonia { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls which log messages are written to the log targets
|
||||
/// </summary>
|
||||
public LogClass[] LoggingFilteredClasses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change Graphics API debug log level
|
||||
/// </summary>
|
||||
public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System Language
|
||||
/// </summary>
|
||||
public Language SystemLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System Region
|
||||
/// </summary>
|
||||
public Region SystemRegion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System TimeZone
|
||||
/// </summary>
|
||||
public string SystemTimeZone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public bool DockedMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Discord Rich Presence
|
||||
/// </summary>
|
||||
public bool EnableDiscordIntegration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DEPRECATED: Checks for updates when Ryujinx starts when enabled
|
||||
/// </summary>
|
||||
public bool CheckUpdatesOnStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
|
||||
/// </summary>
|
||||
public UpdaterType UpdateCheckerType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How the emulator should behave when you click off/on the window.
|
||||
/// </summary>
|
||||
public FocusLostType FocusLostActionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show "Confirm Exit" Dialog
|
||||
/// </summary>
|
||||
public bool ShowConfirmExit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ignore Controller Applet dialog
|
||||
/// </summary>
|
||||
public bool IgnoreApplet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables save window size, position and state on close.
|
||||
/// </summary>
|
||||
public bool RememberWindowState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the redesigned title bar
|
||||
/// </summary>
|
||||
public bool ShowTitleBar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables hardware-accelerated rendering for Avalonia
|
||||
/// </summary>
|
||||
public bool EnableHardwareAcceleration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to hide cursor on idle, always or never
|
||||
/// </summary>
|
||||
public HideCursorMode HideCursor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Vertical Sync
|
||||
/// </summary>
|
||||
/// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
|
||||
/// TODO: Remove this when those older versions aren't in use anymore.
|
||||
public bool EnableVsync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current VSync mode; 60 (Switch), unbounded ("Vsync off"), or custom
|
||||
/// </summary>
|
||||
public VSyncMode VSyncMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the custom present interval
|
||||
/// </summary>
|
||||
public bool EnableCustomVSyncInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The custom present interval value
|
||||
/// </summary>
|
||||
public int CustomVSyncInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Shader cache
|
||||
/// </summary>
|
||||
public bool EnableShaderCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables texture recompression
|
||||
/// </summary>
|
||||
public bool EnableTextureRecompression { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Macro high-level emulation
|
||||
/// </summary>
|
||||
public bool EnableMacroHLE { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables color space passthrough, if available.
|
||||
/// </summary>
|
||||
public bool EnableColorSpacePassthrough { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables profiled translation cache persistency
|
||||
/// </summary>
|
||||
public bool EnablePtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables low-power profiled translation cache persistency loading
|
||||
/// </summary>
|
||||
public bool EnableLowPowerPtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables guest Internet access
|
||||
/// </summary>
|
||||
public bool EnableInternetAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables integrity checks on Game content files
|
||||
/// </summary>
|
||||
public bool EnableFsIntegrityChecks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables FS access log output to the console. Possible modes are 0-3
|
||||
/// </summary>
|
||||
public int FsGlobalAccessLogMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selected audio backend
|
||||
/// </summary>
|
||||
public AudioBackend AudioBackend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The audio volume
|
||||
/// </summary>
|
||||
public float AudioVolume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selected memory manager mode
|
||||
/// </summary>
|
||||
public MemoryManagerMode MemoryManagerMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Expands the RAM amount on the emulated system
|
||||
/// </summary>
|
||||
public MemoryConfiguration DramSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable ignoring missing services
|
||||
/// </summary>
|
||||
public bool IgnoreMissingServices { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to toggle columns in the GUI
|
||||
/// </summary>
|
||||
public GuiColumns GuiColumns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to configure column sort settings in the GUI
|
||||
/// </summary>
|
||||
public ColumnSort ColumnSort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of directories containing games to be used to load games into the games list
|
||||
/// </summary>
|
||||
public List<string> GameDirs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of directories containing DLC/updates the user wants to autoload during library refreshes
|
||||
/// </summary>
|
||||
public List<string> AutoloadDirs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of file types to be hidden in the games List
|
||||
/// </summary>
|
||||
public ShownFileTypes ShownFileTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Main window start-up position, size and state
|
||||
/// </summary>
|
||||
public WindowStartup WindowStartup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Language Code for the UI
|
||||
/// </summary>
|
||||
public string LanguageCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Chooses the base style // Not Used
|
||||
/// </summary>
|
||||
public string BaseStyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Chooses the view mode of the game list // Not Used
|
||||
/// </summary>
|
||||
public int GameListViewMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show application name in Grid Mode // Not Used
|
||||
/// </summary>
|
||||
public bool ShowNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets App Icon Size // Not Used
|
||||
/// </summary>
|
||||
public int GridSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sorts Apps in the game list // Not Used
|
||||
/// </summary>
|
||||
public int ApplicationSort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets if Grid is ordered in Ascending Order // Not Used
|
||||
/// </summary>
|
||||
public bool IsAscendingOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start games in fullscreen mode
|
||||
/// </summary>
|
||||
public bool StartFullscreen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start games with UI hidden
|
||||
/// </summary>
|
||||
public bool StartNoUI { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show console window
|
||||
/// </summary>
|
||||
public bool ShowConsole { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable keyboard support (Independent from controllers binding)
|
||||
/// </summary>
|
||||
public bool EnableKeyboard { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable mouse support (Independent from controllers binding)
|
||||
/// </summary>
|
||||
public bool EnableMouse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable/disable the ability to control Ryujinx when it's not the currently focused window.
|
||||
/// </summary>
|
||||
public bool DisableInputWhenOutOfFocus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hotkey Keyboard Bindings
|
||||
/// </summary>
|
||||
public KeyboardHotkeys Hotkeys { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Input configurations
|
||||
/// </summary>
|
||||
public List<InputConfig> InputConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The speed of spectrum cycling for the Rainbow LED feature.
|
||||
/// </summary>
|
||||
public float RainbowSpeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Graphics backend
|
||||
/// </summary>
|
||||
public GraphicsBackend GraphicsBackend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Preferred GPU
|
||||
/// </summary>
|
||||
public string PreferredGpu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplayer Mode
|
||||
/// </summary>
|
||||
public MultiplayerMode MultiplayerMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// GUID for the network interface used by LAN (or 0 for default)
|
||||
/// </summary>
|
||||
public string MultiplayerLanInterfaceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disable P2p Toggle
|
||||
/// </summary>
|
||||
public bool MultiplayerDisableP2p { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Local network passphrase, for private networks.
|
||||
/// </summary>
|
||||
public string MultiplayerLdnPassphrase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom LDN Server
|
||||
/// </summary>
|
||||
public string LdnServer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Uses Hypervisor over JIT if available
|
||||
/// </summary>
|
||||
public bool UseHypervisor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show toggles for dirty hacks in the UI.
|
||||
/// </summary>
|
||||
public bool ShowDirtyHacks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The packed values of the enabled dirty hacks.
|
||||
/// </summary>
|
||||
public ulong[] DirtyHacks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Loads a configuration file from disk
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the JSON configuration file</param>
|
||||
/// <param name="configurationFileFormat">Parsed configuration file</param>
|
||||
public static bool TryLoad(string path, out ConfigurationFileFormat configurationFileFormat)
|
||||
{
|
||||
try
|
||||
{
|
||||
configurationFileFormat = JsonHelper.DeserializeFromFile(path, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat);
|
||||
|
||||
return configurationFileFormat.Version != 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
configurationFileFormat = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a configuration file to disk
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the JSON configuration file</param>
|
||||
public void SaveConfig(string path)
|
||||
{
|
||||
JsonHelper.SerializeToFile(path, this, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
internal static class ConfigurationFileFormatSettings
|
||||
{
|
||||
public static readonly ConfigurationJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(ConfigurationFileFormat))]
|
||||
internal partial class ConfigurationJsonSerializerContext : JsonSerializerContext;
|
||||
}
|
|
@ -0,0 +1,447 @@
|
|||
using Avalonia.Media;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Systems.Configuration.System;
|
||||
using Ryujinx.Ava.Systems.Configuration.UI;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
public partial class ConfigurationState
|
||||
{
|
||||
public void Load(ConfigurationFileFormat cff, string configurationFilePath, string titleId = "")
|
||||
{
|
||||
bool configurationFileUpdated = false;
|
||||
bool shouldLoadFromFile = string.IsNullOrEmpty(titleId);
|
||||
|
||||
if (cff.Version is < 0 or > ConfigurationFileFormat.CurrentVersion)
|
||||
{
|
||||
RyuLogger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {cff.Version}, loading default.");
|
||||
|
||||
LoadDefault();
|
||||
}
|
||||
|
||||
foreach ((int newVersion, Action<ConfigurationFileFormat> migratorFunction)
|
||||
in _migrations.OrderBy(x => x.Key))
|
||||
{
|
||||
if (cff.Version >= newVersion)
|
||||
continue;
|
||||
|
||||
RyuLogger.Warning?.Print(LogClass.Application,
|
||||
$"Outdated configuration version {cff.Version}, migrating to version {newVersion}.");
|
||||
|
||||
migratorFunction(cff);
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
|
||||
EnableDiscordIntegration.Value = cff.EnableDiscordIntegration;
|
||||
CheckUpdatesOnStart.Value = shouldLoadFromFile ? cff.CheckUpdatesOnStart : CheckUpdatesOnStart.Value; // Get from global config only
|
||||
UpdateCheckerType.Value = shouldLoadFromFile ? cff.UpdateCheckerType : UpdateCheckerType.Value; // Get from global config only
|
||||
FocusLostActionType.Value = cff.FocusLostActionType;
|
||||
ShowConfirmExit.Value = shouldLoadFromFile ? cff.ShowConfirmExit : ShowConfirmExit.Value; // Get from global config only
|
||||
RememberWindowState.Value = shouldLoadFromFile ? cff.RememberWindowState : RememberWindowState.Value; // Get from global config only
|
||||
ShowTitleBar.Value = shouldLoadFromFile ? cff.ShowTitleBar : ShowTitleBar.Value; // Get from global config only
|
||||
EnableHardwareAcceleration.Value = shouldLoadFromFile ? cff.EnableHardwareAcceleration : EnableHardwareAcceleration.Value; // Get from global config only
|
||||
HideCursor.Value = cff.HideCursor;
|
||||
|
||||
Logger.EnableFileLog.Value = cff.EnableFileLog;
|
||||
Logger.EnableDebug.Value = cff.LoggingEnableDebug;
|
||||
Logger.EnableStub.Value = cff.LoggingEnableStub;
|
||||
Logger.EnableInfo.Value = cff.LoggingEnableInfo;
|
||||
Logger.EnableWarn.Value = cff.LoggingEnableWarn;
|
||||
Logger.EnableError.Value = cff.LoggingEnableError;
|
||||
Logger.EnableTrace.Value = cff.LoggingEnableTrace;
|
||||
Logger.EnableGuest.Value = cff.LoggingEnableGuest;
|
||||
Logger.EnableFsAccessLog.Value = cff.LoggingEnableFsAccessLog;
|
||||
Logger.FilteredClasses.Value = cff.LoggingFilteredClasses;
|
||||
Logger.GraphicsDebugLevel.Value = cff.LoggingGraphicsDebugLevel;
|
||||
|
||||
Graphics.ResScale.Value = cff.ResScale;
|
||||
Graphics.ResScaleCustom.Value = cff.ResScaleCustom;
|
||||
Graphics.MaxAnisotropy.Value = cff.MaxAnisotropy;
|
||||
Graphics.AspectRatio.Value = cff.AspectRatio;
|
||||
Graphics.ShadersDumpPath.Value = cff.GraphicsShadersDumpPath;
|
||||
Graphics.BackendThreading.Value = cff.BackendThreading;
|
||||
Graphics.GraphicsBackend.Value = cff.GraphicsBackend;
|
||||
Graphics.PreferredGpu.Value = cff.PreferredGpu;
|
||||
Graphics.AntiAliasing.Value = cff.AntiAliasing;
|
||||
Graphics.ScalingFilter.Value = cff.ScalingFilter;
|
||||
Graphics.ScalingFilterLevel.Value = cff.ScalingFilterLevel;
|
||||
Graphics.VSyncMode.Value = cff.VSyncMode;
|
||||
Graphics.EnableCustomVSyncInterval.Value = cff.EnableCustomVSyncInterval;
|
||||
Graphics.CustomVSyncInterval.Value = cff.CustomVSyncInterval;
|
||||
Graphics.EnableShaderCache.Value = cff.EnableShaderCache;
|
||||
Graphics.EnableTextureRecompression.Value = cff.EnableTextureRecompression;
|
||||
Graphics.EnableMacroHLE.Value = cff.EnableMacroHLE;
|
||||
Graphics.EnableColorSpacePassthrough.Value = cff.EnableColorSpacePassthrough;
|
||||
|
||||
System.Language.Value = cff.SystemLanguage;
|
||||
System.Region.Value = cff.SystemRegion;
|
||||
System.TimeZone.Value = cff.SystemTimeZone;
|
||||
System.SystemTimeOffset.Value = shouldLoadFromFile ? cff.SystemTimeOffset : System.SystemTimeOffset.Value; // Get from global config only
|
||||
System.MatchSystemTime.Value = shouldLoadFromFile ? cff.MatchSystemTime : System.MatchSystemTime.Value; // Get from global config only
|
||||
System.EnableDockedMode.Value = cff.DockedMode;
|
||||
System.EnablePtc.Value = cff.EnablePtc;
|
||||
System.EnableLowPowerPtc.Value = cff.EnableLowPowerPtc;
|
||||
System.EnableInternetAccess.Value = cff.EnableInternetAccess;
|
||||
System.EnableFsIntegrityChecks.Value = cff.EnableFsIntegrityChecks;
|
||||
System.FsGlobalAccessLogMode.Value = cff.FsGlobalAccessLogMode;
|
||||
System.AudioBackend.Value = cff.AudioBackend;
|
||||
System.AudioVolume.Value = cff.AudioVolume;
|
||||
System.MemoryManagerMode.Value = cff.MemoryManagerMode;
|
||||
System.DramSize.Value = cff.DramSize;
|
||||
System.IgnoreMissingServices.Value = cff.IgnoreMissingServices;
|
||||
System.IgnoreControllerApplet.Value = cff.IgnoreApplet;
|
||||
System.UseHypervisor.Value = cff.UseHypervisor;
|
||||
|
||||
UI.GuiColumns.FavColumn.Value = shouldLoadFromFile ? cff.GuiColumns.FavColumn : UI.GuiColumns.FavColumn.Value;
|
||||
UI.GuiColumns.IconColumn.Value = shouldLoadFromFile ? cff.GuiColumns.IconColumn : UI.GuiColumns.IconColumn.Value;
|
||||
UI.GuiColumns.AppColumn.Value = shouldLoadFromFile ? cff.GuiColumns.AppColumn : UI.GuiColumns.AppColumn.Value;
|
||||
UI.GuiColumns.DevColumn.Value = shouldLoadFromFile ? cff.GuiColumns.DevColumn : UI.GuiColumns.DevColumn.Value;
|
||||
UI.GuiColumns.VersionColumn.Value = shouldLoadFromFile ? cff.GuiColumns.VersionColumn : UI.GuiColumns.VersionColumn.Value;
|
||||
UI.GuiColumns.TimePlayedColumn.Value = shouldLoadFromFile ? cff.GuiColumns.TimePlayedColumn : UI.GuiColumns.TimePlayedColumn.Value;
|
||||
UI.GuiColumns.LastPlayedColumn.Value = shouldLoadFromFile ? cff.GuiColumns.LastPlayedColumn : UI.GuiColumns.LastPlayedColumn.Value;
|
||||
UI.GuiColumns.FileExtColumn.Value = shouldLoadFromFile ? cff.GuiColumns.FileExtColumn : UI.GuiColumns.FileExtColumn.Value;
|
||||
UI.GuiColumns.FileSizeColumn.Value = shouldLoadFromFile ? cff.GuiColumns.FileSizeColumn : UI.GuiColumns.FileSizeColumn.Value;
|
||||
UI.GuiColumns.PathColumn.Value = shouldLoadFromFile ? cff.GuiColumns.PathColumn : UI.GuiColumns.PathColumn.Value;
|
||||
UI.ColumnSort.SortColumnId.Value = shouldLoadFromFile ? cff.ColumnSort.SortColumnId : UI.ColumnSort.SortColumnId.Value;
|
||||
UI.ColumnSort.SortAscending.Value = shouldLoadFromFile ? cff.ColumnSort.SortAscending : UI.ColumnSort.SortAscending.Value;
|
||||
UI.GameDirs.Value = shouldLoadFromFile ? cff.GameDirs : UI.GameDirs.Value;
|
||||
UI.AutoloadDirs.Value = shouldLoadFromFile ? (cff.AutoloadDirs ?? []) : UI.AutoloadDirs.Value;
|
||||
UI.ShownFileTypes.NSP.Value = shouldLoadFromFile ? cff.ShownFileTypes.NSP : UI.ShownFileTypes.NSP.Value;
|
||||
UI.ShownFileTypes.PFS0.Value = shouldLoadFromFile ? cff.ShownFileTypes.PFS0 : UI.ShownFileTypes.PFS0.Value;
|
||||
UI.ShownFileTypes.XCI.Value = shouldLoadFromFile ? cff.ShownFileTypes.XCI : UI.ShownFileTypes.XCI.Value;
|
||||
UI.ShownFileTypes.NCA.Value = shouldLoadFromFile ? cff.ShownFileTypes.NCA : UI.ShownFileTypes.NCA.Value;
|
||||
UI.ShownFileTypes.NRO.Value = shouldLoadFromFile ? cff.ShownFileTypes.NRO : UI.ShownFileTypes.NRO.Value;
|
||||
UI.ShownFileTypes.NSO.Value = shouldLoadFromFile ? cff.ShownFileTypes.NSO : UI.ShownFileTypes.NSO.Value;
|
||||
UI.LanguageCode.Value = shouldLoadFromFile ? cff.LanguageCode : UI.LanguageCode.Value;
|
||||
UI.BaseStyle.Value = shouldLoadFromFile ? cff.BaseStyle : UI.BaseStyle.Value;
|
||||
UI.GameListViewMode.Value = shouldLoadFromFile ? cff.GameListViewMode : UI.GameListViewMode.Value;
|
||||
UI.ShowNames.Value = shouldLoadFromFile ? cff.ShowNames : UI.ShowNames.Value;
|
||||
UI.IsAscendingOrder.Value = shouldLoadFromFile ? cff.IsAscendingOrder : UI.IsAscendingOrder.Value;
|
||||
UI.GridSize.Value = shouldLoadFromFile ? cff.GridSize : UI.GridSize.Value;
|
||||
UI.ApplicationSort.Value = shouldLoadFromFile ? cff.ApplicationSort : UI.ApplicationSort.Value;
|
||||
UI.StartFullscreen.Value = shouldLoadFromFile ? cff.StartFullscreen : UI.StartFullscreen.Value;
|
||||
UI.StartNoUI.Value = shouldLoadFromFile ? cff.StartNoUI : UI.StartNoUI.Value;
|
||||
UI.ShowConsole.Value = shouldLoadFromFile ? cff.ShowConsole : UI.ShowConsole.Value;
|
||||
UI.WindowStartup.WindowSizeWidth.Value = shouldLoadFromFile ? cff.WindowStartup.WindowSizeWidth : UI.WindowStartup.WindowSizeWidth.Value;
|
||||
UI.WindowStartup.WindowSizeHeight.Value = shouldLoadFromFile ? cff.WindowStartup.WindowSizeHeight : UI.WindowStartup.WindowSizeHeight.Value;
|
||||
UI.WindowStartup.WindowPositionX.Value = shouldLoadFromFile ? cff.WindowStartup.WindowPositionX : UI.WindowStartup.WindowPositionX.Value;
|
||||
UI.WindowStartup.WindowPositionY.Value = shouldLoadFromFile ? cff.WindowStartup.WindowPositionY : UI.WindowStartup.WindowPositionY.Value;
|
||||
UI.WindowStartup.WindowMaximized.Value = shouldLoadFromFile ? cff.WindowStartup.WindowMaximized : UI.WindowStartup.WindowMaximized.Value;
|
||||
|
||||
|
||||
Hid.EnableKeyboard.Value = cff.EnableKeyboard;
|
||||
Hid.EnableMouse.Value = cff.EnableMouse;
|
||||
Hid.DisableInputWhenOutOfFocus.Value = shouldLoadFromFile ? cff.DisableInputWhenOutOfFocus: Hid.DisableInputWhenOutOfFocus.Value; // Get from global config only
|
||||
Hid.Hotkeys.Value = shouldLoadFromFile ? cff.Hotkeys : Hid.Hotkeys.Value; // Get from global config only
|
||||
Hid.InputConfig.Value = cff.InputConfig ?? [];
|
||||
Hid.RainbowSpeed.Value = cff.RainbowSpeed;
|
||||
|
||||
Multiplayer.LanInterfaceId.Value = cff.MultiplayerLanInterfaceId;
|
||||
Multiplayer.Mode.Value = cff.MultiplayerMode;
|
||||
Multiplayer.DisableP2p.Value = cff.MultiplayerDisableP2p;
|
||||
Multiplayer.LdnPassphrase.Value = cff.MultiplayerLdnPassphrase;
|
||||
Multiplayer.LdnServer.Value = cff.LdnServer;
|
||||
|
||||
{
|
||||
Hacks.ShowDirtyHacks.Value = cff.ShowDirtyHacks;
|
||||
|
||||
DirtyHacks hacks = new (cff.DirtyHacks ?? []);
|
||||
|
||||
Hacks.Xc2MenuSoftlockFix.Value = hacks.IsEnabled(DirtyHack.Xc2MenuSoftlockFix);
|
||||
|
||||
Hacks.EnableShaderTranslationDelay.Value = hacks.IsEnabled(DirtyHack.ShaderTranslationDelay);
|
||||
Hacks.ShaderTranslationDelay.Value = hacks[DirtyHack.ShaderTranslationDelay].CoerceAtLeast(0);
|
||||
}
|
||||
|
||||
if (configurationFileUpdated)
|
||||
{
|
||||
ToFileFormat().SaveConfig(configurationFilePath);
|
||||
|
||||
RyuLogger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}");
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<int, Action<ConfigurationFileFormat>> _migrations =
|
||||
Collections.NewDictionary<int, Action<ConfigurationFileFormat>>(
|
||||
(2, static cff => cff.SystemRegion = Region.USA),
|
||||
(3, static cff => cff.SystemTimeZone = "UTC"),
|
||||
(4, static cff => cff.MaxAnisotropy = -1),
|
||||
(5, static cff => cff.SystemTimeOffset = 0),
|
||||
(8, static cff => cff.EnablePtc = true),
|
||||
(9, static cff =>
|
||||
{
|
||||
cff.ColumnSort = new ColumnSort { SortColumnId = 0, SortAscending = false };
|
||||
cff.Hotkeys = new KeyboardHotkeys { ToggleVSyncMode = Key.F1 };
|
||||
}),
|
||||
(10, static cff => cff.AudioBackend = AudioBackend.OpenAl),
|
||||
(11, static cff =>
|
||||
{
|
||||
cff.ResScale = 1;
|
||||
cff.ResScaleCustom = 1.0f;
|
||||
}),
|
||||
(12, static cff => cff.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None),
|
||||
// 13 -> LDN1
|
||||
(14, static cff => cff.CheckUpdatesOnStart = true),
|
||||
(16, static cff => cff.EnableShaderCache = true),
|
||||
(17, static cff => cff.StartFullscreen = false),
|
||||
(18, static cff => cff.AspectRatio = AspectRatio.Fixed16x9),
|
||||
// 19 -> LDN2
|
||||
(20, static cff => cff.ShowConfirmExit = true),
|
||||
(21, static cff =>
|
||||
{
|
||||
// Initialize network config.
|
||||
|
||||
cff.MultiplayerMode = MultiplayerMode.Disabled;
|
||||
cff.MultiplayerLanInterfaceId = "0";
|
||||
}),
|
||||
(22, static cff => cff.HideCursor = HideCursorMode.Never),
|
||||
(24, static cff =>
|
||||
{
|
||||
cff.InputConfig =
|
||||
[
|
||||
new StandardKeyboardInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.WindowKeyboard,
|
||||
Id = "0",
|
||||
PlayerIndex = PlayerIndex.Player1,
|
||||
ControllerType = ControllerType.ProController,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H,
|
||||
},
|
||||
}
|
||||
];
|
||||
}),
|
||||
(26, static cff => cff.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe),
|
||||
(27, static cff => cff.EnableMouse = false),
|
||||
(29,
|
||||
static cff =>
|
||||
cff.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVSyncMode = Key.F1, Screenshot = Key.F8, ShowUI = Key.F4
|
||||
}),
|
||||
(30, static cff =>
|
||||
{
|
||||
foreach (StandardControllerInputConfig config in cff.InputConfig.OfType<StandardControllerInputConfig>())
|
||||
{
|
||||
config.Rumble = new RumbleConfigController
|
||||
{
|
||||
EnableRumble = false, StrongRumble = 1f, WeakRumble = 1f,
|
||||
};
|
||||
}
|
||||
}),
|
||||
(31, static cff => cff.BackendThreading = BackendThreading.Auto),
|
||||
(32, static cff => cff.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVSyncMode = cff.Hotkeys.ToggleVSyncMode,
|
||||
Screenshot = cff.Hotkeys.Screenshot,
|
||||
ShowUI = cff.Hotkeys.ShowUI,
|
||||
Pause = Key.F5,
|
||||
}),
|
||||
(33, static cff =>
|
||||
{
|
||||
cff.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVSyncMode = cff.Hotkeys.ToggleVSyncMode,
|
||||
Screenshot = cff.Hotkeys.Screenshot,
|
||||
ShowUI = cff.Hotkeys.ShowUI,
|
||||
Pause = cff.Hotkeys.Pause,
|
||||
ToggleMute = Key.F2,
|
||||
};
|
||||
|
||||
cff.AudioVolume = 1;
|
||||
}),
|
||||
(34, static cff => cff.EnableInternetAccess = false),
|
||||
(35, static cff =>
|
||||
{
|
||||
foreach (StandardControllerInputConfig config in cff.InputConfig
|
||||
.OfType<StandardControllerInputConfig>())
|
||||
{
|
||||
config.RangeLeft = 1.0f;
|
||||
config.RangeRight = 1.0f;
|
||||
}
|
||||
}),
|
||||
|
||||
(36, static cff => cff.LoggingEnableTrace = false),
|
||||
(37, static cff => cff.ShowConsole = true),
|
||||
(38, static cff =>
|
||||
{
|
||||
cff.BaseStyle = "Dark";
|
||||
cff.GameListViewMode = 0;
|
||||
cff.ShowNames = true;
|
||||
cff.GridSize = 2;
|
||||
cff.LanguageCode = "en_US";
|
||||
}),
|
||||
(39,
|
||||
static cff => cff.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVSyncMode = cff.Hotkeys.ToggleVSyncMode,
|
||||
Screenshot = cff.Hotkeys.Screenshot,
|
||||
ShowUI = cff.Hotkeys.ShowUI,
|
||||
Pause = cff.Hotkeys.Pause,
|
||||
ToggleMute = cff.Hotkeys.ToggleMute,
|
||||
ResScaleUp = Key.Unbound,
|
||||
ResScaleDown = Key.Unbound
|
||||
}),
|
||||
(40, static cff => cff.GraphicsBackend = GraphicsBackend.OpenGl),
|
||||
(41,
|
||||
static cff => cff.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVSyncMode = cff.Hotkeys.ToggleVSyncMode,
|
||||
Screenshot = cff.Hotkeys.Screenshot,
|
||||
ShowUI = cff.Hotkeys.ShowUI,
|
||||
Pause = cff.Hotkeys.Pause,
|
||||
ToggleMute = cff.Hotkeys.ToggleMute,
|
||||
ResScaleUp = cff.Hotkeys.ResScaleUp,
|
||||
ResScaleDown = cff.Hotkeys.ResScaleDown,
|
||||
VolumeUp = Key.Unbound,
|
||||
VolumeDown = Key.Unbound
|
||||
}),
|
||||
(42, static cff => cff.EnableMacroHLE = true),
|
||||
(43, static cff => cff.UseHypervisor = true),
|
||||
(44, static cff =>
|
||||
{
|
||||
cff.AntiAliasing = AntiAliasing.None;
|
||||
cff.ScalingFilter = ScalingFilter.Bilinear;
|
||||
cff.ScalingFilterLevel = 80;
|
||||
}),
|
||||
(45,
|
||||
static cff => cff.ShownFileTypes = new ShownFileTypes
|
||||
{
|
||||
NSP = true,
|
||||
PFS0 = true,
|
||||
XCI = true,
|
||||
NCA = true,
|
||||
NRO = true,
|
||||
NSO = true
|
||||
}),
|
||||
(46, static cff => cff.UseHypervisor = OperatingSystem.IsMacOS()),
|
||||
(47,
|
||||
static cff => cff.WindowStartup = new WindowStartup
|
||||
{
|
||||
WindowPositionX = 0,
|
||||
WindowPositionY = 0,
|
||||
WindowSizeHeight = 760,
|
||||
WindowSizeWidth = 1280,
|
||||
WindowMaximized = false
|
||||
}),
|
||||
(48, static cff => cff.EnableColorSpacePassthrough = false),
|
||||
(49, static _ =>
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
AppDataManager.FixMacOSConfigurationFolders();
|
||||
}
|
||||
}),
|
||||
(50, static cff => cff.EnableHardwareAcceleration = true),
|
||||
(51, static cff => cff.RememberWindowState = true),
|
||||
(52, static cff => cff.AutoloadDirs = []),
|
||||
(53, static cff => cff.EnableLowPowerPtc = false),
|
||||
(54, static cff => cff.DramSize = MemoryConfiguration.MemoryConfiguration4GiB),
|
||||
(55, static cff => cff.IgnoreApplet = false),
|
||||
(56, static cff => cff.ShowTitleBar = !OperatingSystem.IsWindows()),
|
||||
(57, static cff =>
|
||||
{
|
||||
cff.VSyncMode = VSyncMode.Switch;
|
||||
cff.EnableCustomVSyncInterval = false;
|
||||
|
||||
cff.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVSyncMode = Key.F1,
|
||||
Screenshot = cff.Hotkeys.Screenshot,
|
||||
ShowUI = cff.Hotkeys.ShowUI,
|
||||
Pause = cff.Hotkeys.Pause,
|
||||
ToggleMute = cff.Hotkeys.ToggleMute,
|
||||
ResScaleUp = cff.Hotkeys.ResScaleUp,
|
||||
ResScaleDown = cff.Hotkeys.ResScaleDown,
|
||||
VolumeUp = cff.Hotkeys.VolumeUp,
|
||||
VolumeDown = cff.Hotkeys.VolumeDown,
|
||||
CustomVSyncIntervalIncrement = Key.Unbound,
|
||||
CustomVSyncIntervalDecrement = Key.Unbound,
|
||||
};
|
||||
|
||||
cff.CustomVSyncInterval = 120;
|
||||
}),
|
||||
// 58 migration accidentally got skipped, but it worked with no issues somehow lol
|
||||
(59, static cff =>
|
||||
{
|
||||
cff.ShowDirtyHacks = false;
|
||||
cff.DirtyHacks = [];
|
||||
|
||||
// This was accidentally enabled by default when it was PRed. That is not what we want,
|
||||
// so as a compromise users who want to use it will simply need to re-enable it once after updating.
|
||||
cff.IgnoreApplet = false;
|
||||
}),
|
||||
(60, static cff => cff.StartNoUI = false),
|
||||
(61, static cff =>
|
||||
{
|
||||
foreach (StandardControllerInputConfig config in cff.InputConfig.OfType<StandardControllerInputConfig>())
|
||||
{
|
||||
config.Led = new LedConfigController
|
||||
{
|
||||
EnableLed = false,
|
||||
TurnOffLed = false,
|
||||
UseRainbow = false,
|
||||
LedColor = new Color(255, 5, 1, 253).ToUInt32()
|
||||
};
|
||||
}
|
||||
}),
|
||||
(62, static cff => cff.RainbowSpeed = 1f),
|
||||
(63, static cff => cff.MatchSystemTime = false),
|
||||
(64, static cff => cff.LoggingEnableAvalonia = false),
|
||||
(65, static cff => cff.UpdateCheckerType = cff.CheckUpdatesOnStart ? UpdaterType.PromptAtStartup : UpdaterType.Off),
|
||||
(66, static cff => cff.DisableInputWhenOutOfFocus = false),
|
||||
(67, static cff => cff.FocusLostActionType = cff.DisableInputWhenOutOfFocus ? FocusLostType.BlockInput : FocusLostType.DoNothing)
|
||||
);
|
||||
}
|
||||
}
|
873
src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
Normal file
873
src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs
Normal file
|
@ -0,0 +1,873 @@
|
|||
using ARMeilleure;
|
||||
using Gommon;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.Ava.Systems.Configuration.System;
|
||||
using Ryujinx.Ava.Systems.Configuration.UI;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.HLE.HOS.SystemState;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
public partial class ConfigurationState
|
||||
{
|
||||
/// <summary>
|
||||
/// UI configuration section
|
||||
/// </summary>
|
||||
public class UISection
|
||||
{
|
||||
public class Columns
|
||||
{
|
||||
public ReactiveObject<bool> FavColumn { get; private set; }
|
||||
public ReactiveObject<bool> IconColumn { get; private set; }
|
||||
public ReactiveObject<bool> AppColumn { get; private set; }
|
||||
public ReactiveObject<bool> DevColumn { get; private set; }
|
||||
public ReactiveObject<bool> VersionColumn { get; private set; }
|
||||
public ReactiveObject<bool> LdnInfoColumn { get; private set; }
|
||||
public ReactiveObject<bool> TimePlayedColumn { get; private set; }
|
||||
public ReactiveObject<bool> LastPlayedColumn { get; private set; }
|
||||
public ReactiveObject<bool> FileExtColumn { get; private set; }
|
||||
public ReactiveObject<bool> FileSizeColumn { get; private set; }
|
||||
public ReactiveObject<bool> PathColumn { get; private set; }
|
||||
|
||||
public Columns()
|
||||
{
|
||||
FavColumn = new ReactiveObject<bool>();
|
||||
IconColumn = new ReactiveObject<bool>();
|
||||
AppColumn = new ReactiveObject<bool>();
|
||||
DevColumn = new ReactiveObject<bool>();
|
||||
VersionColumn = new ReactiveObject<bool>();
|
||||
LdnInfoColumn = new ReactiveObject<bool>();
|
||||
TimePlayedColumn = new ReactiveObject<bool>();
|
||||
LastPlayedColumn = new ReactiveObject<bool>();
|
||||
FileExtColumn = new ReactiveObject<bool>();
|
||||
FileSizeColumn = new ReactiveObject<bool>();
|
||||
PathColumn = new ReactiveObject<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ColumnSortSettings
|
||||
{
|
||||
public ReactiveObject<int> SortColumnId { get; private set; }
|
||||
public ReactiveObject<bool> SortAscending { get; private set; }
|
||||
|
||||
public ColumnSortSettings()
|
||||
{
|
||||
SortColumnId = new ReactiveObject<int>();
|
||||
SortAscending = new ReactiveObject<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to toggle which file types are shown in the UI
|
||||
/// </summary>
|
||||
public class ShownFileTypeSettings
|
||||
{
|
||||
public ReactiveObject<bool> NSP { get; private set; }
|
||||
public ReactiveObject<bool> PFS0 { get; private set; }
|
||||
public ReactiveObject<bool> XCI { get; private set; }
|
||||
public ReactiveObject<bool> NCA { get; private set; }
|
||||
public ReactiveObject<bool> NRO { get; private set; }
|
||||
public ReactiveObject<bool> NSO { get; private set; }
|
||||
|
||||
public ShownFileTypeSettings()
|
||||
{
|
||||
NSP = new ReactiveObject<bool>();
|
||||
PFS0 = new ReactiveObject<bool>();
|
||||
XCI = new ReactiveObject<bool>();
|
||||
NCA = new ReactiveObject<bool>();
|
||||
NRO = new ReactiveObject<bool>();
|
||||
NSO = new ReactiveObject<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
// <summary>
|
||||
/// Determines main window start-up position, size and state
|
||||
///<summary>
|
||||
public class WindowStartupSettings
|
||||
{
|
||||
public ReactiveObject<int> WindowSizeWidth { get; private set; }
|
||||
public ReactiveObject<int> WindowSizeHeight { get; private set; }
|
||||
public ReactiveObject<int> WindowPositionX { get; private set; }
|
||||
public ReactiveObject<int> WindowPositionY { get; private set; }
|
||||
public ReactiveObject<bool> WindowMaximized { get; private set; }
|
||||
|
||||
public WindowStartupSettings()
|
||||
{
|
||||
WindowSizeWidth = new ReactiveObject<int>();
|
||||
WindowSizeHeight = new ReactiveObject<int>();
|
||||
WindowPositionX = new ReactiveObject<int>();
|
||||
WindowPositionY = new ReactiveObject<int>();
|
||||
WindowMaximized = new ReactiveObject<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to toggle columns in the GUI
|
||||
/// </summary>
|
||||
public Columns GuiColumns { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to configure column sort settings in the GUI
|
||||
/// </summary>
|
||||
public ColumnSortSettings ColumnSort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of directories containing games to be used to load games into the games list
|
||||
/// </summary>
|
||||
public ReactiveObject<List<string>> GameDirs { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of directories containing DLC/updates the user wants to autoload during library refreshes
|
||||
/// </summary>
|
||||
public ReactiveObject<List<string>> AutoloadDirs { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of file types to be hidden in the games List
|
||||
/// </summary>
|
||||
public ShownFileTypeSettings ShownFileTypes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines main window start-up position, size and state
|
||||
/// </summary>
|
||||
public WindowStartupSettings WindowStartup { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Language Code for the UI
|
||||
/// </summary>
|
||||
public ReactiveObject<string> LanguageCode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Selects the base style
|
||||
/// </summary>
|
||||
public ReactiveObject<string> BaseStyle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start games in fullscreen mode
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> StartFullscreen { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start games with UI hidden
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> StartNoUI { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hide / Show Console Window
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> ShowConsole { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// View Mode of the Game list
|
||||
/// </summary>
|
||||
public ReactiveObject<int> GameListViewMode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show application name in Grid Mode
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> ShowNames { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets App Icon Size in Grid Mode
|
||||
/// </summary>
|
||||
public ReactiveObject<int> GridSize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sorts Apps in Grid Mode
|
||||
/// </summary>
|
||||
public ReactiveObject<int> ApplicationSort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets if Grid is ordered in Ascending Order
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> IsAscendingOrder { get; private set; }
|
||||
|
||||
public UISection()
|
||||
{
|
||||
GuiColumns = new Columns();
|
||||
ColumnSort = new ColumnSortSettings();
|
||||
GameDirs = new ReactiveObject<List<string>>();
|
||||
AutoloadDirs = new ReactiveObject<List<string>>();
|
||||
ShownFileTypes = new ShownFileTypeSettings();
|
||||
WindowStartup = new WindowStartupSettings();
|
||||
BaseStyle = new ReactiveObject<string>();
|
||||
StartFullscreen = new ReactiveObject<bool>();
|
||||
StartNoUI = new ReactiveObject<bool>();
|
||||
GameListViewMode = new ReactiveObject<int>();
|
||||
ShowNames = new ReactiveObject<bool>();
|
||||
GridSize = new ReactiveObject<int>();
|
||||
ApplicationSort = new ReactiveObject<int>();
|
||||
IsAscendingOrder = new ReactiveObject<bool>();
|
||||
LanguageCode = new ReactiveObject<string>();
|
||||
ShowConsole = new ReactiveObject<bool>();
|
||||
ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logger configuration section
|
||||
/// </summary>
|
||||
public class LoggerSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables printing debug log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableDebug { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing stub log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableStub { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing info log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableInfo { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing warning log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableWarn { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing error log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableError { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing trace log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableTrace { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing guest log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableGuest { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing FS access log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableFsAccessLog { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables log messages from Avalonia
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableAvaloniaLog { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls which log messages are written to the log targets
|
||||
/// </summary>
|
||||
public ReactiveObject<LogClass[]> FilteredClasses { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables logging to a file on disk
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableFileLog { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls which OpenGL log messages are recorded in the log
|
||||
/// </summary>
|
||||
public ReactiveObject<GraphicsDebugLevel> GraphicsDebugLevel { get; private set; }
|
||||
|
||||
public LoggerSection()
|
||||
{
|
||||
EnableDebug = new ReactiveObject<bool>();
|
||||
EnableDebug.LogChangesToValue(nameof(EnableDebug));
|
||||
EnableStub = new ReactiveObject<bool>();
|
||||
EnableInfo = new ReactiveObject<bool>();
|
||||
EnableWarn = new ReactiveObject<bool>();
|
||||
EnableError = new ReactiveObject<bool>();
|
||||
EnableTrace = new ReactiveObject<bool>();
|
||||
EnableGuest = new ReactiveObject<bool>();
|
||||
EnableFsAccessLog = new ReactiveObject<bool>();
|
||||
EnableAvaloniaLog = new ReactiveObject<bool>();
|
||||
FilteredClasses = new ReactiveObject<LogClass[]>();
|
||||
EnableFileLog = new ReactiveObject<bool>();
|
||||
EnableFileLog.LogChangesToValue(nameof(EnableFileLog));
|
||||
GraphicsDebugLevel = new ReactiveObject<GraphicsDebugLevel>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System configuration section
|
||||
/// </summary>
|
||||
public class SystemSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Change System Language
|
||||
/// </summary>
|
||||
public ReactiveObject<Language> Language { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System Region
|
||||
/// </summary>
|
||||
public ReactiveObject<Region> Region { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System TimeZone
|
||||
/// </summary>
|
||||
public ReactiveObject<string> TimeZone { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableDockedMode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables persistent profiled translation cache
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnablePtc { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables low-power persistent profiled translation cache loading
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableLowPowerPtc { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables guest Internet access
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableInternetAccess { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables integrity checks on Game content files
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableFsIntegrityChecks { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables FS access log output to the console. Possible modes are 0-3
|
||||
/// </summary>
|
||||
public ReactiveObject<int> FsGlobalAccessLogMode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selected audio backend
|
||||
/// </summary>
|
||||
public ReactiveObject<AudioBackend> AudioBackend { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The audio backend volume
|
||||
/// </summary>
|
||||
public ReactiveObject<float> AudioVolume { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selected memory manager mode
|
||||
/// </summary>
|
||||
public ReactiveObject<MemoryManagerMode> MemoryManagerMode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the amount of RAM available on the emulated system, and how it is distributed
|
||||
/// </summary>
|
||||
public ReactiveObject<MemoryConfiguration> DramSize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable ignoring missing services
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> IgnoreMissingServices { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ignore Controller Applet
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> IgnoreControllerApplet { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Uses Hypervisor over JIT if available
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> UseHypervisor { get; private set; }
|
||||
|
||||
public SystemSection()
|
||||
{
|
||||
Language = new ReactiveObject<Language>();
|
||||
Language.LogChangesToValue(nameof(Language));
|
||||
Region = new ReactiveObject<Region>();
|
||||
Region.LogChangesToValue(nameof(Region));
|
||||
TimeZone = new ReactiveObject<string>();
|
||||
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>();
|
||||
EnablePtc.LogChangesToValue(nameof(EnablePtc));
|
||||
EnableLowPowerPtc = new ReactiveObject<bool>();
|
||||
EnableLowPowerPtc.LogChangesToValue(nameof(EnableLowPowerPtc));
|
||||
EnableLowPowerPtc.Event += (_, evnt)
|
||||
=> Optimizations.LowPower = evnt.NewValue;
|
||||
EnableInternetAccess = new ReactiveObject<bool>();
|
||||
EnableInternetAccess.LogChangesToValue(nameof(EnableInternetAccess));
|
||||
EnableFsIntegrityChecks = new ReactiveObject<bool>();
|
||||
EnableFsIntegrityChecks.LogChangesToValue(nameof(EnableFsIntegrityChecks));
|
||||
FsGlobalAccessLogMode = new ReactiveObject<int>();
|
||||
FsGlobalAccessLogMode.LogChangesToValue(nameof(FsGlobalAccessLogMode));
|
||||
AudioBackend = new ReactiveObject<AudioBackend>();
|
||||
AudioBackend.LogChangesToValue(nameof(AudioBackend));
|
||||
MemoryManagerMode = new ReactiveObject<MemoryManagerMode>();
|
||||
MemoryManagerMode.LogChangesToValue(nameof(MemoryManagerMode));
|
||||
DramSize = new ReactiveObject<MemoryConfiguration>();
|
||||
DramSize.LogChangesToValue(nameof(DramSize));
|
||||
IgnoreMissingServices = new ReactiveObject<bool>();
|
||||
IgnoreMissingServices.LogChangesToValue(nameof(IgnoreMissingServices));
|
||||
IgnoreControllerApplet = new ReactiveObject<bool>();
|
||||
IgnoreControllerApplet.LogChangesToValue(nameof(IgnoreControllerApplet));
|
||||
AudioVolume = new ReactiveObject<float>();
|
||||
AudioVolume.LogChangesToValue(nameof(AudioVolume));
|
||||
UseHypervisor = new ReactiveObject<bool>();
|
||||
UseHypervisor.LogChangesToValue(nameof(UseHypervisor));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hid configuration section
|
||||
/// </summary>
|
||||
public class HidSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable or disable keyboard support (Independent from controllers binding)
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableKeyboard { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable mouse support (Independent from controllers binding)
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableMouse { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable/disable the ability to control Ryujinx when it's not the currently focused window.
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> DisableInputWhenOutOfFocus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hotkey Keyboard Bindings
|
||||
/// </summary>
|
||||
public ReactiveObject<KeyboardHotkeys> Hotkeys { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Input device configuration.
|
||||
/// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed.
|
||||
/// TODO: Implement a ReactiveList class.
|
||||
/// </summary>
|
||||
public ReactiveObject<List<InputConfig>> InputConfig { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The speed of spectrum cycling for the Rainbow LED feature.
|
||||
/// </summary>
|
||||
public ReactiveObject<float> RainbowSpeed { get; }
|
||||
|
||||
public HidSection()
|
||||
{
|
||||
EnableKeyboard = new ReactiveObject<bool>();
|
||||
EnableMouse = new ReactiveObject<bool>();
|
||||
DisableInputWhenOutOfFocus = new ReactiveObject<bool>();
|
||||
Hotkeys = new ReactiveObject<KeyboardHotkeys>();
|
||||
InputConfig = new ReactiveObject<List<InputConfig>>();
|
||||
RainbowSpeed = new ReactiveObject<float>();
|
||||
RainbowSpeed.Event += (_, args) => Rainbow.Speed = args.NewValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Graphics configuration section
|
||||
/// </summary>
|
||||
public class GraphicsSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime.
|
||||
/// </summary>
|
||||
public ReactiveObject<BackendThreading> BackendThreading { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide.
|
||||
/// </summary>
|
||||
public ReactiveObject<float> MaxAnisotropy { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Aspect Ratio applied to the renderer window.
|
||||
/// </summary>
|
||||
public ReactiveObject<AspectRatio> AspectRatio { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead.
|
||||
/// </summary>
|
||||
public ReactiveObject<int> ResScale { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1.
|
||||
/// </summary>
|
||||
public ReactiveObject<float> ResScaleCustom { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dumps shaders in this local directory
|
||||
/// </summary>
|
||||
public ReactiveObject<string> ShadersDumpPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the present interval mode. Options are Switch (60Hz), Unbounded (previously Vsync off), and Custom, if enabled.
|
||||
/// </summary>
|
||||
public ReactiveObject<VSyncMode> VSyncMode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the custom present interval mode.
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableCustomVSyncInterval { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Changes the custom present interval.
|
||||
/// </summary>
|
||||
public ReactiveObject<int> CustomVSyncInterval { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Shader cache
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableShaderCache { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables texture recompression
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableTextureRecompression { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Macro high-level emulation
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableMacroHLE { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables color space passthrough, if available.
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableColorSpacePassthrough { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Graphics backend
|
||||
/// </summary>
|
||||
public ReactiveObject<GraphicsBackend> GraphicsBackend { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies anti-aliasing to the renderer.
|
||||
/// </summary>
|
||||
public ReactiveObject<AntiAliasing> AntiAliasing { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the framebuffer upscaling type.
|
||||
/// </summary>
|
||||
public ReactiveObject<ScalingFilter> ScalingFilter { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the framebuffer upscaling level.
|
||||
/// </summary>
|
||||
public ReactiveObject<int> ScalingFilterLevel { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Preferred GPU
|
||||
/// </summary>
|
||||
public ReactiveObject<string> PreferredGpu { get; private set; }
|
||||
|
||||
public GraphicsSection()
|
||||
{
|
||||
BackendThreading = new ReactiveObject<BackendThreading>();
|
||||
BackendThreading.LogChangesToValue(nameof(BackendThreading));
|
||||
ResScale = new ReactiveObject<int>();
|
||||
ResScale.LogChangesToValue(nameof(ResScale));
|
||||
ResScaleCustom = new ReactiveObject<float>();
|
||||
ResScaleCustom.LogChangesToValue(nameof(ResScaleCustom));
|
||||
MaxAnisotropy = new ReactiveObject<float>();
|
||||
MaxAnisotropy.LogChangesToValue(nameof(MaxAnisotropy));
|
||||
AspectRatio = new ReactiveObject<AspectRatio>();
|
||||
AspectRatio.LogChangesToValue(nameof(AspectRatio));
|
||||
ShadersDumpPath = new ReactiveObject<string>();
|
||||
VSyncMode = new ReactiveObject<VSyncMode>();
|
||||
VSyncMode.LogChangesToValue(nameof(VSyncMode));
|
||||
EnableCustomVSyncInterval = new ReactiveObject<bool>();
|
||||
EnableCustomVSyncInterval.LogChangesToValue(nameof(EnableCustomVSyncInterval));
|
||||
CustomVSyncInterval = new ReactiveObject<int>();
|
||||
CustomVSyncInterval.LogChangesToValue(nameof(CustomVSyncInterval));
|
||||
EnableShaderCache = new ReactiveObject<bool>();
|
||||
EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache));
|
||||
EnableTextureRecompression = new ReactiveObject<bool>();
|
||||
EnableTextureRecompression.LogChangesToValue(nameof(EnableTextureRecompression));
|
||||
GraphicsBackend = new ReactiveObject<GraphicsBackend>();
|
||||
GraphicsBackend.LogChangesToValue(nameof(GraphicsBackend));
|
||||
PreferredGpu = new ReactiveObject<string>();
|
||||
PreferredGpu.LogChangesToValue(nameof(PreferredGpu));
|
||||
EnableMacroHLE = new ReactiveObject<bool>();
|
||||
EnableMacroHLE.LogChangesToValue(nameof(EnableMacroHLE));
|
||||
EnableColorSpacePassthrough = new ReactiveObject<bool>();
|
||||
EnableColorSpacePassthrough.LogChangesToValue(nameof(EnableColorSpacePassthrough));
|
||||
AntiAliasing = new ReactiveObject<AntiAliasing>();
|
||||
AntiAliasing.LogChangesToValue(nameof(AntiAliasing));
|
||||
ScalingFilter = new ReactiveObject<ScalingFilter>();
|
||||
ScalingFilter.LogChangesToValue(nameof(ScalingFilter));
|
||||
ScalingFilterLevel = new ReactiveObject<int>();
|
||||
ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplayer configuration section
|
||||
/// </summary>
|
||||
public class MultiplayerSection
|
||||
{
|
||||
/// <summary>
|
||||
/// GUID for the network interface used by LAN (or 0 for default)
|
||||
/// </summary>
|
||||
public ReactiveObject<string> LanInterfaceId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplayer Mode
|
||||
/// </summary>
|
||||
public ReactiveObject<MultiplayerMode> Mode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disable P2P
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> DisableP2p { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// LDN PassPhrase
|
||||
/// </summary>
|
||||
public ReactiveObject<string> LdnPassphrase { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// LDN Server
|
||||
/// </summary>
|
||||
public ReactiveObject<string> LdnServer { get; private set; }
|
||||
|
||||
public string GetLdnServer()
|
||||
{
|
||||
string ldnServer = LdnServer;
|
||||
return string.IsNullOrEmpty(ldnServer)
|
||||
? SharedConstants.DefaultLanPlayHost
|
||||
: ldnServer;
|
||||
}
|
||||
|
||||
public MultiplayerSection()
|
||||
{
|
||||
LanInterfaceId = new ReactiveObject<string>();
|
||||
Mode = new ReactiveObject<MultiplayerMode>();
|
||||
Mode.LogChangesToValue(nameof(MultiplayerMode));
|
||||
DisableP2p = new ReactiveObject<bool>();
|
||||
DisableP2p.LogChangesToValue(nameof(DisableP2p));
|
||||
LdnPassphrase = new ReactiveObject<string>();
|
||||
LdnPassphrase.LogChangesToValue(nameof(LdnPassphrase));
|
||||
LdnServer = new ReactiveObject<string>();
|
||||
LdnServer.LogChangesToValue(nameof(LdnServer));
|
||||
}
|
||||
}
|
||||
|
||||
public class HacksSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Show toggles for dirty hacks in the UI.
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> ShowDirtyHacks { get; private set; }
|
||||
|
||||
public ReactiveObject<bool> Xc2MenuSoftlockFix { get; private set; }
|
||||
|
||||
public ReactiveObject<bool> EnableShaderTranslationDelay { get; private set; }
|
||||
|
||||
public ReactiveObject<int> ShaderTranslationDelay { get; private set; }
|
||||
|
||||
public HacksSection()
|
||||
{
|
||||
ShowDirtyHacks = new ReactiveObject<bool>();
|
||||
Xc2MenuSoftlockFix = new ReactiveObject<bool>();
|
||||
Xc2MenuSoftlockFix.Event += HackChanged;
|
||||
EnableShaderTranslationDelay = new ReactiveObject<bool>();
|
||||
EnableShaderTranslationDelay.Event += HackChanged;
|
||||
ShaderTranslationDelay = new ReactiveObject<int>();
|
||||
}
|
||||
|
||||
private void HackChanged(object sender, ReactiveEventArgs<bool> rxe)
|
||||
{
|
||||
if (!ShowDirtyHacks)
|
||||
return;
|
||||
|
||||
string newHacks = EnabledHacks.Select(x => x.Hack)
|
||||
.JoinToString(", ");
|
||||
|
||||
if (newHacks != _lastHackCollection)
|
||||
{
|
||||
RyuLogger.Info?.Print(LogClass.Configuration,
|
||||
$"EnabledDirtyHacks set to: [{newHacks}]", "LogValueChange");
|
||||
|
||||
_lastHackCollection = newHacks;
|
||||
}
|
||||
}
|
||||
|
||||
private static string _lastHackCollection;
|
||||
|
||||
public EnabledDirtyHack[] EnabledHacks
|
||||
{
|
||||
get
|
||||
{
|
||||
List<EnabledDirtyHack> enabledHacks = [];
|
||||
|
||||
if (Xc2MenuSoftlockFix)
|
||||
Apply(DirtyHack.Xc2MenuSoftlockFix);
|
||||
|
||||
if (EnableShaderTranslationDelay)
|
||||
Apply(DirtyHack.ShaderTranslationDelay, ShaderTranslationDelay);
|
||||
|
||||
return enabledHacks.ToArray();
|
||||
|
||||
void Apply(DirtyHack hack, int value = 0)
|
||||
{
|
||||
enabledHacks.Add(new EnabledDirtyHack(hack, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default configuration instance
|
||||
/// </summary>
|
||||
public static ConfigurationState Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The UI section
|
||||
/// </summary>
|
||||
public UISection UI { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Logger section
|
||||
/// </summary>
|
||||
public LoggerSection Logger { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The System section
|
||||
/// </summary>
|
||||
public SystemSection System { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Graphics section
|
||||
/// </summary>
|
||||
public GraphicsSection Graphics { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Hid section
|
||||
/// </summary>
|
||||
public HidSection Hid { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Multiplayer section
|
||||
/// </summary>
|
||||
public MultiplayerSection Multiplayer { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Dirty Hacks section
|
||||
/// </summary>
|
||||
public HacksSection Hacks { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Discord Rich Presence
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableDiscordIntegration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates when Ryujinx starts when enabled
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> CheckUpdatesOnStart { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates when Ryujinx starts when enabled, either prompting when an update is found or just showing a notification.
|
||||
/// </summary>
|
||||
public ReactiveObject<UpdaterType> UpdateCheckerType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// How the emulator should behave when you click off/on the window.
|
||||
/// </summary>
|
||||
public ReactiveObject<FocusLostType> FocusLostActionType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show "Confirm Exit" Dialog
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> ShowConfirmExit { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables save window size, position and state on close.
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> RememberWindowState { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the redesigned title bar
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> ShowTitleBar { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables hardware-accelerated rendering for Avalonia
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableHardwareAcceleration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hide Cursor on Idle
|
||||
/// </summary>
|
||||
public ReactiveObject<HideCursorMode> HideCursor { get; private set; }
|
||||
|
||||
private ConfigurationState()
|
||||
{
|
||||
UI = new UISection();
|
||||
Logger = new LoggerSection();
|
||||
System = new SystemSection();
|
||||
Graphics = new GraphicsSection();
|
||||
Hid = new HidSection();
|
||||
Multiplayer = new MultiplayerSection();
|
||||
Hacks = new HacksSection();
|
||||
EnableDiscordIntegration = new ReactiveObject<bool>();
|
||||
CheckUpdatesOnStart = new ReactiveObject<bool>();
|
||||
UpdateCheckerType = new ReactiveObject<UpdaterType>();
|
||||
FocusLostActionType = new ReactiveObject<FocusLostType>();
|
||||
ShowConfirmExit = new ReactiveObject<bool>();
|
||||
RememberWindowState = new ReactiveObject<bool>();
|
||||
ShowTitleBar = new ReactiveObject<bool>();
|
||||
EnableHardwareAcceleration = new ReactiveObject<bool>();
|
||||
HideCursor = new ReactiveObject<HideCursorMode>();
|
||||
}
|
||||
|
||||
public HleConfiguration CreateHleConfiguration() =>
|
||||
new(
|
||||
System.DramSize,
|
||||
(SystemLanguage)System.Language.Value,
|
||||
(RegionCode)System.Region.Value,
|
||||
Graphics.VSyncMode,
|
||||
System.EnableDockedMode,
|
||||
System.EnablePtc,
|
||||
System.EnableInternetAccess,
|
||||
System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None,
|
||||
System.FsGlobalAccessLogMode,
|
||||
System.MatchSystemTime
|
||||
? 0
|
||||
: System.SystemTimeOffset,
|
||||
System.TimeZone,
|
||||
System.MemoryManagerMode,
|
||||
System.IgnoreMissingServices,
|
||||
Graphics.AspectRatio,
|
||||
System.AudioVolume,
|
||||
System.UseHypervisor,
|
||||
Multiplayer.LanInterfaceId,
|
||||
Multiplayer.Mode,
|
||||
Multiplayer.DisableP2p,
|
||||
Multiplayer.LdnPassphrase,
|
||||
Instance.Multiplayer.GetLdnServer(),
|
||||
Instance.Graphics.CustomVSyncInterval,
|
||||
Instance.Hacks.ShowDirtyHacks ? Instance.Hacks.EnabledHacks : null);
|
||||
}
|
||||
}
|
331
src/Ryujinx/Systems/Configuration/ConfigurationState.cs
Normal file
331
src/Ryujinx/Systems/Configuration/ConfigurationState.cs
Normal file
|
@ -0,0 +1,331 @@
|
|||
using Ryujinx.Ava.Systems.Configuration.System;
|
||||
using Ryujinx.Ava.Systems.Configuration.UI;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
using Ryujinx.Graphics.Vulkan;
|
||||
using Ryujinx.HLE;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
public partial class ConfigurationState
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
if (Instance != null)
|
||||
{
|
||||
throw new InvalidOperationException("Configuration is already initialized");
|
||||
}
|
||||
|
||||
Instance = new ConfigurationState();
|
||||
}
|
||||
|
||||
public ConfigurationFileFormat ToFileFormat()
|
||||
{
|
||||
ConfigurationFileFormat configurationFile = new()
|
||||
{
|
||||
Version = ConfigurationFileFormat.CurrentVersion,
|
||||
BackendThreading = Graphics.BackendThreading,
|
||||
EnableFileLog = Logger.EnableFileLog,
|
||||
ResScale = Graphics.ResScale,
|
||||
ResScaleCustom = Graphics.ResScaleCustom,
|
||||
MaxAnisotropy = Graphics.MaxAnisotropy,
|
||||
AspectRatio = Graphics.AspectRatio,
|
||||
AntiAliasing = Graphics.AntiAliasing,
|
||||
ScalingFilter = Graphics.ScalingFilter,
|
||||
ScalingFilterLevel = Graphics.ScalingFilterLevel,
|
||||
GraphicsShadersDumpPath = Graphics.ShadersDumpPath,
|
||||
LoggingEnableDebug = Logger.EnableDebug,
|
||||
LoggingEnableStub = Logger.EnableStub,
|
||||
LoggingEnableInfo = Logger.EnableInfo,
|
||||
LoggingEnableWarn = Logger.EnableWarn,
|
||||
LoggingEnableError = Logger.EnableError,
|
||||
LoggingEnableTrace = Logger.EnableTrace,
|
||||
LoggingEnableGuest = Logger.EnableGuest,
|
||||
LoggingEnableFsAccessLog = Logger.EnableFsAccessLog,
|
||||
LoggingEnableAvalonia = Logger.EnableAvaloniaLog,
|
||||
LoggingFilteredClasses = Logger.FilteredClasses,
|
||||
LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel,
|
||||
SystemLanguage = System.Language,
|
||||
SystemRegion = System.Region,
|
||||
SystemTimeZone = System.TimeZone,
|
||||
SystemTimeOffset = System.SystemTimeOffset,
|
||||
MatchSystemTime = System.MatchSystemTime,
|
||||
DockedMode = System.EnableDockedMode,
|
||||
EnableDiscordIntegration = EnableDiscordIntegration,
|
||||
CheckUpdatesOnStart = CheckUpdatesOnStart,
|
||||
UpdateCheckerType = UpdateCheckerType,
|
||||
FocusLostActionType = FocusLostActionType,
|
||||
ShowConfirmExit = ShowConfirmExit,
|
||||
RememberWindowState = RememberWindowState,
|
||||
ShowTitleBar = ShowTitleBar,
|
||||
EnableHardwareAcceleration = EnableHardwareAcceleration,
|
||||
HideCursor = HideCursor,
|
||||
VSyncMode = Graphics.VSyncMode,
|
||||
EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval,
|
||||
CustomVSyncInterval = Graphics.CustomVSyncInterval,
|
||||
EnableShaderCache = Graphics.EnableShaderCache,
|
||||
EnableTextureRecompression = Graphics.EnableTextureRecompression,
|
||||
EnableMacroHLE = Graphics.EnableMacroHLE,
|
||||
EnableColorSpacePassthrough = Graphics.EnableColorSpacePassthrough,
|
||||
EnablePtc = System.EnablePtc,
|
||||
EnableLowPowerPtc = System.EnableLowPowerPtc,
|
||||
EnableInternetAccess = System.EnableInternetAccess,
|
||||
EnableFsIntegrityChecks = System.EnableFsIntegrityChecks,
|
||||
FsGlobalAccessLogMode = System.FsGlobalAccessLogMode,
|
||||
AudioBackend = System.AudioBackend,
|
||||
AudioVolume = System.AudioVolume,
|
||||
MemoryManagerMode = System.MemoryManagerMode,
|
||||
DramSize = System.DramSize,
|
||||
IgnoreMissingServices = System.IgnoreMissingServices,
|
||||
IgnoreApplet = System.IgnoreControllerApplet,
|
||||
UseHypervisor = System.UseHypervisor,
|
||||
GuiColumns = new GuiColumns
|
||||
{
|
||||
FavColumn = UI.GuiColumns.FavColumn,
|
||||
IconColumn = UI.GuiColumns.IconColumn,
|
||||
AppColumn = UI.GuiColumns.AppColumn,
|
||||
DevColumn = UI.GuiColumns.DevColumn,
|
||||
VersionColumn = UI.GuiColumns.VersionColumn,
|
||||
LdnInfoColumn = UI.GuiColumns.LdnInfoColumn,
|
||||
TimePlayedColumn = UI.GuiColumns.TimePlayedColumn,
|
||||
LastPlayedColumn = UI.GuiColumns.LastPlayedColumn,
|
||||
FileExtColumn = UI.GuiColumns.FileExtColumn,
|
||||
FileSizeColumn = UI.GuiColumns.FileSizeColumn,
|
||||
PathColumn = UI.GuiColumns.PathColumn,
|
||||
},
|
||||
ColumnSort = new ColumnSort
|
||||
{
|
||||
SortColumnId = UI.ColumnSort.SortColumnId,
|
||||
SortAscending = UI.ColumnSort.SortAscending,
|
||||
},
|
||||
GameDirs = UI.GameDirs,
|
||||
AutoloadDirs = UI.AutoloadDirs,
|
||||
ShownFileTypes = new ShownFileTypes
|
||||
{
|
||||
NSP = UI.ShownFileTypes.NSP,
|
||||
PFS0 = UI.ShownFileTypes.PFS0,
|
||||
XCI = UI.ShownFileTypes.XCI,
|
||||
NCA = UI.ShownFileTypes.NCA,
|
||||
NRO = UI.ShownFileTypes.NRO,
|
||||
NSO = UI.ShownFileTypes.NSO,
|
||||
},
|
||||
WindowStartup = new WindowStartup
|
||||
{
|
||||
WindowSizeWidth = UI.WindowStartup.WindowSizeWidth,
|
||||
WindowSizeHeight = UI.WindowStartup.WindowSizeHeight,
|
||||
WindowPositionX = UI.WindowStartup.WindowPositionX,
|
||||
WindowPositionY = UI.WindowStartup.WindowPositionY,
|
||||
WindowMaximized = UI.WindowStartup.WindowMaximized,
|
||||
},
|
||||
LanguageCode = UI.LanguageCode,
|
||||
BaseStyle = UI.BaseStyle,
|
||||
GameListViewMode = UI.GameListViewMode,
|
||||
ShowNames = UI.ShowNames,
|
||||
GridSize = UI.GridSize,
|
||||
ApplicationSort = UI.ApplicationSort,
|
||||
IsAscendingOrder = UI.IsAscendingOrder,
|
||||
StartFullscreen = UI.StartFullscreen,
|
||||
StartNoUI = UI.StartNoUI,
|
||||
ShowConsole = UI.ShowConsole,
|
||||
EnableKeyboard = Hid.EnableKeyboard,
|
||||
EnableMouse = Hid.EnableMouse,
|
||||
DisableInputWhenOutOfFocus = Hid.DisableInputWhenOutOfFocus,
|
||||
Hotkeys = Hid.Hotkeys,
|
||||
InputConfig = Hid.InputConfig,
|
||||
RainbowSpeed = Hid.RainbowSpeed,
|
||||
GraphicsBackend = Graphics.GraphicsBackend,
|
||||
PreferredGpu = Graphics.PreferredGpu,
|
||||
MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId,
|
||||
MultiplayerMode = Multiplayer.Mode,
|
||||
MultiplayerDisableP2p = Multiplayer.DisableP2p,
|
||||
MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase,
|
||||
LdnServer = Multiplayer.LdnServer,
|
||||
ShowDirtyHacks = Hacks.ShowDirtyHacks,
|
||||
DirtyHacks = Hacks.EnabledHacks.Select(it => it.Pack()).ToArray(),
|
||||
};
|
||||
|
||||
return configurationFile;
|
||||
}
|
||||
|
||||
public void LoadDefault()
|
||||
{
|
||||
Logger.EnableFileLog.Value = true;
|
||||
Graphics.BackendThreading.Value = BackendThreading.Auto;
|
||||
Graphics.ResScale.Value = 1;
|
||||
Graphics.ResScaleCustom.Value = 1.0f;
|
||||
Graphics.MaxAnisotropy.Value = -1.0f;
|
||||
Graphics.AspectRatio.Value = AspectRatio.Fixed16x9;
|
||||
Graphics.GraphicsBackend.Value = DefaultGraphicsBackend();
|
||||
Graphics.PreferredGpu.Value = string.Empty;
|
||||
Graphics.ShadersDumpPath.Value = string.Empty;
|
||||
Logger.EnableDebug.Value = false;
|
||||
Logger.EnableStub.Value = true;
|
||||
Logger.EnableInfo.Value = true;
|
||||
Logger.EnableWarn.Value = true;
|
||||
Logger.EnableError.Value = true;
|
||||
Logger.EnableTrace.Value = false;
|
||||
Logger.EnableGuest.Value = true;
|
||||
Logger.EnableFsAccessLog.Value = false;
|
||||
Logger.EnableAvaloniaLog.Value = false;
|
||||
Logger.FilteredClasses.Value = [];
|
||||
Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None;
|
||||
System.Language.Value = Language.AmericanEnglish;
|
||||
System.Region.Value = Region.USA;
|
||||
System.TimeZone.Value = "UTC";
|
||||
System.SystemTimeOffset.Value = 0;
|
||||
System.EnableDockedMode.Value = true;
|
||||
EnableDiscordIntegration.Value = true;
|
||||
UpdateCheckerType.Value = UpdaterType.PromptAtStartup;
|
||||
FocusLostActionType.Value = FocusLostType.DoNothing;
|
||||
ShowConfirmExit.Value = true;
|
||||
RememberWindowState.Value = true;
|
||||
ShowTitleBar.Value = !OperatingSystem.IsWindows();
|
||||
EnableHardwareAcceleration.Value = true;
|
||||
HideCursor.Value = HideCursorMode.OnIdle;
|
||||
Graphics.VSyncMode.Value = VSyncMode.Switch;
|
||||
Graphics.CustomVSyncInterval.Value = 120;
|
||||
Graphics.EnableCustomVSyncInterval.Value = false;
|
||||
Graphics.EnableShaderCache.Value = true;
|
||||
Graphics.EnableTextureRecompression.Value = false;
|
||||
Graphics.EnableMacroHLE.Value = true;
|
||||
Graphics.EnableColorSpacePassthrough.Value = false;
|
||||
Graphics.AntiAliasing.Value = AntiAliasing.None;
|
||||
Graphics.ScalingFilter.Value = ScalingFilter.Bilinear;
|
||||
Graphics.ScalingFilterLevel.Value = 80;
|
||||
System.EnablePtc.Value = true;
|
||||
System.EnableInternetAccess.Value = false;
|
||||
System.EnableFsIntegrityChecks.Value = true;
|
||||
System.FsGlobalAccessLogMode.Value = 0;
|
||||
System.AudioBackend.Value = AudioBackend.SDL2;
|
||||
System.AudioVolume.Value = 1;
|
||||
System.MemoryManagerMode.Value = MemoryManagerMode.HostMappedUnsafe;
|
||||
System.DramSize.Value = MemoryConfiguration.MemoryConfiguration4GiB;
|
||||
System.IgnoreMissingServices.Value = false;
|
||||
System.IgnoreControllerApplet.Value = false;
|
||||
System.UseHypervisor.Value = true;
|
||||
Multiplayer.LanInterfaceId.Value = "0";
|
||||
Multiplayer.Mode.Value = MultiplayerMode.Disabled;
|
||||
Multiplayer.DisableP2p.Value = false;
|
||||
Multiplayer.LdnPassphrase.Value = string.Empty;
|
||||
Multiplayer.LdnServer.Value = string.Empty;
|
||||
UI.GuiColumns.FavColumn.Value = true;
|
||||
UI.GuiColumns.IconColumn.Value = true;
|
||||
UI.GuiColumns.AppColumn.Value = true;
|
||||
UI.GuiColumns.DevColumn.Value = true;
|
||||
UI.GuiColumns.VersionColumn.Value = true;
|
||||
UI.GuiColumns.TimePlayedColumn.Value = true;
|
||||
UI.GuiColumns.LastPlayedColumn.Value = true;
|
||||
UI.GuiColumns.FileExtColumn.Value = true;
|
||||
UI.GuiColumns.FileSizeColumn.Value = true;
|
||||
UI.GuiColumns.PathColumn.Value = true;
|
||||
UI.ColumnSort.SortColumnId.Value = 0;
|
||||
UI.ColumnSort.SortAscending.Value = false;
|
||||
UI.GameDirs.Value = [];
|
||||
UI.AutoloadDirs.Value = [];
|
||||
UI.ShownFileTypes.NSP.Value = true;
|
||||
UI.ShownFileTypes.PFS0.Value = true;
|
||||
UI.ShownFileTypes.XCI.Value = true;
|
||||
UI.ShownFileTypes.NCA.Value = true;
|
||||
UI.ShownFileTypes.NRO.Value = true;
|
||||
UI.ShownFileTypes.NSO.Value = true;
|
||||
UI.LanguageCode.Value = "en_US";
|
||||
UI.BaseStyle.Value = "Dark";
|
||||
UI.GameListViewMode.Value = 0;
|
||||
UI.ShowNames.Value = true;
|
||||
UI.GridSize.Value = 2;
|
||||
UI.ApplicationSort.Value = 0;
|
||||
UI.IsAscendingOrder.Value = true;
|
||||
UI.StartFullscreen.Value = false;
|
||||
UI.StartNoUI.Value = false;
|
||||
UI.ShowConsole.Value = true;
|
||||
UI.WindowStartup.WindowSizeWidth.Value = 1280;
|
||||
UI.WindowStartup.WindowSizeHeight.Value = 760;
|
||||
UI.WindowStartup.WindowPositionX.Value = 0;
|
||||
UI.WindowStartup.WindowPositionY.Value = 0;
|
||||
UI.WindowStartup.WindowMaximized.Value = false;
|
||||
Hid.EnableKeyboard.Value = false;
|
||||
Hid.EnableMouse.Value = false;
|
||||
Hid.DisableInputWhenOutOfFocus.Value = false;
|
||||
Hid.Hotkeys.Value = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVSyncMode = Key.F1,
|
||||
ToggleMute = Key.F2,
|
||||
Screenshot = Key.F8,
|
||||
ShowUI = Key.F4,
|
||||
Pause = Key.F5,
|
||||
ResScaleUp = Key.Unbound,
|
||||
ResScaleDown = Key.Unbound,
|
||||
VolumeUp = Key.Unbound,
|
||||
VolumeDown = Key.Unbound,
|
||||
};
|
||||
Hid.RainbowSpeed.Value = 1f;
|
||||
Hid.InputConfig.Value =
|
||||
[
|
||||
new StandardKeyboardInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.WindowKeyboard,
|
||||
Id = "0",
|
||||
PlayerIndex = PlayerIndex.Player1,
|
||||
ControllerType = ControllerType.ProController,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H,
|
||||
},
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private static GraphicsBackend DefaultGraphicsBackend()
|
||||
{
|
||||
// Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan.
|
||||
// Checks for if the Vulkan version and featureset is compatible should be performed within VulkanRenderer.
|
||||
if (OperatingSystem.IsMacOS() || VulkanRenderer.GetPhysicalDevices().Length > 0)
|
||||
{
|
||||
return GraphicsBackend.Vulkan;
|
||||
}
|
||||
|
||||
return GraphicsBackend.OpenGl;
|
||||
}
|
||||
}
|
||||
}
|
36
src/Ryujinx/Systems/Configuration/FileTypes.cs
Normal file
36
src/Ryujinx/Systems/Configuration/FileTypes.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
|
||||
using static Ryujinx.Ava.Systems.Configuration.ConfigurationState.UISection;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
public enum FileTypes
|
||||
{
|
||||
NSP,
|
||||
PFS0,
|
||||
XCI,
|
||||
NCA,
|
||||
NRO,
|
||||
NSO
|
||||
}
|
||||
|
||||
public static class FileTypesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current <see cref="ConfigurationState.UISection.ShownFileTypeSettings"/> value for the correlating FileType name.
|
||||
/// </summary>
|
||||
/// <param name="type">The name of the <see cref="ConfigurationState.UISection.ShownFileTypeSettings"/> parameter to get the value of.</param>
|
||||
/// <param name="config">The config instance to get the value from.</param>
|
||||
/// <returns>The current value of the setting. Value is <see langword="true"/> if the file type is to be shown on the games list, <see langword="false"/> otherwise.</returns>
|
||||
public static bool GetConfigValue(this FileTypes type, ConfigurationState.UISection.ShownFileTypeSettings config) => type switch
|
||||
{
|
||||
FileTypes.NSP => config.NSP.Value,
|
||||
FileTypes.PFS0 => config.PFS0.Value,
|
||||
FileTypes.XCI => config.XCI.Value,
|
||||
FileTypes.NCA => config.NCA.Value,
|
||||
FileTypes.NRO => config.NRO.Value,
|
||||
FileTypes.NSO => config.NSO.Value,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
|
||||
};
|
||||
}
|
||||
}
|
78
src/Ryujinx/Systems/Configuration/LoggerModule.cs
Normal file
78
src/Ryujinx/Systems/Configuration/LoggerModule.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Logging.Targets;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
public static class LoggerModule
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
ConfigurationState.Instance.Logger.EnableDebug.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Debug, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableStub.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Stub, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableInfo.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Info, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableWarn.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Warning, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableError.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Error, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableTrace.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Trace, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableGuest.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableFsAccessLog.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.AccessLog, e.NewValue);
|
||||
|
||||
ConfigurationState.Instance.Logger.FilteredClasses.Event += (_, e) =>
|
||||
{
|
||||
bool noFilter = e.NewValue.Length == 0;
|
||||
|
||||
foreach (LogClass logClass in Enum.GetValues<LogClass>())
|
||||
{
|
||||
Logger.SetEnable(logClass, noFilter);
|
||||
}
|
||||
|
||||
foreach (LogClass logClass in e.NewValue)
|
||||
{
|
||||
Logger.SetEnable(logClass, true);
|
||||
}
|
||||
};
|
||||
|
||||
ConfigurationState.Instance.Logger.EnableFileLog.Event += (_, e) =>
|
||||
{
|
||||
if (e.NewValue)
|
||||
{
|
||||
string logDir = AppDataManager.LogsDirPath;
|
||||
FileStream logFile = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(logDir))
|
||||
{
|
||||
logFile = FileLogTarget.PrepareLogFile(logDir);
|
||||
}
|
||||
|
||||
if (logFile == null)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application,
|
||||
"No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable.");
|
||||
Logger.RemoveTarget("file");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.AddTarget(new AsyncLogTargetWrapper(
|
||||
new FileLogTarget("file", logFile),
|
||||
1000
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.RemoveTarget("file");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
28
src/Ryujinx/Systems/Configuration/System/Language.cs
Normal file
28
src/Ryujinx/Systems/Configuration/System/Language.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration.System
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<Language>))]
|
||||
public enum Language
|
||||
{
|
||||
Japanese,
|
||||
AmericanEnglish,
|
||||
French,
|
||||
German,
|
||||
Italian,
|
||||
Spanish,
|
||||
Chinese,
|
||||
Korean,
|
||||
Dutch,
|
||||
Portuguese,
|
||||
Russian,
|
||||
Taiwanese,
|
||||
BritishEnglish,
|
||||
CanadianFrench,
|
||||
LatinAmericanSpanish,
|
||||
SimplifiedChinese,
|
||||
TraditionalChinese,
|
||||
BrazilianPortuguese,
|
||||
}
|
||||
}
|
17
src/Ryujinx/Systems/Configuration/System/Region.cs
Normal file
17
src/Ryujinx/Systems/Configuration/System/Region.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration.System
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<Region>))]
|
||||
public enum Region
|
||||
{
|
||||
Japan,
|
||||
USA,
|
||||
Europe,
|
||||
Australia,
|
||||
China,
|
||||
Korea,
|
||||
Taiwan,
|
||||
}
|
||||
}
|
8
src/Ryujinx/Systems/Configuration/UI/ColumnSort.cs
Normal file
8
src/Ryujinx/Systems/Configuration/UI/ColumnSort.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Ryujinx.Ava.Systems.Configuration.UI
|
||||
{
|
||||
public struct ColumnSort
|
||||
{
|
||||
public int SortColumnId { get; set; }
|
||||
public bool SortAscending { get; set; }
|
||||
}
|
||||
}
|
15
src/Ryujinx/Systems/Configuration/UI/FocusLostType.cs
Normal file
15
src/Ryujinx/Systems/Configuration/UI/FocusLostType.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration.UI
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<FocusLostType>))]
|
||||
public enum FocusLostType
|
||||
{
|
||||
DoNothing,
|
||||
BlockInput,
|
||||
MuteAudio,
|
||||
BlockInputAndMuteAudio,
|
||||
PauseEmulation
|
||||
}
|
||||
}
|
17
src/Ryujinx/Systems/Configuration/UI/GuiColumns.cs
Normal file
17
src/Ryujinx/Systems/Configuration/UI/GuiColumns.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
namespace Ryujinx.Ava.Systems.Configuration.UI
|
||||
{
|
||||
public struct GuiColumns
|
||||
{
|
||||
public bool FavColumn { get; set; }
|
||||
public bool IconColumn { get; set; }
|
||||
public bool AppColumn { get; set; }
|
||||
public bool DevColumn { get; set; }
|
||||
public bool VersionColumn { get; set; }
|
||||
public bool LdnInfoColumn { get; set; }
|
||||
public bool TimePlayedColumn { get; set; }
|
||||
public bool LastPlayedColumn { get; set; }
|
||||
public bool FileExtColumn { get; set; }
|
||||
public bool FileSizeColumn { get; set; }
|
||||
public bool PathColumn { get; set; }
|
||||
}
|
||||
}
|
12
src/Ryujinx/Systems/Configuration/UI/ShownFileTypes.cs
Normal file
12
src/Ryujinx/Systems/Configuration/UI/ShownFileTypes.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace Ryujinx.Ava.Systems.Configuration.UI
|
||||
{
|
||||
public struct ShownFileTypes
|
||||
{
|
||||
public bool NSP { get; set; }
|
||||
public bool PFS0 { get; set; }
|
||||
public bool XCI { get; set; }
|
||||
public bool NCA { get; set; }
|
||||
public bool NRO { get; set; }
|
||||
public bool NSO { get; set; }
|
||||
}
|
||||
}
|
13
src/Ryujinx/Systems/Configuration/UI/UpdaterType.cs
Normal file
13
src/Ryujinx/Systems/Configuration/UI/UpdaterType.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using Ryujinx.Common.Utilities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration.UI
|
||||
{
|
||||
[JsonConverter(typeof(TypedStringEnumConverter<UpdaterType>))]
|
||||
public enum UpdaterType
|
||||
{
|
||||
Off,
|
||||
PromptAtStartup,
|
||||
CheckInBackground
|
||||
}
|
||||
}
|
11
src/Ryujinx/Systems/Configuration/UI/WindowStartup.cs
Normal file
11
src/Ryujinx/Systems/Configuration/UI/WindowStartup.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace Ryujinx.Ava.Systems.Configuration.UI
|
||||
{
|
||||
public struct WindowStartup
|
||||
{
|
||||
public int WindowSizeWidth { get; set; }
|
||||
public int WindowSizeHeight { get; set; }
|
||||
public int WindowPositionX { get; set; }
|
||||
public int WindowPositionY { get; set; }
|
||||
public bool WindowMaximized { get; set; }
|
||||
}
|
||||
}
|
151
src/Ryujinx/Systems/PlayReport/Analyzer.cs
Normal file
151
src/Ryujinx/Systems/PlayReport/Analyzer.cs
Normal file
|
@ -0,0 +1,151 @@
|
|||
using Gommon;
|
||||
using Ryujinx.Ava.Systems.AppLibrary;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.PlayReport
|
||||
{
|
||||
/// <summary>
|
||||
/// The entrypoint for the Play Report analysis system.
|
||||
/// </summary>
|
||||
public class Analyzer
|
||||
{
|
||||
private readonly List<GameSpec> _specs = [];
|
||||
|
||||
public string[] TitleIds => Specs.SelectMany(x => x.TitleIds).ToArray();
|
||||
|
||||
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>
|
||||
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
||||
/// </summary>
|
||||
/// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
|
||||
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
|
||||
public Analyzer AddSpec(string titleId, Func<GameSpec, GameSpec> transform)
|
||||
{
|
||||
if (ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _))
|
||||
return AddSpec(transform(GameSpec.Create(titleId)));
|
||||
|
||||
Logger.Notice.PrintMsg(LogClass.Application,
|
||||
$"Tried to add a {nameof(GameSpec)} with a non-hexadecimal title ID value. Input: '{titleId}'");
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
||||
/// </summary>
|
||||
/// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
|
||||
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
|
||||
public Analyzer AddSpec(string titleId, Action<GameSpec> transform)
|
||||
{
|
||||
if (ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _))
|
||||
return AddSpec(GameSpec.Create(titleId).Apply(transform));
|
||||
|
||||
Logger.Notice.PrintMsg(LogClass.Application,
|
||||
$"Tried to add a {nameof(GameSpec)} with a non-hexadecimal title ID value. Input: '{titleId}'");
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
|
||||
/// </summary>
|
||||
/// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
|
||||
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
|
||||
public Analyzer AddSpec(IEnumerable<string> titleIds,
|
||||
Func<GameSpec, GameSpec> transform)
|
||||
{
|
||||
string[] tids = titleIds.ToArray();
|
||||
if (tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _) && !string.IsNullOrEmpty(x)))
|
||||
return AddSpec(transform(GameSpec.Create(tids)));
|
||||
|
||||
Logger.Notice.PrintMsg(LogClass.Application,
|
||||
$"Tried to add a {nameof(GameSpec)} with a non-hexadecimal title ID value. Input: '{
|
||||
tids.FormatCollection(
|
||||
x => x,
|
||||
separator: ", ",
|
||||
prefix: "[",
|
||||
suffix: "]"
|
||||
)
|
||||
}'");
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
|
||||
/// </summary>
|
||||
/// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
|
||||
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
|
||||
public Analyzer AddSpec(IEnumerable<string> titleIds, Action<GameSpec> transform)
|
||||
{
|
||||
string[] tids = titleIds.ToArray();
|
||||
if (tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _) && !string.IsNullOrEmpty(x)))
|
||||
return AddSpec(GameSpec.Create(tids).Apply(transform));
|
||||
|
||||
Logger.Notice.PrintMsg(LogClass.Application,
|
||||
$"Tried to add a {nameof(GameSpec)} with a non-hexadecimal title ID value. Input: '{
|
||||
tids.FormatCollection(
|
||||
x => x,
|
||||
separator: ", ",
|
||||
prefix: "[",
|
||||
suffix: "]"
|
||||
)
|
||||
}'");
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an analysis spec matching a specific game by title ID, with the provided pre-configured spec.
|
||||
/// </summary>
|
||||
/// <param name="spec">The <see cref="GameSpec"/> to add.</param>
|
||||
/// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
|
||||
public Analyzer AddSpec(GameSpec spec)
|
||||
{
|
||||
_specs.Add(spec);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Runs the configured <see cref="FormatterSpec"/> for the specified game title ID.
|
||||
/// </summary>
|
||||
/// <param name="runningGameId">The game currently running.</param>
|
||||
/// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param>
|
||||
/// <param name="playReport">The Play Report received from HLE.</param>
|
||||
/// <returns>A struct representing a possible formatted value.</returns>
|
||||
public FormattedValue Format(
|
||||
string runningGameId,
|
||||
ApplicationMetadata appMeta,
|
||||
Horizon.Prepo.Types.PlayReport playReport
|
||||
)
|
||||
{
|
||||
if (!playReport.ReportData.IsDictionary)
|
||||
return FormattedValue.Unhandled;
|
||||
|
||||
if (!TryGetSpec(runningGameId, out GameSpec spec))
|
||||
return FormattedValue.Unhandled;
|
||||
|
||||
foreach (FormatterSpecBase formatSpec in spec.ValueFormatters.OrderBy(x => x.Priority))
|
||||
{
|
||||
if (!formatSpec.TryFormat(appMeta, playReport, out FormattedValue value))
|
||||
continue;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return FormattedValue.Unhandled;
|
||||
}
|
||||
}
|
||||
}
|
40
src/Ryujinx/Systems/PlayReport/Delegates.cs
Normal file
40
src/Ryujinx/Systems/PlayReport/Delegates.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
namespace Ryujinx.Ava.Systems.PlayReport
|
||||
{
|
||||
/// <summary>
|
||||
/// The delegate type that powers single value formatters.<br/>
|
||||
/// Takes in the result value from the Play Report, and outputs:
|
||||
/// <br/>
|
||||
/// a formatted string,
|
||||
/// <br/>
|
||||
/// a signal that nothing was available to handle it,
|
||||
/// <br/>
|
||||
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||
/// </summary>
|
||||
public delegate FormattedValue SingleValueFormatter(SingleValue value);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type that powers multiple value formatters.<br/>
|
||||
/// Takes in the result values from the Play Report, and outputs:
|
||||
/// <br/>
|
||||
/// a formatted string,
|
||||
/// <br/>
|
||||
/// a signal that nothing was available to handle it,
|
||||
/// <br/>
|
||||
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||
/// </summary>
|
||||
public delegate FormattedValue MultiValueFormatter(MultiValue value);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type that powers multiple value formatters.
|
||||
/// The dictionary passed to this delegate is sparsely populated;
|
||||
/// that is, not every key specified in the Play Report needs to match for this to be used.<br/>
|
||||
/// Takes in the result values from the Play Report, and outputs:
|
||||
/// <br/>
|
||||
/// a formatted string,
|
||||
/// <br/>
|
||||
/// a signal that nothing was available to handle it,
|
||||
/// <br/>
|
||||
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||
/// </summary>
|
||||
public delegate FormattedValue SparseMultiValueFormatter(SparseMultiValue value);
|
||||
}
|
73
src/Ryujinx/Systems/PlayReport/MatchedValues.cs
Normal file
73
src/Ryujinx/Systems/PlayReport/MatchedValues.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
using MsgPack;
|
||||
using Ryujinx.Ava.Systems.AppLibrary;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.PlayReport
|
||||
{
|
||||
public abstract class MatchedValue<T>
|
||||
{
|
||||
protected MatchedValue(T matched)
|
||||
{
|
||||
Matched = matched;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The currently running application's <see cref="ApplicationMetadata"/>.
|
||||
/// </summary>
|
||||
public ApplicationMetadata Application { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The entire play report.
|
||||
/// </summary>
|
||||
public Horizon.Prepo.Types.PlayReport PlayReport { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The matched value from the Play Report.
|
||||
/// </summary>
|
||||
public T Matched { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The input data to a <see cref="SingleValueFormatter"/>,
|
||||
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
||||
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
||||
/// </summary>
|
||||
public class SingleValue : MatchedValue<Value>
|
||||
{
|
||||
public SingleValue(Value matched) : base(matched)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The input data to a <see cref="MultiValueFormatter"/>,
|
||||
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
||||
/// and the matched <see cref="MessagePackObject"/>s from the Play Report.
|
||||
/// </summary>
|
||||
public class MultiValue : MatchedValue<Value[]>
|
||||
{
|
||||
public MultiValue(Value[] matched) : base(matched)
|
||||
{
|
||||
}
|
||||
|
||||
public MultiValue(IEnumerable<MessagePackObject> matched) : base(Value.ConvertPackedObjects(matched))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The input data to a <see cref="SparseMultiValueFormatter"/>,
|
||||
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
||||
/// and the matched <see cref="MessagePackObject"/>s from the Play Report.
|
||||
/// </summary>
|
||||
public class SparseMultiValue : MatchedValue<Dictionary<string, Value>>
|
||||
{
|
||||
public SparseMultiValue(Dictionary<string, Value> matched) : base(matched)
|
||||
{
|
||||
}
|
||||
|
||||
public SparseMultiValue(Dictionary<string, MessagePackObject> matched) : base(Value.ConvertPackedObjectMap(matched))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
645
src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs
Normal file
645
src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs
Normal file
|
@ -0,0 +1,645 @@
|
|||
using Gommon;
|
||||
using Humanizer;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.PlayReport
|
||||
{
|
||||
public partial class PlayReports
|
||||
{
|
||||
private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value)
|
||||
=> value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset;
|
||||
|
||||
private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) =>
|
||||
value.Matched.DoubleValue switch
|
||||
{
|
||||
> 800d => "Exploring the Sky Islands",
|
||||
< -201d => "Exploring the Depths",
|
||||
_ => "Roaming Hyrule"
|
||||
};
|
||||
|
||||
private static FormattedValue SkywardSwordHD_Rupees(SingleValue value)
|
||||
=> "rupee".ToQuantity(value.Matched.IntValue);
|
||||
|
||||
private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value)
|
||||
=> value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
|
||||
|
||||
private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value)
|
||||
=> value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
|
||||
|
||||
private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value)
|
||||
=> value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
|
||||
|
||||
private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value)
|
||||
=> value.Matched.StringValue 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",
|
||||
_ => FormattedValue.ForceReset
|
||||
};
|
||||
|
||||
private static FormattedValue PokemonSV(MultiValue values)
|
||||
{
|
||||
|
||||
string playStatus = values.Matched[0].BoxedValue is 0 ? "Playing Alone" : "Playing in a group";
|
||||
|
||||
FormattedValue locations = values.Matched[1].ToString() switch
|
||||
{
|
||||
// Base Game Locations
|
||||
"a_w01" => "South Area One",
|
||||
"a_w02" => "Mesagoza",
|
||||
"a_w03" => "The Pokemon League",
|
||||
"a_w04" => "South Area Two",
|
||||
"a_w05" => "South Area Four",
|
||||
"a_w06" => "South Area Six",
|
||||
"a_w07" => "South Area Five",
|
||||
"a_w08" => "South Area Three",
|
||||
"a_w09" => "West Area One",
|
||||
"a_w10" => "Asado Desert",
|
||||
"a_w11" => "West Area Two",
|
||||
"a_w12" => "Medali",
|
||||
"a_w13" => "Tagtree Thicket",
|
||||
"a_w14" => "East Area Three",
|
||||
"a_w15" => "Artazon",
|
||||
"a_w16" => "East Area Two",
|
||||
"a_w18" => "Casseroya Lake",
|
||||
"a_w19" => "Glaseado Mountain",
|
||||
"a_w20" => "North Area Three",
|
||||
"a_w21" => "North Area One",
|
||||
"a_w22" => "North Area Two",
|
||||
"a_w23" => "The Great Crater of Paldea",
|
||||
"a_w24" => "South Paldean Sea",
|
||||
"a_w25" => "West Paldean Sea",
|
||||
"a_w26" => "East Paldean Sea",
|
||||
"a_w27" => "North Paldean Sea",
|
||||
//TODO DLC Locations
|
||||
_ => FormattedValue.ForceReset
|
||||
};
|
||||
|
||||
return$"{playStatus} in {locations}";
|
||||
}
|
||||
|
||||
private static FormattedValue SuperSmashBrosUltimate_Mode(SparseMultiValue values)
|
||||
{
|
||||
// Check if the PlayReport is for a challenger approach or an achievement.
|
||||
if (values.Matched.TryGetValue("fighter", out Value fighter) && values.Matched.ContainsKey("reason"))
|
||||
{
|
||||
return $"Challenger Approaches - {SuperSmashBrosUltimate_Character(fighter)}";
|
||||
}
|
||||
|
||||
if (values.Matched.TryGetValue("fighter", out fighter) && values.Matched.ContainsKey("challenge_count"))
|
||||
{
|
||||
return $"Fighter Unlocked - {SuperSmashBrosUltimate_Character(fighter)}";
|
||||
}
|
||||
|
||||
if (values.Matched.TryGetValue("anniversary", out Value anniversary))
|
||||
{
|
||||
return $"Achievement Unlocked - ID: {anniversary}";
|
||||
}
|
||||
|
||||
if (values.Matched.ContainsKey("is_created"))
|
||||
{
|
||||
return "Edited a Custom Stage!";
|
||||
}
|
||||
|
||||
if (values.Matched.ContainsKey("adv_slot"))
|
||||
{
|
||||
return
|
||||
"Playing Adventure Mode"; // Doing this as it can be a placeholder until we can grab the character.
|
||||
}
|
||||
|
||||
// Check if we have a match_mode at this point, if not, go to default.
|
||||
if (!values.Matched.TryGetValue("match_mode", out Value matchMode))
|
||||
{
|
||||
return "Smashing";
|
||||
}
|
||||
|
||||
return matchMode.BoxedValue switch
|
||||
{
|
||||
0 when values.Matched.TryGetValue("player_1_fighter", out Value player) &&
|
||||
values.Matched.TryGetValue("player_2_fighter", out Value challenger)
|
||||
=> $"Last Smashed: {SuperSmashBrosUltimate_Character(challenger)}'s Fighter Challenge - {SuperSmashBrosUltimate_Character(player)}",
|
||||
1 => $"Last Smashed: Normal Battle - {SuperSmashBrosUltimate_PlayerListing(values)}",
|
||||
2 when values.Matched.TryGetValue("player_1_rank", out Value team)
|
||||
=> team.BoxedValue is 0
|
||||
? "Last Smashed: Squad Strike - Red Team Wins"
|
||||
: "Last Smashed: Squad Strike - Blue Team Wins",
|
||||
3 => $"Last Smashed: Custom Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
|
||||
4 => $"Last Smashed: Super Sudden Death - {SuperSmashBrosUltimate_PlayerListing(values)}",
|
||||
5 => $"Last Smashed: Smashdown - {SuperSmashBrosUltimate_PlayerListing(values)}",
|
||||
6 => $"Last Smashed: Tourney Battle - {SuperSmashBrosUltimate_PlayerListing(values)}",
|
||||
7 when values.Matched.TryGetValue("player_1_fighter", out Value player)
|
||||
=> $"Last Smashed: Spirit Board Battle as {SuperSmashBrosUltimate_Character(player)}",
|
||||
8 when values.Matched.TryGetValue("player_1_fighter", out Value player)
|
||||
=> $"Playing Adventure Mode as {SuperSmashBrosUltimate_Character(player)}",
|
||||
10 when values.Matched.TryGetValue("match_submode", out Value battle) &&
|
||||
values.Matched.TryGetValue("player_1_fighter", out Value player)
|
||||
=> $"Last Smashed: Classic Mode, Battle {(int)battle.BoxedValue + 1}/8 as {SuperSmashBrosUltimate_Character(player)}",
|
||||
12 => $"Last Smashed: Century Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
|
||||
13 => $"Last Smashed: All-Star Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
|
||||
14 => $"Last Smashed: Cruel Smash - {SuperSmashBrosUltimate_PlayerListing(values)}",
|
||||
15 when values.Matched.TryGetValue("player_1_fighter", out Value player)
|
||||
=> $"Last Smashed: Home-Run Contest - {SuperSmashBrosUltimate_Character(player)}",
|
||||
16 when values.Matched.TryGetValue("player_1_fighter", out Value player1) &&
|
||||
values.Matched.TryGetValue("player_2_fighter", out Value player2)
|
||||
=> $"Last Smashed: Home-Run Content (Co-op) - {SuperSmashBrosUltimate_Character(player1)} and {SuperSmashBrosUltimate_Character(player2)}",
|
||||
17 => $"Last Smashed: Home-Run Contest (Versus) - {SuperSmashBrosUltimate_PlayerListing(values)}",
|
||||
18 when values.Matched.TryGetValue("player_1_fighter", out Value player1) &&
|
||||
values.Matched.TryGetValue("player_2_fighter", out Value player2)
|
||||
=> $"Fresh out of Training mode - {SuperSmashBrosUltimate_Character(player1)} with {SuperSmashBrosUltimate_Character(player2)}",
|
||||
58 => $"Last Smashed: LDN Battle - {SuperSmashBrosUltimate_PlayerListing(values)}",
|
||||
63 when values.Matched.TryGetValue("player_1_fighter", out Value player)
|
||||
=> $"Last Smashed: DLC Spirit Board Battle as {SuperSmashBrosUltimate_Character(player)}",
|
||||
_ => "Smashing"
|
||||
};
|
||||
}
|
||||
|
||||
private static string SuperSmashBrosUltimate_Character(Value value) =>
|
||||
BinaryPrimitives.ReverseEndianness(
|
||||
BitConverter.ToInt64(((MsgPack.MessagePackExtendedTypeObject)value.BoxedValue).GetBody(), 0)) switch
|
||||
{
|
||||
0x0 => "Mario",
|
||||
0x1 => "Donkey Kong",
|
||||
0x2 => "Link",
|
||||
0x3 => "Samus",
|
||||
0x4 => "Dark Samus",
|
||||
0x5 => "Yoshi",
|
||||
0x6 => "Kirby",
|
||||
0x7 => "Fox",
|
||||
0x8 => "Pikachu",
|
||||
0x9 => "Luigi",
|
||||
0xA => "Ness",
|
||||
0xB => "Captain Falcon",
|
||||
0xC => "Jigglypuff",
|
||||
0xD => "Peach",
|
||||
0xE => "Daisy",
|
||||
0xF => "Bowser",
|
||||
0x10 => "Ice Climbers",
|
||||
0x11 => "Sheik",
|
||||
0x12 => "Zelda",
|
||||
0x13 => "Dr. Mario",
|
||||
0x14 => "Pichu",
|
||||
0x15 => "Falco",
|
||||
0x16 => "Marth",
|
||||
0x17 => "Lucina",
|
||||
0x18 => "Young Link",
|
||||
0x19 => "Ganondorf",
|
||||
0x1A => "Mewtwo",
|
||||
0x1B => "Roy",
|
||||
0x1C => "Chrom",
|
||||
0x1D => "Mr Game & Watch",
|
||||
0x1E => "Meta Knight",
|
||||
0x1F => "Pit",
|
||||
0x20 => "Dark Pit",
|
||||
0x21 => "Zero Suit Samus",
|
||||
0x22 => "Wario",
|
||||
0x23 => "Snake",
|
||||
0x24 => "Ike",
|
||||
0x25 => "Pokémon Trainer",
|
||||
0x26 => "Diddy Kong",
|
||||
0x27 => "Lucas",
|
||||
0x28 => "Sonic",
|
||||
0x29 => "King Dedede",
|
||||
0x2A => "Olimar",
|
||||
0x2B => "Lucario",
|
||||
0x2C => "R.O.B.",
|
||||
0x2D => "Toon Link",
|
||||
0x2E => "Wolf",
|
||||
0x2F => "Villager",
|
||||
0x30 => "Mega Man",
|
||||
0x31 => "Wii Fit Trainer",
|
||||
0x32 => "Rosalina & Luma",
|
||||
0x33 => "Little Mac",
|
||||
0x34 => "Greninja",
|
||||
0x35 => "Palutena",
|
||||
0x36 => "Pac-Man",
|
||||
0x37 => "Robin",
|
||||
0x38 => "Shulk",
|
||||
0x39 => "Bowser Jr.",
|
||||
0x3A => "Duck Hunt",
|
||||
0x3B => "Ryu",
|
||||
0x3C => "Ken",
|
||||
0x3D => "Cloud",
|
||||
0x3E => "Corrin",
|
||||
0x3F => "Bayonetta",
|
||||
0x40 => "Richter",
|
||||
0x41 => "Inkling",
|
||||
0x42 => "Ridley",
|
||||
0x43 => "King K. Rool",
|
||||
0x44 => "Simon",
|
||||
0x45 => "Isabelle",
|
||||
0x46 => "Incineroar",
|
||||
0x47 => "Mii Brawler",
|
||||
0x48 => "Mii Swordfighter",
|
||||
0x49 => "Mii Gunner",
|
||||
0x4A => "Piranha Plant",
|
||||
0x4B => "Joker",
|
||||
0x4C => "Hero",
|
||||
0x4D => "Banjo",
|
||||
0x4E => "Terry",
|
||||
0x4F => "Byleth",
|
||||
0x50 => "Min Min",
|
||||
0x51 => "Steve",
|
||||
0x52 => "Sephiroth",
|
||||
0x53 => "Pyra/Mythra",
|
||||
0x54 => "Kazuya",
|
||||
0x55 => "Sora",
|
||||
0xFE => "Random",
|
||||
0xFF => "Scripted Entity",
|
||||
_ => "Unknown"
|
||||
};
|
||||
|
||||
private static string SuperSmashBrosUltimate_PlayerListing(SparseMultiValue values)
|
||||
{
|
||||
List<(string Character, int PlayerNumber, int? Rank)> players = [];
|
||||
|
||||
foreach (KeyValuePair<string, Value> player in values.Matched)
|
||||
{
|
||||
if (player.Key.StartsWith("player_") && player.Key.EndsWith("_fighter") &&
|
||||
player.Value.BoxedValue is not null)
|
||||
{
|
||||
if (!int.TryParse(player.Key.Split('_')[1], out int playerNumber))
|
||||
continue;
|
||||
|
||||
string character = SuperSmashBrosUltimate_Character(player.Value);
|
||||
int? rank = values.Matched.TryGetValue($"player_{playerNumber}_rank", out Value rankValue)
|
||||
? rankValue.IntValue
|
||||
: null;
|
||||
|
||||
players.Add((character, playerNumber, rank));
|
||||
}
|
||||
}
|
||||
|
||||
players = players.OrderBy(p => p.Rank ?? int.MaxValue).ToList();
|
||||
|
||||
return players.Count > 4
|
||||
? $"{players.Count} Players - {
|
||||
players.Take(3)
|
||||
.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")
|
||||
.JoinToString(", ")
|
||||
}"
|
||||
: players
|
||||
.Select(p => $"{p.Character}({p.PlayerNumber}){RankMedal(p.Rank)}")
|
||||
.JoinToString(", ");
|
||||
|
||||
string RankMedal(int? rank) => rank switch
|
||||
{
|
||||
0 => "🥇",
|
||||
1 => "🥈",
|
||||
2 => "🥉",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
private static FormattedValue NsoEmulator_LaunchedGame(SingleValue value) => value.Matched.StringValue switch
|
||||
{
|
||||
#region SEGA Genesis
|
||||
|
||||
"m_0054_e" => Playing("Alien Soldier"),
|
||||
"m_3978_e" => Playing("Alien Storm"),
|
||||
"m_5234_e" => Playing("ALISIA DRAGOON"),
|
||||
"m_5003_e" => Playing("Streets of Rage 2"),
|
||||
"m_4843_e" => Playing("Kid Chameleon"),
|
||||
"m_2874_e" => Playing("Columns"),
|
||||
"m_3167_e" => Playing("Comix Zone"),
|
||||
"m_5007_e" => Playing("Contra: Hard Corps"),
|
||||
"m_0865_e" => Playing("Ghouls 'n Ghosts"),
|
||||
"m_0935_e" => Playing("Dynamite Headdy"),
|
||||
"m_8314_e" => Playing("Earthworm Jim"),
|
||||
"m_5012_e" => Playing("Ecco the Dolphin"),
|
||||
"m_2207_e" => Playing("Flicky"),
|
||||
"m_9432_e" => Playing("Golden Axe II"),
|
||||
"m_5015_e" => Playing("Golden Axe"),
|
||||
"m_5017_e" => Playing("Gunstar Heroes"),
|
||||
"m_0732_e" => Playing("Altered Beast"),
|
||||
"m_2245_e" or "m_2245_pd" or "m_2245_pf" => Playing("Landstalker"),
|
||||
"m_1654_e" => Playing("Target Earth"),
|
||||
"m_7050_e" => Playing("Light Crusader"),
|
||||
"m_5027_e" => Playing("M.U.S.H.A."),
|
||||
"m_5028_e" => Playing("Phantasy Star IV"),
|
||||
"m_9155_e" => Playing("Pulseman"),
|
||||
"m_5030_e" => Playing("Dr. Robotnik's Mean Bean Machine"),
|
||||
"m_0098_e" => Playing("Crusader of Centy"),
|
||||
"m_0098_k" => Playing("신창세기 라그나센티"),
|
||||
"m_0098_pd" or "m_0098_pf" or "m_0098_ps" => Playing("Soleil"),
|
||||
"m_5033_e" => Playing("Ristar"),
|
||||
"m_1987_e" => Playing("MEGA MAN: THE WILY WARS"),
|
||||
"m_2609_e" => Playing("WOLF OF THE BATTLEFIELD: MERCS"),
|
||||
"m_3353_e" => Playing("Shining Force II"),
|
||||
"m_5036_e" => Playing("Shining Force"),
|
||||
"m_9866_e" => Playing("Sonic The Hedgehog Spinball"),
|
||||
"m_5041_e" => Playing("Sonic The Hedgehog 2"),
|
||||
"m_5523_e" => Playing("Space Harrier II"),
|
||||
"m_0041_e" => Playing("STREET FIGHTER II' : SPECIAL CHAMPION EDITION"),
|
||||
"m_5044_e" => Playing("STRIDER"),
|
||||
"m_6353_e" => Playing("Super Fantasy Zone"),
|
||||
"m_9569_e" => Playing("Beyond Oasis"),
|
||||
"m_9569_k" => Playing("스토리 오브 도어"),
|
||||
"m_9569_pd" or "m_9569_ps" => Playing("The Story of Thor"),
|
||||
"m_9569_pf" => Playing("La Légende de Thor"),
|
||||
"m_5049_e" => Playing("Shinobi III: Return of the Ninja Master"),
|
||||
"m_6811_e" => Playing("The Revenge of Shinobi"),
|
||||
"m_4372_e" => Playing("Thunder Force II"),
|
||||
"m_1535_e" => Playing("ToeJam & Earl in Panic on Funkotron"),
|
||||
"m_0432_e" => Playing("ToeJam & Earl"),
|
||||
"m_5052_e" => Playing("Castlevania: BLOODLINES"),
|
||||
"m_3626_e" => Playing("VectorMan"),
|
||||
"m_7955_e" => Playing("Sword of Vermilion"),
|
||||
"m_0394_e" => Playing("Virtua Fighter 2"),
|
||||
"m_9417_e" => Playing("Zero Wing"),
|
||||
|
||||
#endregion
|
||||
|
||||
#region Nintendo 64
|
||||
|
||||
"n_1653_e" or "n_1653_p" => Playing("1080º ™ Snowboarding"),
|
||||
"n_4868_e" or "n_4868_p" => Playing("Banjo Kazooie™"),
|
||||
"n_1226_e" or "n_1226_p" => Playing("Banjo-Tooie™"),
|
||||
"n_3083_e" or "n_3083_p" => Playing("Blast Corps"),
|
||||
"n_3007_e" => Playing("Dr. Mario™ 64"),
|
||||
"n_4238_e" => Playing("Excitebike™ 64"),
|
||||
"n_1870_e" => Playing("Extreme G"),
|
||||
"n_2456_e" => Playing("F-Zero™ X"),
|
||||
"n_4631_e" => Playing("GoldenEye 007"),
|
||||
"n_1635_e" => Playing("Harvest Moon 64"),
|
||||
"n_2225_e" => Playing("Iggy’s Reckin’ Balls"),
|
||||
"n_1625_e" or "n_1625_p" => Playing("JET FORCE GEMINI™"),
|
||||
"n_3052_e" => Playing("Kirby 64™: The Crystal Shards"),
|
||||
"n_4371_e" => Playing("Mario Golf™"),
|
||||
"n_3013_e" => Playing("Mario Kart™ 64"),
|
||||
"n_1053_e" or "n_1053_p" => Playing("Mario Party™ 2"),
|
||||
"n_2965_e" or "n_2965_p" => Playing("Mario Party™ 3"),
|
||||
"n_4737_e" or "n_4737_p" => Playing("Mario Party™"),
|
||||
"n_3017_e" => Playing("Mario Tennis™"),
|
||||
"n_2992_e" or "n_2992_p" => Playing("Paper Mario™"),
|
||||
"n_3783_e" or "n_3783_p" => Playing("Pilotwings™ 64"),
|
||||
"n_1848_e" or "n_1848_pd" or "n_1848_pf" => Playing("Pokémon™ Puzzle League"),
|
||||
"n_3240_e" or "n_3240_pd" or "n_3240_pf" or "n_3240_pi" or "n_3240_ps" => Playing("Pokémon Snap™"),
|
||||
"n_4590_e" or "n_4590_pd" or "n_4590_pf" or "n_4590_pi" or "n_4590_ps" => Playing("Pokémon Stadium™"),
|
||||
"n_3309_e" or "n_3309_pd" or "n_3309_pf" or "n_3309_pi" or "n_3309_ps" => Playing("Pokémon Stadium 2™"),
|
||||
"n_3029_e" => Playing("Sin & Punishment™"),
|
||||
"n_3030_e" => Playing("Star Fox™ 64"),
|
||||
"n_3030_p" => Playing("Lylat Wars™"),
|
||||
"n_3031_e" or "n_3031_p" => Playing("Super Mario 64™"),
|
||||
"n_4813_e" or "n_4813_p" => Playing("Wave Race™ 64"),
|
||||
"n_3034_e" => Playing("WIN BACK: COVERT OPERATIONS"),
|
||||
"n_3034_p" => Playing("OPERATION: WIN BACK"),
|
||||
"n_3036_e" or "n_3036_p" => Playing("Yoshi's Story™"),
|
||||
"n_1407_e" or "n_1407_p" => Playing("The Legend of Zelda™: Majora's Mask™"),
|
||||
"n_3038_e" or "n_3038_p" => Playing("The Legend of Zelda™: Ocarina of Time™"),
|
||||
|
||||
#endregion
|
||||
|
||||
#region NES
|
||||
|
||||
"clv_p_naaae" => Playing("Super Mario Bros.™"),
|
||||
"clv_p_naabe" => Playing("Super Mario Bros.™: The Lost Levels"),
|
||||
"clv_p_naace" or "clv_p_naace_sp1" => Playing("Super Mario Bros.™ 3"),
|
||||
"clv_p_naade" => Playing("Super Mario Bros.™ 2"),
|
||||
"clv_p_naaee" => Playing("Donkey Kong™"),
|
||||
"clv_p_naafe" => Playing("Donkey Kong Jr.™"),
|
||||
"clv_p_naage" => Playing("Donkey Kong™ 3"),
|
||||
"clv_p_naahe" => Playing("Excitebike™"),
|
||||
"clv_p_naaje" => Playing("EarthBound Beginnings"),
|
||||
"clv_p_naame" => Playing("NES™ Open Tournament Golf"),
|
||||
"clv_p_naane" or "clv_p_naane_sp1" => Playing("The Legend of Zelda™"),
|
||||
"clv_p_naape" or "clv_p_naape_sp1" => Playing("Kirby's Adventure™"),
|
||||
"clv_p_naaqe" or "clv_p_naaqe_sp1" or "clv_p_naaqe_sp2" => Playing("Metroid™"),
|
||||
"clv_p_naare" => Playing("Balloon Fight™"),
|
||||
"clv_p_naase" or "clv_p_naase_sp1" => Playing("Zelda II - The Adventure of Link™"),
|
||||
"clv_p_naate" => Playing("Punch-Out!!™ Featuring Mr. Dream"),
|
||||
"clv_p_naaue" => Playing("Ice Climber™"),
|
||||
"clv_p_naave" or "clv_p_naave_sp1" => Playing("Kid Icarus™"),
|
||||
"clv_p_naawe" => Playing("Mario Bros.™"),
|
||||
"clv_p_naaxe" or "clv_p_naaxe_sp1" => Playing("Dr. Mario™"),
|
||||
"clv_p_naaye" => Playing("Yoshi™"),
|
||||
"clv_p_naaze" => Playing("StarTropics™"),
|
||||
"clv_p_nabce" or "clv_p_nabce_sp1" => Playing("Ghosts'n Goblins™"),
|
||||
"clv_p_nabre" or "clv_p_nabre_sp1" or "clv_p_nabre_sp2" => Playing("Gradius"),
|
||||
"clv_p_nacbe" or "clv_p_nacbe_sp1" => Playing("Ninja Gaiden"),
|
||||
"clv_p_nacce" => Playing("Solomon's Key"),
|
||||
"clv_p_nacde" => Playing("Tecmo Bowl"),
|
||||
"clv_p_nacfe" => Playing("Double Dragon"),
|
||||
"clv_p_nache" => Playing("Double Dragon II: The Revenge"),
|
||||
"clv_p_nacje" => Playing("River City Ransom"),
|
||||
"clv_p_nacke" => Playing("Super Dodge Ball"),
|
||||
"clv_p_nacle" => Playing("Downtown Nekketsu March Super-Awesome Field Day!"),
|
||||
"clv_p_nacpe" => Playing("The Mystery of Atlantis"),
|
||||
"clv_p_nacre" => Playing("Soccer"),
|
||||
"clv_p_nacse" or "clv_p_nacse_sp1" => Playing("Ninja JaJaMaru-kun"),
|
||||
"clv_p_nacte" => Playing("Ice Hockey"),
|
||||
"clv_p_nacue" or "clv_p_nacue_sp1" => Playing("Blaster Master"),
|
||||
"clv_p_nacwe" => Playing("ADVENTURES OF LOLO"),
|
||||
"clv_p_nacxe" => Playing("Wario's Woods™"),
|
||||
"clv_p_nacye" => Playing("Tennis"),
|
||||
"clv_p_nacze" => Playing("Wrecking Crew™"),
|
||||
"clv_p_nadbe" => Playing("Joy Mech Fight™"),
|
||||
"clv_p_nadde" or "clv_p_nadde_sp1" => Playing("Star Soldier"),
|
||||
"clv_p_nadke" => Playing("Tetris®"),
|
||||
"clv_p_nadle" => Playing("Pro Wrestling"),
|
||||
"clv_p_nadpe" => Playing("Baseball"),
|
||||
"clv_p_nadte" or "clv_p_nadte_sp1" => Playing("TwinBee"),
|
||||
"clv_p_nadue" or "clv_p_nadue_sp1" => Playing("Mighty Bomb Jack"),
|
||||
"clv_p_nadve" => Playing("Kung-Fu Heroes"),
|
||||
"clv_p_nadxe" => Playing("City Connection"),
|
||||
"clv_p_nadye" => Playing("Rygar"),
|
||||
"clv_p_naeae" => Playing("Crystalis"),
|
||||
"clv_p_naece" => Playing("Vice: Project Doom"),
|
||||
"clv_p_naehe" => Playing("Clu Clu Land™"),
|
||||
"clv_p_naeie" => Playing("VS. Excitebike™"),
|
||||
"clv_p_naeje" => Playing("Volleyball™"),
|
||||
"clv_p_naeke" => Playing("JOURNEY TO SILIUS"),
|
||||
"clv_p_naele" => Playing("S.C.A.T.: Special Cybernetic Attack Team"),
|
||||
"clv_p_naeme" => Playing("Shadow of the Ninja"),
|
||||
"clv_p_naene" => Playing("Nightshade"),
|
||||
"clv_p_naepe" => Playing("The Immortal"),
|
||||
"clv_p_naeqe" => Playing("Eliminator Boat Duel"),
|
||||
"clv_p_naere" => Playing("Fire 'n Ice"),
|
||||
"clv_p_nafce" => Playing("XEVIOUS"),
|
||||
"clv_p_nagpe" => Playing("DAIVA STORY 6 IMPERIAL OF NIRSARTIA"),
|
||||
"clv_p_nagqe" => Playing("DIG DUGⅡ"),
|
||||
"clv_p_nague" => Playing("MAPPY-LAND"),
|
||||
"clv_p_nahhe" => Playing("Mach Rider™"),
|
||||
"clv_p_nahje" => Playing("Pinball"),
|
||||
"clv_p_nahre" => Playing("Mystery Tower"),
|
||||
"clv_p_nahte" => Playing("Urban Champion™"),
|
||||
"clv_p_nahue" => Playing("Donkey Kong Jr.™ Math"),
|
||||
"clv_p_nahve" => Playing("The Mysterious Murasame Castle"),
|
||||
"clv_p_najae" => Playing("DEVIL WORLD™"),
|
||||
"clv_p_najbe" => Playing("Golf"),
|
||||
"clv_p_najpe" => Playing("R.C. PRO-AM™"),
|
||||
"clv_p_najre" => Playing("COBRA TRIANGLE™"),
|
||||
"clv_p_najse" => Playing("SNAKE RATTLE N ROLL™"),
|
||||
"clv_p_najte" => Playing("SOLAR® JETMAN"),
|
||||
|
||||
#endregion
|
||||
|
||||
#region SNES
|
||||
|
||||
"s_2180_e" => Playing("BATTLETOADS™ DOUBLE DRAGON™"),
|
||||
"s_2179_e" => Playing("BATTLETOADS™ IN BATTLEMANIACS"),
|
||||
"s_2182_e" => Playing("BIG RUN"),
|
||||
"s_2156_e" => Playing("Bombuzal"),
|
||||
"s_2002_e" => Playing("BRAWL BROTHERS"),
|
||||
"s_2025_e" => Playing("Breath of Fire II"),
|
||||
"s_2003_e" => Playing("Breath Of Fire"),
|
||||
"s_2163_e" => Playing("Claymates"),
|
||||
"s_2150_e" => Playing("Congo's Caper"),
|
||||
"s_2171_e" => Playing("COSMO GANG THE PUZZLE"),
|
||||
"s_2004_e" => Playing("Demon's Crest"),
|
||||
"s_2026_e" => Playing("Kunio-kun no Dodgeball da yo Zen'in Shūgō!"),
|
||||
"s_2060_e" => Playing("Donkey Kong Country 2: Diddy's Kong Quest"),
|
||||
"s_2061_e" => Playing("Donkey Kong Country 3: Dixie Kong's Double Trouble!"),
|
||||
"s_2055_e" => Playing("Donkey Kong Country"),
|
||||
"s_2139_e" => Playing("DOOMSDAY WARRIOR"),
|
||||
"s_2051_e" => Playing("EarthBound"),
|
||||
"s_2162_e" => Playing("Earthworm Jim™ 2"),
|
||||
"s_2005_e" => Playing("F-ZERO™"),
|
||||
"s_2183_e" => Playing("FATAL FURY 2"),
|
||||
"s_2174_e" => Playing("Fighter's History"),
|
||||
"s_2037_e" => Playing("Harvest Moon"),
|
||||
"s_2161_e" => Playing("Jelly Boy"),
|
||||
"s_2006_e" => Playing("Joe & Mac 2: Lost in the Tropics"),
|
||||
"s_2169_e" => Playing("Caveman Ninja"),
|
||||
"s_2181_e" => Playing("KILLER INSTINCT™"),
|
||||
"s_2029_e" or "s_2029_e_sp1" => Playing("Kirby Super Star™"),
|
||||
"s_2121_e" => Playing("Kirby's Avalanche™"),
|
||||
"s_2007_e" or "s_2007_e_sp1" => Playing("Kirby's Dream Course™"),
|
||||
"s_2008_e" or "s_2008_e_sp1" => Playing("Kirby's Dream Land™ 3"),
|
||||
"s_2172_e" => Playing("Kirby’s Star Stacker™"),
|
||||
"s_2151_e" => Playing("Magical Drop2"),
|
||||
"s_2044_e" => Playing("Mario's Super Picross"),
|
||||
"s_2038_e" => Playing("Natsume Championship Wrestling"),
|
||||
"s_2140_e" => Playing("Operation Logic Bomb"),
|
||||
"s_2034_e" => Playing("Panel de Pon"),
|
||||
"s_2009_e" => Playing("Pilotwings™"),
|
||||
"s_2010_e" => Playing("Pop'n TwinBee"),
|
||||
"s_2157_e" => Playing("Prehistorik Man"),
|
||||
"s_2145_e" => Playing("Psycho Dream"),
|
||||
"s_2141_e" => Playing("Rival Turf!"),
|
||||
"s_2152_e" => Playing("SIDE POCKET"),
|
||||
"s_2158_e" => Playing("Spanky’s™ Quest"),
|
||||
"s_2031_e" => Playing("Star Fox™ 2"),
|
||||
"s_2011_e" => Playing("Star Fox™"),
|
||||
"s_2012_e" => Playing("Stunt Race FX™"),
|
||||
"s_2032_e" => Playing("Amazing Hebereke"),
|
||||
"s_2159_e" => Playing("Super Baseball Simulator 1.000"),
|
||||
"s_2013_e" => Playing("SUPER E.D.F. EARTH DEFENSE FORCE"),
|
||||
"s_2014_e" => Playing("Smash Tennis"),
|
||||
"s_2015_e" => Playing("Super Ghouls'n Ghosts™"),
|
||||
"s_2033_e" => Playing("Super Mario All-Stars™"),
|
||||
"s_2016_e" or "s_2016_e_sp1" => Playing("Super Mario Kart™"),
|
||||
"s_2017_e" or "s_2017_e_sp1" => Playing("Super Mario World™"),
|
||||
"s_2018_e" or "s_2018_e_sp1" => Playing("Super Metroid™"),
|
||||
"s_2184_e" => Playing("Super Ninja Boy"),
|
||||
"s_2019_e" or "s_2019_e_sp1" => Playing("Super Punch-Out!!™"),
|
||||
"s_2020_e" => Playing("Super Puyo Puyo 2"),
|
||||
"s_2133_e" => Playing("SUPER R-TYPE"),
|
||||
"s_2021_e" => Playing("Super Soccer"),
|
||||
"s_2022_e" => Playing("Super Tennis"),
|
||||
"s_2136_e" => Playing("Sutte Hakkun"),
|
||||
"s_2142_e" => Playing("The Ignition Factor"),
|
||||
"s_2143_e" => Playing("The Peace Keepers"),
|
||||
"s_2146_e" => Playing("Tuff E Nuff"),
|
||||
"s_2144_e" => Playing("SUPER VALIS Ⅳ"),
|
||||
"s_2049_e" => Playing("Wild Guns"),
|
||||
"s_2096_e" => Playing("Wrecking Crew™ '98"),
|
||||
"s_2023_e" => Playing("Super Mario World™ 2: Yoshi's Island™"),
|
||||
"s_2024_e" => Playing("The Legend of Zelda™: A Link to the Past™"),
|
||||
|
||||
#endregion
|
||||
|
||||
#region GameBoy
|
||||
|
||||
"c_7224_e" or "c_7224_p" => Playing("Alone in the Dark: The New Nightmare"),
|
||||
"c_5022_e" => Playing("Blaster Master: Enemy Below"),
|
||||
"c_3381_e" => Playing("Game & Watch™ Gallery 3"),
|
||||
"c_0282_e" => Playing("Kirby Tilt ‘n’ Tumble™"),
|
||||
"c_4471_e" or "c_4471_p" => Playing("Mario Golf™"),
|
||||
"c_9947_e" => Playing("Mario Tennis™"),
|
||||
"c_3191_e" or "c_3191_p" or "c_3191_x" => Playing("Pokémon™ Trading Card Game"),
|
||||
"c_8914_e" or "c_8914_p" => Playing("Quest for Camelot™"),
|
||||
"c_2648_e" => Playing("Tetris® DX"),
|
||||
"c_5928_e" => Playing("Wario Land™ 3"),
|
||||
"c_3996_e" or "c_3996_pd" or "c_3996_pf" => Playing("The Legend of Zelda™: Link's Awakening DX™"),
|
||||
"c_8852_e" or "c_8852_p" => Playing("The Legend of Zelda™: Oracle of Ages™"),
|
||||
"c_9130_e" or "c_9130_p" => Playing("The Legend of Zelda™: Oracle of Seasons™"),
|
||||
"d_6879_e" => Playing("Alleyway™"),
|
||||
"d_7618_e" => Playing("Baseball"),
|
||||
"d_6005_e" => Playing("BurgerTime Deluxe"),
|
||||
"d_7120_e" => Playing("Castlevania Legends"),
|
||||
"d_2744_e" => Playing("Dr. Mario™"),
|
||||
"d_1593_e" => Playing("Donkey Kong Land 2™"),
|
||||
"d_7216_e" => Playing("Donkey Kong Land III™"),
|
||||
"d_4971_e" => Playing("Donkey Kong Land™"),
|
||||
"d_7984_e" => Playing("GARGOYLE'S QUEST"),
|
||||
"d_8212_e" => Playing("Kirby's Dream Land™ 2"),
|
||||
"d_5661_e" => Playing("Kirby's Dream Land™"),
|
||||
"d_3837_e" => Playing("MEGA MAN II"),
|
||||
"d_1965_e" => Playing("MEGA MAN III"),
|
||||
"d_0194_e" => Playing("MEGA MAN IV"),
|
||||
"d_1425_e" => Playing("MEGA MAN V"),
|
||||
"d_9324_e" => Playing("MEGA MAN: DR. WILY'S REVENGE"),
|
||||
"d_1577_e" => Playing("Metroid™ II - Return of Samus™"),
|
||||
"d_5124_e" => Playing("Super Mario Land™ 2 - 6 Golden Coins™"),
|
||||
"d_7970_e" => Playing("Super Mario Land™"),
|
||||
"d_8484_e" => Playing("Tetris®"),
|
||||
|
||||
#endregion
|
||||
|
||||
#region GameBoy Advance
|
||||
|
||||
"a_9694_e" => Playing("Densetsu no Starfy 1"),
|
||||
"a_5600_e" => Playing("Densetsu no Starfy 2"),
|
||||
"a_7565_e" => Playing("Densetsu no Starfy 3"),
|
||||
"a_6553_e" => Playing("F-ZERO CLIMAX"),
|
||||
"a_7842_e" or "a_7842_p" => Playing("F-Zero™- GP Legend"),
|
||||
"a_9283_e" => Playing("F-Zero™ Maximum Velocity"),
|
||||
"a_3744_e" or "a_3744_x" or "a_3744_y" => Playing("Fire Emblem™"),
|
||||
"a_8978_d" or "a_8978_e" or "a_8978_f" or "a_8978_i" or "a_8978_s" => Playing("Golden Sun™: The Lost Age"),
|
||||
"a_3108_d" or "a_3108_e" or "a_3108_f" or "a_3108_i" or "a_3108_s" => Playing("Golden Sun™"),
|
||||
"a_3654_e" or "a_3654_p" => Playing("Kirby™ & The Amazing Mirror"),
|
||||
"a_7279_p" => Playing("Kuru Kuru Kururin™"),
|
||||
"a_7311_e" or "a_7311_p" => Playing("Mario & Luigi™: Superstar Saga"),
|
||||
"a_6845_e" => Playing("Mario Kart™: Super Circuit™"),
|
||||
"a_4139_e" or "a_4139_p" => Playing("Metroid™ Fusion"),
|
||||
"a_6834_e" or "a_6834_p" => Playing("Metroid™: Zero Mission"),
|
||||
"a_8989_e" or "a_8989_p" => Playing("Pokémon™ Mystery Dungeon: Red Rescue Team"),
|
||||
"a_9444_e" => Playing("Super Mario™ Advance"),
|
||||
"a_9901_e" or "a_9901_p" => Playing("Super Mario™ Advance 4: Super Mario Bros.™ 3"),
|
||||
"a_2939_e" => Playing("Super Mario World™: Super Mario Advance 2"),
|
||||
"a_2939_p" => Playing("Super Mario World™: Super Mario Advance 2™"),
|
||||
"a_1302_e" => Playing("WarioWare™, Inc.: Mega Microgame$!"),
|
||||
"a_1302_p" => Playing("WarioWare™, Inc.: Minigame Mania."),
|
||||
"a_6960_e" or "a_6960_p" => Playing("Yoshi's Island™: Super Mario™ Advance 3"),
|
||||
"a_5190_e" or "a_5190_p" => Playing("The Legend of Zelda™: A Link to the Past™ Four Swords"),
|
||||
"a_8665_e" or "a_8665_p" => Playing("The Legend of Zelda™: The Minish Cap"),
|
||||
|
||||
#endregion
|
||||
|
||||
_ => FormattedValue.ForceReset
|
||||
};
|
||||
}
|
||||
}
|
98
src/Ryujinx/Systems/PlayReport/PlayReports.cs
Normal file
98
src/Ryujinx/Systems/PlayReport/PlayReports.cs
Normal file
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.PlayReport
|
||||
{
|
||||
public static partial class PlayReports
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
// init lazy value
|
||||
_ = Analyzer;
|
||||
}
|
||||
|
||||
public static Analyzer Analyzer => _analyzerLazy.Value;
|
||||
|
||||
private static readonly Lazy<Analyzer> _analyzerLazy = new(() => new Analyzer()
|
||||
.AddSpec(
|
||||
"01007ef00011e000",
|
||||
spec => spec
|
||||
.WithDescription("based on being in Master Mode.")
|
||||
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
||||
// reset to normal status when switching between normal & master mode in title screen
|
||||
.AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
|
||||
)
|
||||
.AddSpec(
|
||||
"0100f2c0115b6000",
|
||||
spec => spec
|
||||
.WithDescription("based on where you are in Hyrule (Depths, Surface, Sky).")
|
||||
.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
|
||||
.AddSpec(
|
||||
"01002da013484000",
|
||||
spec => spec
|
||||
.WithDescription("based on how many Rupees you have.")
|
||||
.AddValueFormatter("rupees", SkywardSwordHD_Rupees))
|
||||
.AddSpec(
|
||||
"0100000000010000",
|
||||
spec => spec
|
||||
.WithDescription("based on if you're playing with Assist Mode.")
|
||||
.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
|
||||
)
|
||||
.AddSpec(
|
||||
"010075000ecbe000",
|
||||
spec => spec
|
||||
.WithDescription("based on if you're playing with Assist Mode.")
|
||||
.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
|
||||
)
|
||||
.AddSpec(
|
||||
"010028600ebda000",
|
||||
spec => spec
|
||||
.WithDescription("based on being in either Super Mario 3D World or Bowser's Fury.")
|
||||
.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
|
||||
)
|
||||
.AddSpec( // Global & China IDs
|
||||
["0100152000022000", "010075100e8ec000"],
|
||||
spec => spec
|
||||
.WithDescription(
|
||||
"based on what modes you're selecting in the menu & whether or not you're in a race.")
|
||||
.AddValueFormatter("To", MarioKart8Deluxe_Mode)
|
||||
)
|
||||
.AddSpec(
|
||||
["0100a3d008c5c000", "01008f6008c5e000"],
|
||||
spec => spec
|
||||
.WithDescription("based on if you're playing alone or in a group and what area of Paldea you're exploring.")
|
||||
.AddMultiValueFormatter(["team_circle", "area_no"], PokemonSV)
|
||||
)
|
||||
.AddSpec(
|
||||
"01006a800016e000",
|
||||
spec => spec
|
||||
.WithDescription("based on what mode you're playing, who won, and what characters were present.")
|
||||
.AddSparseMultiValueFormatter(
|
||||
[
|
||||
// Metadata to figure out what PlayReport we have.
|
||||
"match_mode", "match_submode", "anniversary", "fighter", "reason", "challenge_count",
|
||||
"adv_slot", "is_created",
|
||||
// List of Fighters
|
||||
"player_1_fighter", "player_2_fighter", "player_3_fighter", "player_4_fighter",
|
||||
"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}";
|
||||
}
|
||||
}
|
249
src/Ryujinx/Systems/PlayReport/Specs.cs
Normal file
249
src/Ryujinx/Systems/PlayReport/Specs.cs
Normal file
|
@ -0,0 +1,249 @@
|
|||
using MsgPack;
|
||||
using Ryujinx.Ava.Systems.AppLibrary;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.PlayReport
|
||||
{
|
||||
/// <summary>
|
||||
/// A mapping of title IDs to value formatter specs.
|
||||
///
|
||||
/// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
|
||||
/// </summary>
|
||||
public class GameSpec
|
||||
{
|
||||
public static GameSpec Create(string requiredTitleId, params IEnumerable<string> otherTitleIds)
|
||||
=> new() { TitleIds = otherTitleIds.Prepend(requiredTitleId).ToArray() };
|
||||
|
||||
public static GameSpec Create(IEnumerable<string> titleIds)
|
||||
=> new() { TitleIds = titleIds.ToArray() };
|
||||
|
||||
private int _lastPriority;
|
||||
|
||||
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; } = [];
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add a value formatter to the current <see cref="GameSpec"/>
|
||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="reportKey">The key name to match.</param>
|
||||
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||
public GameSpec AddValueFormatter(
|
||||
string reportKey,
|
||||
SingleValueFormatter valueFormatter
|
||||
) => AddValueFormatter(_lastPriority++, reportKey, valueFormatter);
|
||||
|
||||
/// <summary>
|
||||
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||
/// <param name="reportKey">The key name to match.</param>
|
||||
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||
public GameSpec AddValueFormatter(
|
||||
int priority,
|
||||
string reportKey,
|
||||
SingleValueFormatter valueFormatter
|
||||
) => AddValueFormatter(new FormatterSpec
|
||||
{
|
||||
Priority = priority, ReportKeys = [reportKey], Formatter = valueFormatter
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="reportKeys">The key names to match.</param>
|
||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||
public GameSpec AddMultiValueFormatter(
|
||||
string[] reportKeys,
|
||||
MultiValueFormatter valueFormatter
|
||||
) => AddMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter);
|
||||
|
||||
/// <summary>
|
||||
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||
/// <param name="reportKeys">The key names to match.</param>
|
||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||
public GameSpec AddMultiValueFormatter(
|
||||
int priority,
|
||||
string[] reportKeys,
|
||||
MultiValueFormatter valueFormatter
|
||||
) => AddValueFormatter(new MultiFormatterSpec
|
||||
{
|
||||
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||
/// <br/><br/>
|
||||
/// The 'Sparse' multi-value formatters do not require every key to be present.
|
||||
/// If you need this requirement, use <see cref="AddMultiValueFormatter(string[], MultiValueFormatter)"/>.
|
||||
/// </summary>
|
||||
/// <param name="reportKeys">The key names to match.</param>
|
||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||
public GameSpec AddSparseMultiValueFormatter(
|
||||
string[] reportKeys,
|
||||
SparseMultiValueFormatter valueFormatter
|
||||
) => AddSparseMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter);
|
||||
|
||||
/// <summary>
|
||||
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||
/// <br/><br/>
|
||||
/// The 'Sparse' multi-value formatters do not require every key to be present.
|
||||
/// If you need this requirement, use <see cref="AddMultiValueFormatter(int, string[], MultiValueFormatter)"/>.
|
||||
/// </summary>
|
||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||
/// <param name="reportKeys">The key names to match.</param>
|
||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||
public GameSpec AddSparseMultiValueFormatter(
|
||||
int priority,
|
||||
string[] reportKeys,
|
||||
SparseMultiValueFormatter valueFormatter
|
||||
) => AddValueFormatter(new SparseMultiFormatterSpec
|
||||
{
|
||||
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
|
||||
});
|
||||
|
||||
private GameSpec AddValueFormatter<T>(T formatterSpec) where T : FormatterSpecBase
|
||||
{
|
||||
ValueFormatters.Add(formatterSpec);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
|
||||
/// </summary>
|
||||
public class FormatterSpec : FormatterSpecBase
|
||||
{
|
||||
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
|
||||
{
|
||||
if (!playReport.ReportData.AsDictionary().TryGetValue(ReportKeys[0], out MessagePackObject valuePackObject))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = valuePackObject;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
|
||||
/// </summary>
|
||||
public class MultiFormatterSpec : FormatterSpecBase
|
||||
{
|
||||
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
|
||||
{
|
||||
List<MessagePackObject> packedObjects = [];
|
||||
foreach (string reportKey in ReportKeys)
|
||||
{
|
||||
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
packedObjects.Add(valuePackObject);
|
||||
}
|
||||
|
||||
result = packedObjects;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their sparsely populated potential values.
|
||||
/// </summary>
|
||||
public class SparseMultiFormatterSpec : FormatterSpecBase
|
||||
{
|
||||
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
|
||||
{
|
||||
Dictionary<string, MessagePackObject> packedObjects = [];
|
||||
foreach (string reportKey in ReportKeys)
|
||||
{
|
||||
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
||||
continue;
|
||||
|
||||
packedObjects.Add(reportKey, valuePackObject);
|
||||
}
|
||||
|
||||
result = packedObjects;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class FormatterSpecBase
|
||||
{
|
||||
public abstract bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object data);
|
||||
|
||||
public int Priority { get; init; }
|
||||
public string[] ReportKeys { get; init; }
|
||||
public Delegate Formatter { get; init; }
|
||||
|
||||
public bool TryFormat(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport,
|
||||
out FormattedValue formattedValue)
|
||||
{
|
||||
formattedValue = default;
|
||||
if (!GetData(playReport, out object data))
|
||||
return false;
|
||||
|
||||
if (data is FormattedValue fv)
|
||||
{
|
||||
formattedValue = fv;
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (Formatter)
|
||||
{
|
||||
case SingleValueFormatter svf when data is MessagePackObject match:
|
||||
formattedValue = svf(
|
||||
new SingleValue(match) { Application = appMeta, PlayReport = playReport }
|
||||
);
|
||||
return true;
|
||||
case MultiValueFormatter mvf when data is List<MessagePackObject> matches:
|
||||
formattedValue = mvf(
|
||||
new MultiValue(matches) { Application = appMeta, PlayReport = playReport }
|
||||
);
|
||||
return true;
|
||||
case SparseMultiValueFormatter smvf when data is Dictionary<string, MessagePackObject> sparseMatches:
|
||||
formattedValue = smvf(
|
||||
new SparseMultiValue(sparseMatches) { Application = appMeta, PlayReport = playReport }
|
||||
);
|
||||
return true;
|
||||
default:
|
||||
throw new InvalidOperationException("Formatter delegate is not of a known type!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
160
src/Ryujinx/Systems/PlayReport/Value.cs
Normal file
160
src/Ryujinx/Systems/PlayReport/Value.cs
Normal file
|
@ -0,0 +1,160 @@
|
|||
using MsgPack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.PlayReport
|
||||
{
|
||||
/// <summary>
|
||||
/// The base input data to a ValueFormatter delegate,
|
||||
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
||||
/// </summary>
|
||||
public readonly struct Value
|
||||
{
|
||||
public Value(MessagePackObject packedValue)
|
||||
{
|
||||
PackedValue = packedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The matched value from the Play Report.
|
||||
/// </summary>
|
||||
public MessagePackObject PackedValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
|
||||
///
|
||||
/// Does not seem to work well with comparing numeric types,
|
||||
/// so use XValue properties for that.
|
||||
/// </summary>
|
||||
public object BoxedValue => PackedValue.ToObject();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
object boxed = BoxedValue;
|
||||
return boxed == null
|
||||
? "null"
|
||||
: boxed.ToString();
|
||||
}
|
||||
|
||||
public static implicit operator Value(MessagePackObject matched) => new(matched);
|
||||
|
||||
public static Value[] ConvertPackedObjects(IEnumerable<MessagePackObject> packObjects)
|
||||
=> packObjects.Select(packObject => new Value(packObject)).ToArray();
|
||||
|
||||
public static Dictionary<string, Value> ConvertPackedObjectMap(Dictionary<string, MessagePackObject> packObjects)
|
||||
=> packObjects.ToDictionary(
|
||||
x => x.Key,
|
||||
x => new Value(x.Value)
|
||||
);
|
||||
|
||||
#region AsX accessors
|
||||
|
||||
public bool BooleanValue => PackedValue.AsBoolean();
|
||||
public byte ByteValue => PackedValue.AsByte();
|
||||
public sbyte SByteValue => PackedValue.AsSByte();
|
||||
public short ShortValue => PackedValue.AsInt16();
|
||||
public ushort UShortValue => PackedValue.AsUInt16();
|
||||
public int IntValue => PackedValue.AsInt32();
|
||||
public uint UIntValue => PackedValue.AsUInt32();
|
||||
public long LongValue => PackedValue.AsInt64();
|
||||
public ulong ULongValue => PackedValue.AsUInt64();
|
||||
public float FloatValue => PackedValue.AsSingle();
|
||||
public double DoubleValue => PackedValue.AsDouble();
|
||||
public string StringValue => PackedValue.AsString();
|
||||
public Span<byte> BinaryValue => PackedValue.AsBinary();
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A potential formatted value returned by a ValueFormatter delegate.
|
||||
/// </summary>
|
||||
public readonly struct FormattedValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Was any handler able to match anything in the Play Report?
|
||||
/// </summary>
|
||||
public bool Handled { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Did the handler request the caller of the <see cref="Analyzer"/> to reset the existing value?
|
||||
/// </summary>
|
||||
public bool Reset { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// The formatted value, only present if <see cref="Handled"/> is true, and <see cref="Reset"/> is false.
|
||||
/// </summary>
|
||||
public string FormattedString { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// The intended path of execution for having a string to return: simply return the string.
|
||||
/// This implicit conversion will make the struct for you.<br/><br/>
|
||||
///
|
||||
/// If the input is null, <see cref="Unhandled"/> is returned.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The formatted string value.</param>
|
||||
/// <returns>The automatically constructed <see cref="FormattedValue"/> struct.</returns>
|
||||
public static implicit operator FormattedValue(string formattedValue)
|
||||
=> formattedValue is not null
|
||||
? new FormattedValue { Handled = true, FormattedString = formattedValue }
|
||||
: Unhandled;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (!Handled)
|
||||
return "<Unhandled>";
|
||||
|
||||
if (Reset)
|
||||
return "<Reset>";
|
||||
|
||||
return FormattedString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return this to tell the caller there is no value to return.
|
||||
/// </summary>
|
||||
public static readonly FormattedValue Unhandled = default;
|
||||
|
||||
/// <summary>
|
||||
/// Return this to suggest the caller reset the value it's using the <see cref="Analyzer"/> for.
|
||||
/// </summary>
|
||||
public static readonly FormattedValue ForceReset = new() { Handled = true, Reset = true };
|
||||
|
||||
/// <summary>
|
||||
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="SingleValueFormatter"/>.
|
||||
/// </summary>
|
||||
public static readonly SingleValueFormatter SingleAlwaysResets = _ => ForceReset;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="MultiValueFormatter"/>.
|
||||
/// </summary>
|
||||
public static readonly MultiValueFormatter MultiAlwaysResets = _ => ForceReset;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="SparseMultiValueFormatter"/>.
|
||||
/// </summary>
|
||||
public static readonly SparseMultiValueFormatter SparseMultiAlwaysResets = _ => ForceReset;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate factory you can use to always return the specified
|
||||
/// <paramref name="formattedValue"/> in a <see cref="SingleValueFormatter"/>.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
||||
public static SingleValueFormatter SingleAlwaysReturns(string formattedValue) => _ => formattedValue;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate factory you can use to always return the specified
|
||||
/// <paramref name="formattedValue"/> in a <see cref="MultiValueFormatter"/>.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
||||
public static MultiValueFormatter MultiAlwaysReturns(string formattedValue) => _ => formattedValue;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate factory you can use to always return the specified
|
||||
/// <paramref name="formattedValue"/> in a <see cref="SparseMultiValueFormatter"/>.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
||||
public static SparseMultiValueFormatter SparseMultiAlwaysReturns(string formattedValue) => _ => formattedValue;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue