From 3249f8ff41446b47cf458bbc04f8c27e005889aa Mon Sep 17 00:00:00 2001 From: Andrey Sukharev Date: Mon, 3 Apr 2023 13:14:19 +0300 Subject: [PATCH] Source generated json serializers (#4582) * Use source generated json serializers in order to improve code trimming * Use strongly typed github releases model to fetch updates instead of raw Newtonsoft.Json parsing * Use separate model for LogEventArgs serialization * Make dynamic object formatter static. Fix string builder pooling. * Do not inherit json version of LogEventArgs from EventArgs * Fix extra space in object formatting * Write log json directly to stream instead of using buffer writer * Rebase fixes * Rebase fixes * Rebase fixes * Enforce block-scoped namespaces in the solution. Convert style for existing code * Apply suggestions from code review Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Rebase indent fix * Fix indent * Delete unnecessary json properties * Rebase fix * Remove overridden json property names as they are handled in the options * Apply suggestions from code review Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Use default json options in github api calls * Indentation and spacing fixes * Fix json serialization * Fix missing JsonConverter for config enums * Add double \n\n after the whole string, not inside join --------- Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> --- .editorconfig | 4 + ARMeilleure/Decoders/IOpCode32Exception.cs | 9 +- Ryujinx.Ava/Common/Locale/LocaleManager.cs | 2 +- Ryujinx.Ava/Modules/Updater/Updater.cs | 22 +- Ryujinx.Ava/UI/Models/Amiibo.cs | 72 ---- .../UI/ViewModels/AboutWindowViewModel.cs | 2 +- .../UI/ViewModels/AmiiboWindowViewModel.cs | 35 +- .../ViewModels/ControllerSettingsViewModel.cs | 9 +- .../DownloadableContentManagerViewModel.cs | 10 +- .../UI/ViewModels/TitleUpdateViewModel.cs | 367 +++++++++--------- .../UI/Views/Main/MainMenuBarView.axaml.cs | 2 +- Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs | 6 +- .../UI/Windows/TitleUpdateWindow.axaml.cs | 3 - Ryujinx.Common/Configuration/AntiAliasing.cs | 8 +- .../Configuration/AspectRatioExtensions.cs | 6 +- .../Configuration/BackendThreading.cs | 6 +- ...ownloadableContentJsonSerializerContext.cs | 11 + .../Configuration/GraphicsBackend.cs | 6 +- .../Configuration/GraphicsDebugLevel.cs | 4 + .../JsonMotionConfigControllerConverter.cs | 13 +- .../Motion/MotionConfigController.cs | 5 +- .../MotionConfigJsonSerializerContext.cs | 12 + .../Motion/MotionInputBackendType.cs | 6 +- .../Configuration/Hid/ControllerType.cs | 5 +- .../Configuration/Hid/InputBackendType.cs | 6 +- .../Configuration/Hid/InputConfig.cs | 2 + .../Hid/InputConfigJsonSerializerContext.cs | 14 + .../Hid/JsonInputConfigConverter.cs | 13 +- Ryujinx.Common/Configuration/Hid/Key.cs | 8 +- .../Configuration/Hid/KeyboardHotkeys.cs | 4 +- .../Configuration/Hid/PlayerIndex.cs | 4 + .../Configuration/MemoryManagerMode.cs | 6 +- Ryujinx.Common/Configuration/ScalingFilter.cs | 4 + ...itleUpdateMetadataJsonSerializerContext.cs | 10 + .../Logging/Formatters/DefaultLogFormatter.cs | 54 +-- .../Formatters/DynamicObjectFormatter.cs | 84 ++++ Ryujinx.Common/Logging/LogClass.cs | 4 + Ryujinx.Common/Logging/LogEventArgs.cs | 10 +- Ryujinx.Common/Logging/LogEventArgsJson.cs | 30 ++ .../Logging/LogEventJsonSerializerContext.cs | 9 + Ryujinx.Common/Logging/LogLevel.cs | 4 + .../Logging/Targets/JsonLogTarget.cs | 12 +- Ryujinx.Common/Utilities/CommonJsonContext.cs | 11 + Ryujinx.Common/Utilities/JsonHelper.cs | 122 +++--- .../Utilities/TypedStringEnumConverter.cs | 34 ++ .../Account/Acc/AccountSaveDataManager.cs | 30 +- .../Acc/ProfilesJsonSerializerContext.cs | 11 + .../Account/Acc/Types/AccountState.cs | 4 + .../Account/Acc/Types/ProfilesJson.cs | 10 + .../Account/Acc/Types/UserProfileJson.cs | 12 + .../Nfc/Nfp/AmiiboJsonSerializerContext.cs | 10 + .../HOS/Services/Nfc/Nfp/VirtualAmiibo.cs | 10 +- .../PartitionFileSystemExtensions.cs | 7 +- Ryujinx.Headless.SDL2/Program.cs | 7 +- .../App/ApplicationJsonSerializerContext.cs | 10 + Ryujinx.Ui.Common/App/ApplicationLibrary.cs | 17 +- .../Configuration/AudioBackend.cs | 6 +- .../Configuration/ConfigurationFileFormat.cs | 12 +- .../ConfigurationFileFormatSettings.cs | 9 + .../ConfigurationJsonSerializerContext.cs | 10 + .../Configuration/ConfigurationState.cs | 5 +- .../Configuration/System/Language.cs | 6 +- .../Configuration/System/Region.cs | 6 +- Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs | 57 +++ .../Models/Amiibo/AmiiboApiGamesSwitch.cs | 15 + .../Models/Amiibo/AmiiboApiUsage.cs | 12 + Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs | 14 + .../Amiibo/AmiiboJsonSerializerContext.cs | 9 + .../Github/GithubReleaseAssetJsonResponse.cs | 9 + .../Github/GithubReleasesJsonResponse.cs | 10 + .../GithubReleasesJsonSerializerContext.cs | 9 + Ryujinx/Modules/Updater/Updater.cs | 24 +- Ryujinx/Ui/Windows/AboutWindow.cs | 2 +- Ryujinx/Ui/Windows/AmiiboWindow.cs | 65 +--- Ryujinx/Ui/Windows/ControllerWindow.cs | 11 +- Ryujinx/Ui/Windows/DlcWindow.cs | 15 +- Ryujinx/Ui/Windows/TitleUpdateWindow.cs | 15 +- 77 files changed, 904 insertions(+), 615 deletions(-) delete mode 100644 Ryujinx.Ava/UI/Models/Amiibo.cs create mode 100644 Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs create mode 100644 Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs create mode 100644 Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs create mode 100644 Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs create mode 100644 Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs create mode 100644 Ryujinx.Common/Logging/LogEventArgsJson.cs create mode 100644 Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs create mode 100644 Ryujinx.Common/Utilities/CommonJsonContext.cs create mode 100644 Ryujinx.Common/Utilities/TypedStringEnumConverter.cs create mode 100644 Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs create mode 100644 Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs create mode 100644 Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/AmiiboJsonSerializerContext.cs create mode 100644 Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs create mode 100644 Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs create mode 100644 Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs create mode 100644 Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs create mode 100644 Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs create mode 100644 Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs create mode 100644 Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs create mode 100644 Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs create mode 100644 Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs create mode 100644 Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs create mode 100644 Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs diff --git a/.editorconfig b/.editorconfig index 9e00e3ba..8a305428 100644 --- a/.editorconfig +++ b/.editorconfig @@ -63,6 +63,10 @@ dotnet_code_quality_unused_parameters = all:suggestion #### C# Coding Conventions #### +# Namespace preferences +csharp_style_namespace_declarations = block_scoped:warning +resharper_csharp_namespace_body = block_scoped + # var preferences csharp_style_var_elsewhere = false:silent csharp_style_var_for_built_in_types = false:silent diff --git a/ARMeilleure/Decoders/IOpCode32Exception.cs b/ARMeilleure/Decoders/IOpCode32Exception.cs index 82819bdd..8f0fb81a 100644 --- a/ARMeilleure/Decoders/IOpCode32Exception.cs +++ b/ARMeilleure/Decoders/IOpCode32Exception.cs @@ -1,6 +1,7 @@ -namespace ARMeilleure.Decoders; - -interface IOpCode32Exception +namespace ARMeilleure.Decoders { - int Id { get; } + interface IOpCode32Exception + { + int Id { get; } + } } \ No newline at end of file diff --git a/Ryujinx.Ava/Common/Locale/LocaleManager.cs b/Ryujinx.Ava/Common/Locale/LocaleManager.cs index 1374bfee..464ab780 100644 --- a/Ryujinx.Ava/Common/Locale/LocaleManager.cs +++ b/Ryujinx.Ava/Common/Locale/LocaleManager.cs @@ -130,7 +130,7 @@ namespace Ryujinx.Ava.Common.Locale { var localeStrings = new Dictionary(); string languageJson = EmbeddedResources.ReadAllText($"Ryujinx.Ava/Assets/Locales/{languageCode}.json"); - var strings = JsonHelper.Deserialize>(languageJson); + var strings = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary); foreach (var item in strings) { diff --git a/Ryujinx.Ava/Modules/Updater/Updater.cs b/Ryujinx.Ava/Modules/Updater/Updater.cs index e89abd1d..c5857528 100644 --- a/Ryujinx.Ava/Modules/Updater/Updater.cs +++ b/Ryujinx.Ava/Modules/Updater/Updater.cs @@ -4,13 +4,14 @@ using FluentAvalonia.UI.Controls; using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Zip; -using Newtonsoft.Json.Linq; using Ryujinx.Ava; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Common; using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; using Ryujinx.Ui.Common.Helper; +using Ryujinx.Ui.Common.Models.Github; using System; using System.Collections.Generic; using System.Diagnostics; @@ -31,6 +32,7 @@ namespace Ryujinx.Modules internal static class Updater { private const string GitHubApiURL = "https://api.github.com"; + private static readonly GithubReleasesJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly string HomeDir = AppDomain.CurrentDomain.BaseDirectory; private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update"); @@ -99,22 +101,16 @@ namespace Ryujinx.Modules string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest"; string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL); - JObject jsonRoot = JObject.Parse(fetchedJson); - JToken assets = jsonRoot["assets"]; + var fetched = JsonHelper.Deserialize(fetchedJson, SerializerContext.GithubReleasesJsonResponse); + _buildVer = fetched.Name; - _buildVer = (string)jsonRoot["name"]; - - foreach (JToken asset in assets) + foreach (var asset in fetched.Assets) { - string assetName = (string)asset["name"]; - string assetState = (string)asset["state"]; - string downloadURL = (string)asset["browser_download_url"]; - - if (assetName.StartsWith("test-ava-ryujinx") && assetName.EndsWith(_platformExt)) + if (asset.Name.StartsWith("test-ava-ryujinx") && asset.Name.EndsWith(_platformExt)) { - _buildUrl = downloadURL; + _buildUrl = asset.BrowserDownloadUrl; - if (assetState != "uploaded") + if (asset.State != "uploaded") { if (showVersionUpToDate) { diff --git a/Ryujinx.Ava/UI/Models/Amiibo.cs b/Ryujinx.Ava/UI/Models/Amiibo.cs deleted file mode 100644 index d0ccafd0..00000000 --- a/Ryujinx.Ava/UI/Models/Amiibo.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Ryujinx.Ava.UI.Models -{ - public class Amiibo - { - public struct AmiiboJson - { - [JsonPropertyName("amiibo")] public List Amiibo { get; set; } - [JsonPropertyName("lastUpdated")] public DateTime LastUpdated { get; set; } - } - - public struct AmiiboApi - { - [JsonPropertyName("name")] public string Name { get; set; } - [JsonPropertyName("head")] public string Head { get; set; } - [JsonPropertyName("tail")] public string Tail { get; set; } - [JsonPropertyName("image")] public string Image { get; set; } - [JsonPropertyName("amiiboSeries")] public string AmiiboSeries { get; set; } - [JsonPropertyName("character")] public string Character { get; set; } - [JsonPropertyName("gameSeries")] public string GameSeries { get; set; } - [JsonPropertyName("type")] public string Type { get; set; } - - [JsonPropertyName("release")] public Dictionary Release { get; set; } - - [JsonPropertyName("gamesSwitch")] public List GamesSwitch { get; set; } - - public override string ToString() - { - return Name; - } - - public string GetId() - { - return Head + Tail; - } - - public override bool Equals(object obj) - { - if (obj is AmiiboApi amiibo) - { - return amiibo.Head + amiibo.Tail == Head + Tail; - } - - return false; - } - - public override int GetHashCode() - { - return base.GetHashCode(); - } - } - - public class AmiiboApiGamesSwitch - { - [JsonPropertyName("amiiboUsage")] public List AmiiboUsage { get; set; } - - [JsonPropertyName("gameID")] public List GameId { get; set; } - - [JsonPropertyName("gameName")] public string GameName { get; set; } - } - - public class AmiiboApiUsage - { - [JsonPropertyName("Usage")] public string Usage { get; set; } - - [JsonPropertyName("write")] public bool Write { get; set; } - } - } -} \ No newline at end of file diff --git a/Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs b/Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs index 872c1a37..479411cb 100644 --- a/Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs @@ -122,7 +122,7 @@ namespace Ryujinx.Ava.UI.ViewModels { string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/"); - Supporters = string.Join(", ", JsonHelper.Deserialize(patreonJsonString)) + "\n\n"; + Supporters = string.Join(", ", JsonHelper.Deserialize(patreonJsonString, CommonJsonContext.Default.StringArray)) + "\n\n"; } catch { diff --git a/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs b/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs index 5311318c..090c13a9 100644 --- a/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs @@ -4,11 +4,11 @@ using Avalonia.Media.Imaging; using Avalonia.Threading; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; +using Ryujinx.Ui.Common.Models.Amiibo; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -17,6 +17,7 @@ using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using AmiiboJsonSerializerContext = Ryujinx.Ui.Common.Models.Amiibo.AmiiboJsonSerializerContext; namespace Ryujinx.Ava.UI.ViewModels { @@ -31,8 +32,8 @@ namespace Ryujinx.Ava.UI.ViewModels private readonly StyleableWindow _owner; private Bitmap _amiiboImage; - private List _amiiboList; - private AvaloniaList _amiibos; + private List _amiiboList; + private AvaloniaList _amiibos; private ObservableCollection _amiiboSeries; private int _amiiboSelectedIndex; @@ -41,6 +42,8 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _showAllAmiibo; private bool _useRandomUuid; private string _usage; + + private static readonly AmiiboJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public AmiiboWindowViewModel(StyleableWindow owner, string lastScannedAmiiboId, string titleId) { @@ -52,9 +55,9 @@ namespace Ryujinx.Ava.UI.ViewModels Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo")); _amiiboJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json"); - _amiiboList = new List(); + _amiiboList = new List(); _amiiboSeries = new ObservableCollection(); - _amiibos = new AvaloniaList(); + _amiibos = new AvaloniaList(); _amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png"); @@ -94,7 +97,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public AvaloniaList AmiiboList + public AvaloniaList AmiiboList { get => _amiibos; set @@ -187,9 +190,9 @@ namespace Ryujinx.Ava.UI.ViewModels if (File.Exists(_amiiboJsonPath)) { - amiiboJsonString = File.ReadAllText(_amiiboJsonPath); + amiiboJsonString = await File.ReadAllTextAsync(_amiiboJsonPath); - if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString).LastUpdated)) + if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).LastUpdated)) { amiiboJsonString = await DownloadAmiiboJson(); } @@ -206,7 +209,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - _amiiboList = JsonHelper.Deserialize(amiiboJsonString).Amiibo; + _amiiboList = JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).Amiibo; _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); ParseAmiiboData(); @@ -223,7 +226,7 @@ namespace Ryujinx.Ava.UI.ViewModels { if (!ShowAllAmiibo) { - foreach (Amiibo.AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch) + foreach (AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch) { if (game != null) { @@ -255,7 +258,7 @@ namespace Ryujinx.Ava.UI.ViewModels private void SelectLastScannedAmiibo() { - Amiibo.AmiiboApi scanned = _amiiboList.FirstOrDefault(amiibo => amiibo.GetId() == LastScannedAmiiboId); + AmiiboApi scanned = _amiiboList.FirstOrDefault(amiibo => amiibo.GetId() == LastScannedAmiiboId); SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries); AmiiboSelectedIndex = AmiiboList.IndexOf(scanned); @@ -270,7 +273,7 @@ namespace Ryujinx.Ava.UI.ViewModels return; } - List amiiboSortedList = _amiiboList + List amiiboSortedList = _amiiboList .Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex]) .OrderBy(amiibo => amiibo.Name).ToList(); @@ -280,7 +283,7 @@ namespace Ryujinx.Ava.UI.ViewModels { if (!_showAllAmiibo) { - foreach (Amiibo.AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch) + foreach (AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch) { if (game != null) { @@ -314,7 +317,7 @@ namespace Ryujinx.Ava.UI.ViewModels return; } - Amiibo.AmiiboApi selected = _amiibos[_amiiboSelectedIndex]; + AmiiboApi selected = _amiibos[_amiiboSelectedIndex]; string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image; @@ -326,11 +329,11 @@ namespace Ryujinx.Ava.UI.ViewModels { bool writable = false; - foreach (Amiibo.AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch) + foreach (AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch) { if (item.GameId.Contains(TitleId)) { - foreach (Amiibo.AmiiboApiUsage usageItem in item.AmiiboUsage) + foreach (AmiiboApiUsage usageItem in item.AmiiboUsage) { usageString += Environment.NewLine + $"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}"; diff --git a/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs b/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs index 35256b3b..dd261b10 100644 --- a/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs @@ -51,6 +51,8 @@ namespace Ryujinx.Ava.UI.ViewModels private bool _isLoaded; private readonly UserControl _owner; + private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public IGamepadDriver AvaloniaKeyboardDriver { get; } public IGamepad SelectedGamepad { get; private set; } @@ -706,10 +708,7 @@ namespace Ryujinx.Ava.UI.ViewModels try { - using (Stream stream = File.OpenRead(path)) - { - config = JsonHelper.Deserialize(stream); - } + config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig); } catch (JsonException) { } catch (InvalidOperationException) @@ -775,7 +774,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.ControllerType = Controllers[_controller].Type; - string jsonString = JsonHelper.Serialize(config, true); + string jsonString = JsonHelper.Serialize(config, SerializerContext.InputConfig); await File.WriteAllTextAsync(path, jsonString); diff --git a/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs index e5e4f66b..1d7da9a4 100644 --- a/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs @@ -21,7 +21,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using Path = System.IO.Path; @@ -41,6 +40,8 @@ namespace Ryujinx.Ava.UI.ViewModels private ulong _titleId; private string _titleName; + private static readonly DownloadableContentJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public AvaloniaList DownloadableContents { get => _downloadableContents; @@ -100,7 +101,7 @@ namespace Ryujinx.Ava.UI.ViewModels try { - _downloadableContentContainerList = JsonHelper.DeserializeFromFile>(_downloadableContentJsonPath); + _downloadableContentContainerList = JsonHelper.DeserializeFromFile(_downloadableContentJsonPath, SerializerContext.ListDownloadableContentContainer); } catch { @@ -330,10 +331,7 @@ namespace Ryujinx.Ava.UI.ViewModels _downloadableContentContainerList.Add(container); } - using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough)) - { - downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true))); - } + JsonHelper.SerializeToFile(_downloadableContentJsonPath, _downloadableContentContainerList, SerializerContext.ListDownloadableContentContainer); } } diff --git a/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs index 0798502c..1f4e3c62 100644 --- a/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs +++ b/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs @@ -22,230 +22,231 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using Path = System.IO.Path; using SpanHelpers = LibHac.Common.SpanHelpers; -namespace Ryujinx.Ava.UI.ViewModels; - -public class TitleUpdateViewModel : BaseModel +namespace Ryujinx.Ava.UI.ViewModels { - public TitleUpdateMetadata _titleUpdateWindowData; - public readonly string _titleUpdateJsonPath; - private VirtualFileSystem _virtualFileSystem { get; } - private ulong _titleId { get; } - private string _titleName { get; } - - private AvaloniaList _titleUpdates = new(); - private AvaloniaList _views = new(); - private object _selectedUpdate; - - public AvaloniaList TitleUpdates + public class TitleUpdateViewModel : BaseModel { - get => _titleUpdates; - set + public TitleUpdateMetadata _titleUpdateWindowData; + public readonly string _titleUpdateJsonPath; + private VirtualFileSystem _virtualFileSystem { get; } + private ulong _titleId { get; } + private string _titleName { get; } + + private AvaloniaList _titleUpdates = new(); + private AvaloniaList _views = new(); + private object _selectedUpdate; + + private static readonly TitleUpdateMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public AvaloniaList TitleUpdates { - _titleUpdates = value; - OnPropertyChanged(); - } - } - - public AvaloniaList Views - { - get => _views; - set - { - _views = value; - OnPropertyChanged(); - } - } - - public object SelectedUpdate - { - get => _selectedUpdate; - set - { - _selectedUpdate = value; - OnPropertyChanged(); - } - } - - public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) - { - _virtualFileSystem = virtualFileSystem; - - _titleId = titleId; - _titleName = titleName; - - _titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); - - try - { - _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_titleUpdateJsonPath); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {_titleId} at {_titleUpdateJsonPath}"); - - _titleUpdateWindowData = new TitleUpdateMetadata + get => _titleUpdates; + set { - Selected = "", - Paths = new List() - }; - - Save(); - } - - LoadUpdates(); - } - - private void LoadUpdates() - { - foreach (string path in _titleUpdateWindowData.Paths) - { - AddUpdate(path); - } - - TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected, null); - - SelectedUpdate = selected; - - // NOTE: Save the list again to remove leftovers. - Save(); - - SortUpdates(); - } - - public void SortUpdates() - { - var list = TitleUpdates.ToList(); - - list.Sort((first, second) => - { - if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString())) - { - return -1; - } - else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString())) - { - return 1; - } - - return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1; - }); - - Views.Clear(); - Views.Add(new BaseModel()); - Views.AddRange(list); - - if (SelectedUpdate == null) - { - SelectedUpdate = Views[0]; - } - else if (!TitleUpdates.Contains(SelectedUpdate)) - { - if (Views.Count > 1) - { - SelectedUpdate = Views[1]; - } - else - { - SelectedUpdate = Views[0]; + _titleUpdates = value; + OnPropertyChanged(); } } - } - private void AddUpdate(string path) - { - if (File.Exists(path) && TitleUpdates.All(x => x.Path != path)) + public AvaloniaList Views { - using FileStream file = new(path, FileMode.Open, FileAccess.Read); + get => _views; + set + { + _views = value; + OnPropertyChanged(); + } + } + + public object SelectedUpdate + { + get => _selectedUpdate; + set + { + _selectedUpdate = value; + OnPropertyChanged(); + } + } + + public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) + { + _virtualFileSystem = virtualFileSystem; + + _titleId = titleId; + _titleName = titleName; + + _titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json"); try { - (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0); + _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_titleUpdateJsonPath, SerializerContext.TitleUpdateMetadata); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {_titleId} at {_titleUpdateJsonPath}"); - if (controlNca != null && patchNca != null) + _titleUpdateWindowData = new TitleUpdateMetadata { - ApplicationControlProperty controlData = new(); + Selected = "", + Paths = new List() + }; - using UniqueRef nacpFile = new(); + Save(); + } - controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + LoadUpdates(); + } - TitleUpdates.Add(new TitleUpdateModel(controlData, path)); + private void LoadUpdates() + { + foreach (string path in _titleUpdateWindowData.Paths) + { + AddUpdate(path); + } + + TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected, null); + + SelectedUpdate = selected; + + // NOTE: Save the list again to remove leftovers. + Save(); + SortUpdates(); + } + + public void SortUpdates() + { + var list = TitleUpdates.ToList(); + + list.Sort((first, second) => + { + if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString())) + { + return -1; + } + else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString())) + { + return 1; + } + + return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1; + }); + + Views.Clear(); + Views.Add(new BaseModel()); + Views.AddRange(list); + + if (SelectedUpdate == null) + { + SelectedUpdate = Views[0]; + } + else if (!TitleUpdates.Contains(SelectedUpdate)) + { + if (Views.Count > 1) + { + SelectedUpdate = Views[1]; } else + { + SelectedUpdate = Views[0]; + } + } + } + + private void AddUpdate(string path) + { + if (File.Exists(path) && TitleUpdates.All(x => x.Path != path)) + { + using FileStream file = new(path, FileMode.Open, FileAccess.Read); + + try + { + (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0); + + if (controlNca != null && patchNca != null) + { + ApplicationControlProperty controlData = new(); + + using UniqueRef nacpFile = new(); + + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + + TitleUpdates.Add(new TitleUpdateModel(controlData, path)); + } + else + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]); + }); + } + } + catch (Exception ex) { Dispatcher.UIThread.Post(async () => { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]); + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path)); }); } } - catch (Exception ex) - { - Dispatcher.UIThread.Post(async () => - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path)); - }); - } } - } - public void RemoveUpdate(TitleUpdateModel update) - { - TitleUpdates.Remove(update); - - SortUpdates(); - } - - public async void Add() - { - OpenFileDialog dialog = new() + public void RemoveUpdate(TitleUpdateModel update) { - Title = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle], - AllowMultiple = true - }; + TitleUpdates.Remove(update); - dialog.Filters.Add(new FileDialogFilter + SortUpdates(); + } + + public async void Add() { - Name = "NSP", - Extensions = { "nsp" } - }); - - if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - string[] files = await dialog.ShowAsync(desktop.MainWindow); - - if (files != null) + OpenFileDialog dialog = new() { - foreach (string file in files) + Title = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle], + AllowMultiple = true + }; + + dialog.Filters.Add(new FileDialogFilter + { + Name = "NSP", + Extensions = { "nsp" } + }); + + if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + string[] files = await dialog.ShowAsync(desktop.MainWindow); + + if (files != null) { - AddUpdate(file); + foreach (string file in files) + { + AddUpdate(file); + } } } + + SortUpdates(); } - SortUpdates(); - } - - public void Save() - { - _titleUpdateWindowData.Paths.Clear(); - _titleUpdateWindowData.Selected = ""; - - foreach (TitleUpdateModel update in TitleUpdates) + public void Save() { - _titleUpdateWindowData.Paths.Add(update.Path); + _titleUpdateWindowData.Paths.Clear(); + _titleUpdateWindowData.Selected = ""; - if (update == SelectedUpdate) + foreach (TitleUpdateModel update in TitleUpdates) { - _titleUpdateWindowData.Selected = update.Path; - } - } + _titleUpdateWindowData.Paths.Add(update.Path); - File.WriteAllBytes(_titleUpdateJsonPath, Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true))); + if (update == SelectedUpdate) + { + _titleUpdateWindowData.Selected = update.Path; + } + } + + JsonHelper.SerializeToFile(_titleUpdateJsonPath, _titleUpdateWindowData, SerializerContext.TitleUpdateMetadata); + } } } \ No newline at end of file diff --git a/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs b/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs index 30d41150..c1eaf86f 100644 --- a/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs @@ -42,7 +42,7 @@ namespace Ryujinx.Ava.UI.Views.Main { string languageCode = Path.GetFileNameWithoutExtension(locale).Split('.').Last(); string languageJson = EmbeddedResources.ReadAllText($"{localePath}/{languageCode}{localeExt}"); - var strings = JsonHelper.Deserialize>(languageJson); + var strings = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary); if (!strings.TryGetValue("Language", out string languageName)) { diff --git a/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs index 5368a133..206d0a7e 100644 --- a/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs +++ b/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Interactivity; using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ui.Common.Models.Amiibo; namespace Ryujinx.Ava.UI.Windows { @@ -35,14 +35,14 @@ namespace Ryujinx.Ava.UI.Windows } public bool IsScanned { get; set; } - public Amiibo.AmiiboApi ScannedAmiibo { get; set; } + public AmiiboApi ScannedAmiibo { get; set; } public AmiiboWindowViewModel ViewModel { get; set; } private void ScanButton_Click(object sender, RoutedEventArgs e) { if (ViewModel.AmiiboSelectedIndex > -1) { - Amiibo.AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex]; + AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex]; ScannedAmiibo = amiibo; IsScanned = true; Close(); diff --git a/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs index 1b50c46f..153ce95d 100644 --- a/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs +++ b/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs @@ -6,11 +6,8 @@ using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.Ui.Common.Helper; -using System.IO; -using System.Text; using System.Threading.Tasks; using Button = Avalonia.Controls.Button; diff --git a/Ryujinx.Common/Configuration/AntiAliasing.cs b/Ryujinx.Common/Configuration/AntiAliasing.cs index 6543598c..159108ae 100644 --- a/Ryujinx.Common/Configuration/AntiAliasing.cs +++ b/Ryujinx.Common/Configuration/AntiAliasing.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Common.Configuration +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum AntiAliasing { None, @@ -9,4 +13,4 @@ SmaaHigh, SmaaUltra } -} +} \ No newline at end of file diff --git a/Ryujinx.Common/Configuration/AspectRatioExtensions.cs b/Ryujinx.Common/Configuration/AspectRatioExtensions.cs index 3d0be88e..5e97ed19 100644 --- a/Ryujinx.Common/Configuration/AspectRatioExtensions.cs +++ b/Ryujinx.Common/Configuration/AspectRatioExtensions.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Common.Configuration +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum AspectRatio { Fixed4x3, diff --git a/Ryujinx.Common/Configuration/BackendThreading.cs b/Ryujinx.Common/Configuration/BackendThreading.cs index cfc08914..8833b3f0 100644 --- a/Ryujinx.Common/Configuration/BackendThreading.cs +++ b/Ryujinx.Common/Configuration/BackendThreading.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Common.Configuration +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum BackendThreading { Auto, diff --git a/Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs b/Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs new file mode 100644 index 00000000..132c45a4 --- /dev/null +++ b/Ryujinx.Common/Configuration/DownloadableContentJsonSerializerContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(List))] + public partial class DownloadableContentJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.Common/Configuration/GraphicsBackend.cs b/Ryujinx.Common/Configuration/GraphicsBackend.cs index 26e4a28a..d74dd6e1 100644 --- a/Ryujinx.Common/Configuration/GraphicsBackend.cs +++ b/Ryujinx.Common/Configuration/GraphicsBackend.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Common.Configuration +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum GraphicsBackend { Vulkan, diff --git a/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs b/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs index 556af689..ad12302a 100644 --- a/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs +++ b/Ryujinx.Common/Configuration/GraphicsDebugLevel.cs @@ -1,5 +1,9 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + namespace Ryujinx.Common.Configuration { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum GraphicsDebugLevel { None, diff --git a/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs b/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs index d1c2e4e8..2b9e0af4 100644 --- a/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs +++ b/Ryujinx.Common/Configuration/Hid/Controller/Motion/JsonMotionConfigControllerConverter.cs @@ -1,4 +1,5 @@ -using System; +using Ryujinx.Common.Utilities; +using System; using System.Text.Json; using System.Text.Json.Serialization; @@ -6,6 +7,8 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion { class JsonMotionConfigControllerConverter : JsonConverter { + private static readonly MotionConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static MotionInputBackendType GetMotionInputBackendType(ref Utf8JsonReader reader) { // Temporary reader to get the backend type @@ -52,8 +55,8 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion return motionBackendType switch { - MotionInputBackendType.GamepadDriver => (MotionConfigController)JsonSerializer.Deserialize(ref reader, typeof(StandardMotionConfigController), options), - MotionInputBackendType.CemuHook => (MotionConfigController)JsonSerializer.Deserialize(ref reader, typeof(CemuHookMotionConfigController), options), + MotionInputBackendType.GamepadDriver => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardMotionConfigController), + MotionInputBackendType.CemuHook => JsonSerializer.Deserialize(ref reader, SerializerContext.CemuHookMotionConfigController), _ => throw new InvalidOperationException($"Unknown backend type {motionBackendType}"), }; } @@ -63,10 +66,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion switch (value.MotionBackend) { case MotionInputBackendType.GamepadDriver: - JsonSerializer.Serialize(writer, value as StandardMotionConfigController, options); + JsonSerializer.Serialize(writer, value as StandardMotionConfigController, SerializerContext.StandardMotionConfigController); break; case MotionInputBackendType.CemuHook: - JsonSerializer.Serialize(writer, value as CemuHookMotionConfigController, options); + JsonSerializer.Serialize(writer, value as CemuHookMotionConfigController, SerializerContext.CemuHookMotionConfigController); break; default: throw new ArgumentException($"Unknown motion backend type {value.MotionBackend}"); diff --git a/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs index 832aae0d..7636aa41 100644 --- a/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs +++ b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigController.cs @@ -1,5 +1,8 @@ -namespace Ryujinx.Common.Configuration.Hid.Controller.Motion +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration.Hid.Controller.Motion { + [JsonConverter(typeof(JsonMotionConfigControllerConverter))] public class MotionConfigController { public MotionInputBackendType MotionBackend { get; set; } diff --git a/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs new file mode 100644 index 00000000..5cd9e452 --- /dev/null +++ b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionConfigJsonSerializerContext.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration.Hid.Controller.Motion +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(MotionConfigController))] + [JsonSerializable(typeof(CemuHookMotionConfigController))] + [JsonSerializable(typeof(StandardMotionConfigController))] + public partial class MotionConfigJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs index 45d654ed..c6551047 100644 --- a/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs +++ b/Ryujinx.Common/Configuration/Hid/Controller/Motion/MotionInputBackendType.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Common.Configuration.Hid.Controller.Motion +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration.Hid.Controller.Motion { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum MotionInputBackendType : byte { Invalid, diff --git a/Ryujinx.Common/Configuration/Hid/ControllerType.cs b/Ryujinx.Common/Configuration/Hid/ControllerType.cs index 0ad01bbb..70f811c8 100644 --- a/Ryujinx.Common/Configuration/Hid/ControllerType.cs +++ b/Ryujinx.Common/Configuration/Hid/ControllerType.cs @@ -1,9 +1,12 @@ +using Ryujinx.Common.Utilities; using System; +using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration.Hid { - [Flags] // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical + [Flags] + [JsonConverter(typeof(TypedStringEnumConverter))] public enum ControllerType : int { None, diff --git a/Ryujinx.Common/Configuration/Hid/InputBackendType.cs b/Ryujinx.Common/Configuration/Hid/InputBackendType.cs index 9e944f9e..1db3f570 100644 --- a/Ryujinx.Common/Configuration/Hid/InputBackendType.cs +++ b/Ryujinx.Common/Configuration/Hid/InputBackendType.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Common.Configuration.Hid +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration.Hid { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum InputBackendType { Invalid, diff --git a/Ryujinx.Common/Configuration/Hid/InputConfig.cs b/Ryujinx.Common/Configuration/Hid/InputConfig.cs index 3364e35f..16c8f8e3 100644 --- a/Ryujinx.Common/Configuration/Hid/InputConfig.cs +++ b/Ryujinx.Common/Configuration/Hid/InputConfig.cs @@ -1,8 +1,10 @@ using System.ComponentModel; using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration.Hid { + [JsonConverter(typeof(JsonInputConfigConverter))] public class InputConfig : INotifyPropertyChanged { /// diff --git a/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs b/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs new file mode 100644 index 00000000..254c4feb --- /dev/null +++ b/Ryujinx.Common/Configuration/Hid/InputConfigJsonSerializerContext.cs @@ -0,0 +1,14 @@ +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration.Hid +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(InputConfig))] + [JsonSerializable(typeof(StandardKeyboardInputConfig))] + [JsonSerializable(typeof(StandardControllerInputConfig))] + public partial class InputConfigJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs b/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs index 7223ad45..08bbcbf1 100644 --- a/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs +++ b/Ryujinx.Common/Configuration/Hid/JsonInputConfigConverter.cs @@ -1,13 +1,16 @@ using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Utilities; using System; using System.Text.Json; using System.Text.Json.Serialization; namespace Ryujinx.Common.Configuration.Hid { - class JsonInputConfigConverter : JsonConverter + public class JsonInputConfigConverter : JsonConverter { + private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static InputBackendType GetInputBackendType(ref Utf8JsonReader reader) { // Temporary reader to get the backend type @@ -54,8 +57,8 @@ namespace Ryujinx.Common.Configuration.Hid return backendType switch { - InputBackendType.WindowKeyboard => (InputConfig)JsonSerializer.Deserialize(ref reader, typeof(StandardKeyboardInputConfig), options), - InputBackendType.GamepadSDL2 => (InputConfig)JsonSerializer.Deserialize(ref reader, typeof(StandardControllerInputConfig), options), + InputBackendType.WindowKeyboard => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardKeyboardInputConfig), + InputBackendType.GamepadSDL2 => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardControllerInputConfig), _ => throw new InvalidOperationException($"Unknown backend type {backendType}"), }; } @@ -65,10 +68,10 @@ namespace Ryujinx.Common.Configuration.Hid switch (value.Backend) { case InputBackendType.WindowKeyboard: - JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, options); + JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, SerializerContext.StandardKeyboardInputConfig); break; case InputBackendType.GamepadSDL2: - JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, options); + JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, SerializerContext.StandardControllerInputConfig); break; default: throw new ArgumentException($"Unknown backend type {value.Backend}"); diff --git a/Ryujinx.Common/Configuration/Hid/Key.cs b/Ryujinx.Common/Configuration/Hid/Key.cs index 194843a3..3501b8ae 100644 --- a/Ryujinx.Common/Configuration/Hid/Key.cs +++ b/Ryujinx.Common/Configuration/Hid/Key.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Common.Configuration.Hid +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration.Hid { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum Key { Unknown, @@ -136,4 +140,4 @@ Count } -} +} \ No newline at end of file diff --git a/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index 45217e02..8b6c8c14 100644 --- a/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -1,6 +1,6 @@ namespace Ryujinx.Common.Configuration.Hid { - public class KeyboardHotkeys + public struct KeyboardHotkeys { public Key ToggleVsync { get; set; } public Key Screenshot { get; set; } @@ -12,4 +12,4 @@ public Key VolumeUp { get; set; } public Key VolumeDown { get; set; } } -} +} \ No newline at end of file diff --git a/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs b/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs index 2e34cb96..dd6495d4 100644 --- a/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs +++ b/Ryujinx.Common/Configuration/Hid/PlayerIndex.cs @@ -1,6 +1,10 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + namespace Ryujinx.Common.Configuration.Hid { // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical + [JsonConverter(typeof(TypedStringEnumConverter))] public enum PlayerIndex : int { Player1 = 0, diff --git a/Ryujinx.Common/Configuration/MemoryManagerMode.cs b/Ryujinx.Common/Configuration/MemoryManagerMode.cs index ad6c2a34..f10fd6f1 100644 --- a/Ryujinx.Common/Configuration/MemoryManagerMode.cs +++ b/Ryujinx.Common/Configuration/MemoryManagerMode.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Common.Configuration +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum MemoryManagerMode : byte { SoftwarePageTable, diff --git a/Ryujinx.Common/Configuration/ScalingFilter.cs b/Ryujinx.Common/Configuration/ScalingFilter.cs index 2095b89b..e38c7d73 100644 --- a/Ryujinx.Common/Configuration/ScalingFilter.cs +++ b/Ryujinx.Common/Configuration/ScalingFilter.cs @@ -1,5 +1,9 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + namespace Ryujinx.Common.Configuration { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum ScalingFilter { Bilinear, diff --git a/Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs b/Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs new file mode 100644 index 00000000..5b661b87 --- /dev/null +++ b/Ryujinx.Common/Configuration/TitleUpdateMetadataJsonSerializerContext.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(TitleUpdateMetadata))] + public partial class TitleUpdateMetadataJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs b/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs index b9a08323..28a7d546 100644 --- a/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs +++ b/Ryujinx.Common/Logging/Formatters/DefaultLogFormatter.cs @@ -1,22 +1,20 @@ -using System; -using System.Reflection; -using System.Text; +using System.Text; namespace Ryujinx.Common.Logging { internal class DefaultLogFormatter : ILogFormatter { - private static readonly ObjectPool _stringBuilderPool = SharedPools.Default(); + private static readonly ObjectPool StringBuilderPool = SharedPools.Default(); public string Format(LogEventArgs args) { - StringBuilder sb = _stringBuilderPool.Allocate(); + StringBuilder sb = StringBuilderPool.Allocate(); try { sb.Clear(); - sb.AppendFormat(@"{0:hh\:mm\:ss\.fff}", args.Time); + sb.Append($@"{args.Time:hh\:mm\:ss\.fff}"); sb.Append($" |{args.Level.ToString()[0]}| "); if (args.ThreadName != null) @@ -27,53 +25,17 @@ namespace Ryujinx.Common.Logging sb.Append(args.Message); - if (args.Data != null) + if (args.Data is not null) { - PropertyInfo[] props = args.Data.GetType().GetProperties(); - - sb.Append(" {"); - - foreach (var prop in props) - { - sb.Append(prop.Name); - sb.Append(": "); - - if (typeof(Array).IsAssignableFrom(prop.PropertyType)) - { - Array array = (Array)prop.GetValue(args.Data); - foreach (var item in array) - { - sb.Append(item.ToString()); - sb.Append(", "); - } - - if (array.Length > 0) - { - sb.Remove(sb.Length - 2, 2); - } - } - else - { - sb.Append(prop.GetValue(args.Data)); - } - - sb.Append(" ; "); - } - - // We remove the final ';' from the string - if (props.Length > 0) - { - sb.Remove(sb.Length - 3, 3); - } - - sb.Append('}'); + sb.Append(' '); + DynamicObjectFormatter.Format(sb, args.Data); } return sb.ToString(); } finally { - _stringBuilderPool.Release(sb); + StringBuilderPool.Release(sb); } } } diff --git a/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs b/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs new file mode 100644 index 00000000..5f15cc2a --- /dev/null +++ b/Ryujinx.Common/Logging/Formatters/DynamicObjectFormatter.cs @@ -0,0 +1,84 @@ +#nullable enable +using System; +using System.Reflection; +using System.Text; + +namespace Ryujinx.Common.Logging +{ + internal class DynamicObjectFormatter + { + private static readonly ObjectPool StringBuilderPool = SharedPools.Default(); + + public static string? Format(object? dynamicObject) + { + if (dynamicObject is null) + { + return null; + } + + StringBuilder sb = StringBuilderPool.Allocate(); + + try + { + Format(sb, dynamicObject); + + return sb.ToString(); + } + finally + { + StringBuilderPool.Release(sb); + } + } + + public static void Format(StringBuilder sb, object? dynamicObject) + { + if (dynamicObject is null) + { + return; + } + + PropertyInfo[] props = dynamicObject.GetType().GetProperties(); + + sb.Append('{'); + + foreach (var prop in props) + { + sb.Append(prop.Name); + sb.Append(": "); + + if (typeof(Array).IsAssignableFrom(prop.PropertyType)) + { + Array? array = (Array?) prop.GetValue(dynamicObject); + + if (array is not null) + { + foreach (var item in array) + { + sb.Append(item); + sb.Append(", "); + } + + if (array.Length > 0) + { + sb.Remove(sb.Length - 2, 2); + } + } + } + else + { + sb.Append(prop.GetValue(dynamicObject)); + } + + sb.Append(" ; "); + } + + // We remove the final ';' from the string + if (props.Length > 0) + { + sb.Remove(sb.Length - 3, 3); + } + + sb.Append('}'); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Common/Logging/LogClass.cs b/Ryujinx.Common/Logging/LogClass.cs index 7e53c972..e62676cd 100644 --- a/Ryujinx.Common/Logging/LogClass.cs +++ b/Ryujinx.Common/Logging/LogClass.cs @@ -1,5 +1,9 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + namespace Ryujinx.Common.Logging { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum LogClass { Application, diff --git a/Ryujinx.Common/Logging/LogEventArgs.cs b/Ryujinx.Common/Logging/LogEventArgs.cs index 511c8e6e..a27af780 100644 --- a/Ryujinx.Common/Logging/LogEventArgs.cs +++ b/Ryujinx.Common/Logging/LogEventArgs.cs @@ -11,15 +11,7 @@ namespace Ryujinx.Common.Logging public readonly string Message; public readonly object Data; - public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message) - { - Level = level; - Time = time; - ThreadName = threadName; - Message = message; - } - - public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data) + public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data = null) { Level = level; Time = time; diff --git a/Ryujinx.Common/Logging/LogEventArgsJson.cs b/Ryujinx.Common/Logging/LogEventArgsJson.cs new file mode 100644 index 00000000..425b9766 --- /dev/null +++ b/Ryujinx.Common/Logging/LogEventArgsJson.cs @@ -0,0 +1,30 @@ +using System; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Logging +{ + internal class LogEventArgsJson + { + public LogLevel Level { get; } + public TimeSpan Time { get; } + public string ThreadName { get; } + + public string Message { get; } + public string Data { get; } + + [JsonConstructor] + public LogEventArgsJson(LogLevel level, TimeSpan time, string threadName, string message, string data = null) + { + Level = level; + Time = time; + ThreadName = threadName; + Message = message; + Data = data; + } + + public static LogEventArgsJson FromLogEventArgs(LogEventArgs args) + { + return new LogEventArgsJson(args.Level, args.Time, args.ThreadName, args.Message, DynamicObjectFormatter.Format(args.Data)); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs b/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs new file mode 100644 index 00000000..da21f11e --- /dev/null +++ b/Ryujinx.Common/Logging/LogEventJsonSerializerContext.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Logging +{ + [JsonSerializable(typeof(LogEventArgsJson))] + internal partial class LogEventJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.Common/Logging/LogLevel.cs b/Ryujinx.Common/Logging/LogLevel.cs index 8857fb45..3786c756 100644 --- a/Ryujinx.Common/Logging/LogLevel.cs +++ b/Ryujinx.Common/Logging/LogLevel.cs @@ -1,5 +1,9 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + namespace Ryujinx.Common.Logging { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum LogLevel { Debug, diff --git a/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs b/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs index 95f96576..06976433 100644 --- a/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs +++ b/Ryujinx.Common/Logging/Targets/JsonLogTarget.cs @@ -1,5 +1,5 @@ -using System.IO; -using System.Text.Json; +using Ryujinx.Common.Utilities; +using System.IO; namespace Ryujinx.Common.Logging { @@ -25,12 +25,8 @@ namespace Ryujinx.Common.Logging public void Log(object sender, LogEventArgs e) { - string text = JsonSerializer.Serialize(e); - - using (BinaryWriter writer = new BinaryWriter(_stream)) - { - writer.Write(text); - } + var logEventArgsJson = LogEventArgsJson.FromLogEventArgs(e); + JsonHelper.SerializeToStream(_stream, logEventArgsJson, LogEventJsonSerializerContext.Default.LogEventArgsJson); } public void Dispose() diff --git a/Ryujinx.Common/Utilities/CommonJsonContext.cs b/Ryujinx.Common/Utilities/CommonJsonContext.cs new file mode 100644 index 00000000..d7b3f78c --- /dev/null +++ b/Ryujinx.Common/Utilities/CommonJsonContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Utilities +{ + [JsonSerializable(typeof(string[]), TypeInfoPropertyName = "StringArray")] + [JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "StringDictionary")] + public partial class CommonJsonContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.Common/Utilities/JsonHelper.cs b/Ryujinx.Common/Utilities/JsonHelper.cs index 36f39114..9a2d6f18 100644 --- a/Ryujinx.Common/Utilities/JsonHelper.cs +++ b/Ryujinx.Common/Utilities/JsonHelper.cs @@ -1,15 +1,62 @@ -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Configuration.Hid.Controller.Motion; -using System.IO; +using System.IO; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace Ryujinx.Common.Utilities { public class JsonHelper { - public static JsonNamingPolicy SnakeCase { get; } + private static readonly JsonNamingPolicy SnakeCasePolicy = new SnakeCaseNamingPolicy(); + private const int DefaultFileWriteBufferSize = 4096; + + /// + /// Creates new serializer options with default settings. + /// + /// + /// It is REQUIRED for you to save returned options statically or as a part of static serializer context + /// in order to avoid performance issues. You can safely modify returned options for your case before storing. + /// + public static JsonSerializerOptions GetDefaultSerializerOptions(bool indented = true) + { + JsonSerializerOptions options = new() + { + DictionaryKeyPolicy = SnakeCasePolicy, + PropertyNamingPolicy = SnakeCasePolicy, + WriteIndented = indented, + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip + }; + + return options; + } + + public static string Serialize(T value, JsonTypeInfo typeInfo) + { + return JsonSerializer.Serialize(value, typeInfo); + } + + public static T Deserialize(string value, JsonTypeInfo typeInfo) + { + return JsonSerializer.Deserialize(value, typeInfo); + } + + public static void SerializeToFile(string filePath, T value, JsonTypeInfo typeInfo) + { + using FileStream file = File.Create(filePath, DefaultFileWriteBufferSize, FileOptions.WriteThrough); + JsonSerializer.Serialize(file, value, typeInfo); + } + + public static T DeserializeFromFile(string filePath, JsonTypeInfo typeInfo) + { + using FileStream file = File.OpenRead(filePath); + return JsonSerializer.Deserialize(file, typeInfo); + } + + public static void SerializeToStream(Stream stream, T value, JsonTypeInfo typeInfo) + { + JsonSerializer.Serialize(stream, value, typeInfo); + } private class SnakeCaseNamingPolicy : JsonNamingPolicy { @@ -20,7 +67,7 @@ namespace Ryujinx.Common.Utilities return name; } - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new(); for (int i = 0; i < name.Length; i++) { @@ -34,7 +81,7 @@ namespace Ryujinx.Common.Utilities } else { - builder.Append("_"); + builder.Append('_'); builder.Append(char.ToLowerInvariant(c)); } } @@ -47,64 +94,5 @@ namespace Ryujinx.Common.Utilities return builder.ToString(); } } - - static JsonHelper() - { - SnakeCase = new SnakeCaseNamingPolicy(); - } - - public static JsonSerializerOptions GetDefaultSerializerOptions(bool prettyPrint = false) - { - JsonSerializerOptions options = new JsonSerializerOptions - { - DictionaryKeyPolicy = SnakeCase, - PropertyNamingPolicy = SnakeCase, - WriteIndented = prettyPrint, - AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip - }; - - options.Converters.Add(new JsonStringEnumConverter()); - options.Converters.Add(new JsonInputConfigConverter()); - options.Converters.Add(new JsonMotionConfigControllerConverter()); - - return options; - } - - public static T Deserialize(Stream stream) - { - using (BinaryReader reader = new BinaryReader(stream)) - { - return JsonSerializer.Deserialize(reader.ReadBytes((int)(stream.Length - stream.Position)), GetDefaultSerializerOptions()); - } - } - - public static T DeserializeFromFile(string path) - { - return Deserialize(File.ReadAllText(path)); - } - - public static T Deserialize(string json) - { - return JsonSerializer.Deserialize(json, GetDefaultSerializerOptions()); - } - - public static void Serialize(Stream stream, TValue obj, bool prettyPrint = false) - { - using (BinaryWriter writer = new BinaryWriter(stream)) - { - writer.Write(SerializeToUtf8Bytes(obj, prettyPrint)); - } - } - - public static string Serialize(TValue obj, bool prettyPrint = false) - { - return JsonSerializer.Serialize(obj, GetDefaultSerializerOptions(prettyPrint)); - } - - public static byte[] SerializeToUtf8Bytes(T obj, bool prettyPrint = false) - { - return JsonSerializer.SerializeToUtf8Bytes(obj, GetDefaultSerializerOptions(prettyPrint)); - } } -} +} \ No newline at end of file diff --git a/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs b/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs new file mode 100644 index 00000000..c0127dc4 --- /dev/null +++ b/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs @@ -0,0 +1,34 @@ +#nullable enable +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Utilities +{ + /// + /// Specifies that value of will be serialized as string in JSONs + /// + /// + /// Trimming friendly alternative to . + /// Get rid of this converter if dotnet supports similar functionality out of the box. + /// + /// Type of enum to serialize + public sealed class TypedStringEnumConverter : JsonConverter where TEnum : struct, Enum + { + public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var enumValue = reader.GetString(); + if (string.IsNullOrEmpty(enumValue)) + { + return default; + } + + return Enum.Parse(enumValue); + } + + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } +} diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs index ec0b0a10..535779d2 100644 --- a/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs +++ b/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs @@ -1,11 +1,11 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Account.Acc.Types; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; -using System.Text.Json.Serialization; namespace Ryujinx.HLE.HOS.Services.Account.Acc { @@ -13,29 +13,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc { private readonly string _profilesJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "Profiles.json"); - private struct ProfilesJson - { - [JsonPropertyName("profiles")] - public List Profiles { get; set; } - [JsonPropertyName("last_opened")] - public string LastOpened { get; set; } - } - - private struct UserProfileJson - { - [JsonPropertyName("user_id")] - public string UserId { get; set; } - [JsonPropertyName("name")] - public string Name { get; set; } - [JsonPropertyName("account_state")] - public AccountState AccountState { get; set; } - [JsonPropertyName("online_play_state")] - public AccountState OnlinePlayState { get; set; } - [JsonPropertyName("last_modified_timestamp")] - public long LastModifiedTimestamp { get; set; } - [JsonPropertyName("image")] - public byte[] Image { get; set; } - } + private static readonly ProfilesJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public UserId LastOpened { get; set; } @@ -47,7 +25,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc { try { - ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath); + ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath, SerializerContext.ProfilesJson); foreach (var profile in profilesJson.Profiles) { @@ -92,7 +70,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc }); } - File.WriteAllText(_profilesJsonPath, JsonHelper.Serialize(profilesJson, true)); + JsonHelper.SerializeToFile(_profilesJsonPath, profilesJson, SerializerContext.ProfilesJson); } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs new file mode 100644 index 00000000..6b54898e --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Account/Acc/ProfilesJsonSerializerContext.cs @@ -0,0 +1,11 @@ +using Ryujinx.HLE.HOS.Services.Account.Acc.Types; +using System.Text.Json.Serialization; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ProfilesJson))] + internal partial class ProfilesJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs index 2382a255..1699abfb 100644 --- a/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs +++ b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/AccountState.cs @@ -1,5 +1,9 @@ +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + namespace Ryujinx.HLE.HOS.Services.Account.Acc { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum AccountState { Closed, diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs new file mode 100644 index 00000000..09f9d142 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/ProfilesJson.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Services.Account.Acc.Types +{ + internal struct ProfilesJson + { + public List Profiles { get; set; } + public string LastOpened { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs new file mode 100644 index 00000000..06ff4833 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Account/Acc/Types/UserProfileJson.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.HOS.Services.Account.Acc.Types +{ + internal struct UserProfileJson + { + public string UserId { get; set; } + public string Name { get; set; } + public AccountState AccountState { get; set; } + public AccountState OnlinePlayState { get; set; } + public long LastModifiedTimestamp { get; set; } + public byte[] Image { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/AmiiboJsonSerializerContext.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/AmiiboJsonSerializerContext.cs new file mode 100644 index 00000000..e75f6200 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/AmiiboJsonSerializerContext.cs @@ -0,0 +1,10 @@ +using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; +using System.Text.Json.Serialization; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp +{ + [JsonSerializable(typeof(VirtualAmiiboFile))] + internal partial class AmiiboJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs index 4fdeadcb..9166e87f 100644 --- a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs +++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration; using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; using Ryujinx.Cpu; using Ryujinx.HLE.HOS.Services.Mii; using Ryujinx.HLE.HOS.Services.Mii.Types; @@ -8,8 +9,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; -using System.Text.Json; namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp { @@ -17,6 +16,8 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp { private static uint _openedApplicationAreaId; + private static readonly AmiiboJsonSerializerContext SerializerContext = AmiiboJsonSerializerContext.Default; + public static byte[] GenerateUuid(string amiiboId, bool useRandomUuid) { if (useRandomUuid) @@ -173,7 +174,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp if (File.Exists(filePath)) { - virtualAmiiboFile = JsonSerializer.Deserialize(File.ReadAllText(filePath), new JsonSerializerOptions(JsonSerializerDefaults.General)); + virtualAmiiboFile = JsonHelper.DeserializeFromFile(filePath, SerializerContext.VirtualAmiiboFile); } else { @@ -197,8 +198,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp private static void SaveAmiiboFile(VirtualAmiiboFile virtualAmiiboFile) { string filePath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json"); - - File.WriteAllText(filePath, JsonSerializer.Serialize(virtualAmiiboFile)); + JsonHelper.SerializeToFile(filePath, virtualAmiiboFile, SerializerContext.VirtualAmiiboFile); } } } diff --git a/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs index 5147f5c3..e93802ae 100644 --- a/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs +++ b/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs @@ -17,6 +17,9 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions { public static class PartitionFileSystemExtensions { + private static readonly DownloadableContentJsonSerializerContext ContentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly TitleUpdateMetadataJsonSerializerContext TitleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + internal static (bool, ProcessResult) TryLoad(this PartitionFileSystem partitionFileSystem, Switch device, string path, out string errorMessage) { errorMessage = null; @@ -85,7 +88,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); if (File.Exists(titleUpdateMetadataPath)) { - string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath).Selected; + string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, TitleSerializerContext.TitleUpdateMetadata).Selected; if (File.Exists(updatePath)) { PartitionFileSystem updatePartitionFileSystem = new(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()); @@ -139,7 +142,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions string addOnContentMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json"); if (File.Exists(addOnContentMetadataPath)) { - List dlcContainerList = JsonHelper.DeserializeFromFile>(addOnContentMetadataPath); + List dlcContainerList = JsonHelper.DeserializeFromFile(addOnContentMetadataPath, ContentSerializerContext.ListDownloadableContentContainer); foreach (DownloadableContentContainer downloadableContentContainer in dlcContainerList) { diff --git a/Ryujinx.Headless.SDL2/Program.cs b/Ryujinx.Headless.SDL2/Program.cs index 54ab18cd..8dff6a1d 100644 --- a/Ryujinx.Headless.SDL2/Program.cs +++ b/Ryujinx.Headless.SDL2/Program.cs @@ -56,6 +56,8 @@ namespace Ryujinx.Headless.SDL2 private static bool _enableKeyboard; private static bool _enableMouse; + private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + static void Main(string[] args) { Version = ReleaseInformation.GetVersion(); @@ -285,10 +287,7 @@ namespace Ryujinx.Headless.SDL2 try { - using (Stream stream = File.OpenRead(path)) - { - config = JsonHelper.Deserialize(stream); - } + config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig); } catch (JsonException) { diff --git a/Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs b/Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs new file mode 100644 index 00000000..f81121c2 --- /dev/null +++ b/Ryujinx.Ui.Common/App/ApplicationJsonSerializerContext.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.App.Common +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ApplicationMetadata))] + internal partial class ApplicationJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index 113e9cb3..8686383e 100644 --- a/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -10,6 +10,7 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.Loaders.Npdm; @@ -22,7 +23,6 @@ using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; -using JsonHelper = Ryujinx.Common.Utilities.JsonHelper; using Path = System.IO.Path; namespace Ryujinx.Ui.App.Common @@ -42,6 +42,9 @@ namespace Ryujinx.Ui.App.Common private Language _desiredTitleLanguage; private CancellationTokenSource _cancellationToken; + private static readonly ApplicationJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly TitleUpdateMetadataJsonSerializerContext TitleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public ApplicationLibrary(VirtualFileSystem virtualFileSystem) { _virtualFileSystem = virtualFileSystem; @@ -489,14 +492,12 @@ namespace Ryujinx.Ui.App.Common appMetadata = new ApplicationMetadata(); - using FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough); - - JsonHelper.Serialize(stream, appMetadata, true); + JsonHelper.SerializeToFile(metadataFile, appMetadata, SerializerContext.ApplicationMetadata); } try { - appMetadata = JsonHelper.DeserializeFromFile(metadataFile); + appMetadata = JsonHelper.DeserializeFromFile(metadataFile, SerializerContext.ApplicationMetadata); } catch (JsonException) { @@ -509,9 +510,7 @@ namespace Ryujinx.Ui.App.Common { modifyFunction(appMetadata); - using FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough); - - JsonHelper.Serialize(stream, appMetadata, true); + JsonHelper.SerializeToFile(metadataFile, appMetadata, SerializerContext.ApplicationMetadata); } return appMetadata; @@ -890,7 +889,7 @@ namespace Ryujinx.Ui.App.Common if (File.Exists(titleUpdateMetadataPath)) { - updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath).Selected; + updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, TitleSerializerContext.TitleUpdateMetadata).Selected; if (File.Exists(updatePath)) { diff --git a/Ryujinx.Ui.Common/Configuration/AudioBackend.cs b/Ryujinx.Ui.Common/Configuration/AudioBackend.cs index 99111ea6..1f9bd0ba 100644 --- a/Ryujinx.Ui.Common/Configuration/AudioBackend.cs +++ b/Ryujinx.Ui.Common/Configuration/AudioBackend.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Ui.Common.Configuration +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.Common.Configuration { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum AudioBackend { Dummy, diff --git a/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs b/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs index e9aec04b..14c03957 100644 --- a/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs +++ b/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs @@ -5,7 +5,7 @@ using Ryujinx.Common.Utilities; using Ryujinx.Ui.Common.Configuration.System; using Ryujinx.Ui.Common.Configuration.Ui; using System.Collections.Generic; -using System.IO; +using System.Text.Json.Nodes; namespace Ryujinx.Ui.Common.Configuration { @@ -321,14 +321,14 @@ namespace Ryujinx.Ui.Common.Configuration /// /// Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions) /// TODO: Remove this when those older versions aren't in use anymore. - public List KeyboardConfig { get; set; } + public List KeyboardConfig { get; set; } /// /// Legacy controller control bindings /// /// Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions) /// TODO: Remove this when those older versions aren't in use anymore. - public List ControllerConfig { get; set; } + public List ControllerConfig { get; set; } /// /// Input configurations @@ -354,11 +354,12 @@ namespace Ryujinx.Ui.Common.Configuration /// Loads a configuration file from disk /// /// The path to the JSON configuration file + /// Parsed configuration file public static bool TryLoad(string path, out ConfigurationFileFormat configurationFileFormat) { try { - configurationFileFormat = JsonHelper.DeserializeFromFile(path); + configurationFileFormat = JsonHelper.DeserializeFromFile(path, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat); return configurationFileFormat.Version != 0; } @@ -376,8 +377,7 @@ namespace Ryujinx.Ui.Common.Configuration /// The path to the JSON configuration file public void SaveConfig(string path) { - using FileStream fileStream = File.Create(path, 4096, FileOptions.WriteThrough); - JsonHelper.Serialize(fileStream, this, true); + JsonHelper.SerializeToFile(path, this, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat); } } } diff --git a/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs b/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs new file mode 100644 index 00000000..6ce2ef01 --- /dev/null +++ b/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormatSettings.cs @@ -0,0 +1,9 @@ +using Ryujinx.Common.Utilities; + +namespace Ryujinx.Ui.Common.Configuration +{ + internal static class ConfigurationFileFormatSettings + { + public static readonly ConfigurationJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + } +} \ No newline at end of file diff --git a/Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs b/Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs new file mode 100644 index 00000000..bb8dfb49 --- /dev/null +++ b/Ryujinx.Ui.Common/Configuration/ConfigurationJsonSerializerContext.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.Common.Configuration +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(ConfigurationFileFormat))] + internal partial class ConfigurationJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs index bcdd2e70..82a331c1 100644 --- a/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs +++ b/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs @@ -9,6 +9,7 @@ using Ryujinx.Ui.Common.Configuration.Ui; using Ryujinx.Ui.Common.Helper; using System; using System.Collections.Generic; +using System.Text.Json.Nodes; namespace Ryujinx.Ui.Common.Configuration { @@ -631,8 +632,8 @@ namespace Ryujinx.Ui.Common.Configuration EnableKeyboard = Hid.EnableKeyboard, EnableMouse = Hid.EnableMouse, Hotkeys = Hid.Hotkeys, - KeyboardConfig = new List(), - ControllerConfig = new List(), + KeyboardConfig = new List(), + ControllerConfig = new List(), InputConfig = Hid.InputConfig, GraphicsBackend = Graphics.GraphicsBackend, PreferredGpu = Graphics.PreferredGpu diff --git a/Ryujinx.Ui.Common/Configuration/System/Language.cs b/Ryujinx.Ui.Common/Configuration/System/Language.cs index 3d2dc991..404f8063 100644 --- a/Ryujinx.Ui.Common/Configuration/System/Language.cs +++ b/Ryujinx.Ui.Common/Configuration/System/Language.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Ui.Common.Configuration.System +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.Common.Configuration.System { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum Language { Japanese, diff --git a/Ryujinx.Ui.Common/Configuration/System/Region.cs b/Ryujinx.Ui.Common/Configuration/System/Region.cs index fb51e08e..7dfac638 100644 --- a/Ryujinx.Ui.Common/Configuration/System/Region.cs +++ b/Ryujinx.Ui.Common/Configuration/System/Region.cs @@ -1,5 +1,9 @@ -namespace Ryujinx.Ui.Common.Configuration.System +using Ryujinx.Common.Utilities; +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.Common.Configuration.System { + [JsonConverter(typeof(TypedStringEnumConverter))] public enum Region { Japan, diff --git a/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs new file mode 100644 index 00000000..f412b950 --- /dev/null +++ b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApi.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.Common.Models.Amiibo +{ + public struct AmiiboApi : IEquatable + { + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("head")] + public string Head { get; set; } + [JsonPropertyName("tail")] + public string Tail { get; set; } + [JsonPropertyName("image")] + public string Image { get; set; } + [JsonPropertyName("amiiboSeries")] + public string AmiiboSeries { get; set; } + [JsonPropertyName("character")] + public string Character { get; set; } + [JsonPropertyName("gameSeries")] + public string GameSeries { get; set; } + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("release")] + public Dictionary Release { get; set; } + + [JsonPropertyName("gamesSwitch")] + public List GamesSwitch { get; set; } + + public override string ToString() + { + return Name; + } + + public string GetId() + { + return Head + Tail; + } + + public bool Equals(AmiiboApi other) + { + return Head + Tail == other.Head + other.Tail; + } + + public override bool Equals(object obj) + { + return obj is AmiiboApi other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Head, Tail); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs new file mode 100644 index 00000000..def7d1bc --- /dev/null +++ b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiGamesSwitch.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.Common.Models.Amiibo +{ + public class AmiiboApiGamesSwitch + { + [JsonPropertyName("amiiboUsage")] + public List AmiiboUsage { get; set; } + [JsonPropertyName("gameID")] + public List GameId { get; set; } + [JsonPropertyName("gameName")] + public string GameName { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs new file mode 100644 index 00000000..814573c2 --- /dev/null +++ b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboApiUsage.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.Common.Models.Amiibo +{ + public class AmiiboApiUsage + { + [JsonPropertyName("Usage")] + public string Usage { get; set; } + [JsonPropertyName("write")] + public bool Write { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs new file mode 100644 index 00000000..feb7993c --- /dev/null +++ b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJson.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.Common.Models.Amiibo +{ + public struct AmiiboJson + { + [JsonPropertyName("amiibo")] + public List Amiibo { get; set; } + [JsonPropertyName("lastUpdated")] + public DateTime LastUpdated { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs new file mode 100644 index 00000000..4cbb5a7b --- /dev/null +++ b/Ryujinx.Ui.Common/Models/Amiibo/AmiiboJsonSerializerContext.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.Common.Models.Amiibo +{ + [JsonSerializable(typeof(AmiiboJson))] + public partial class AmiiboJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs b/Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs new file mode 100644 index 00000000..10d01478 --- /dev/null +++ b/Ryujinx.Ui.Common/Models/Github/GithubReleaseAssetJsonResponse.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Ui.Common.Models.Github +{ + public class GithubReleaseAssetJsonResponse + { + public string Name { get; set; } + public string State { get; set; } + public string BrowserDownloadUrl { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs b/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs new file mode 100644 index 00000000..954d03e3 --- /dev/null +++ b/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Ryujinx.Ui.Common.Models.Github +{ + public class GithubReleasesJsonResponse + { + public string Name { get; set; } + public List Assets { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs b/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs new file mode 100644 index 00000000..e5fd9d09 --- /dev/null +++ b/Ryujinx.Ui.Common/Models/Github/GithubReleasesJsonSerializerContext.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.Common.Models.Github +{ + [JsonSerializable(typeof(GithubReleasesJsonResponse), GenerationMode = JsonSourceGenerationMode.Metadata)] + public partial class GithubReleasesJsonSerializerContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Updater/Updater.cs b/Ryujinx/Modules/Updater/Updater.cs index 5ad5924e..3f186ce6 100644 --- a/Ryujinx/Modules/Updater/Updater.cs +++ b/Ryujinx/Modules/Updater/Updater.cs @@ -2,14 +2,14 @@ using Gtk; using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Zip; -using Newtonsoft.Json.Linq; using Ryujinx.Common; using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; using Ryujinx.Ui; +using Ryujinx.Ui.Common.Models.Github; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -38,6 +38,8 @@ namespace Ryujinx.Modules private static string _buildUrl; private static long _buildSize; + private static readonly GithubReleasesJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + // On Windows, GtkSharp.Dependencies adds these extra dirs that must be cleaned during updates. private static readonly string[] WindowsDependencyDirs = new string[] { "bin", "etc", "lib", "share" }; @@ -107,22 +109,16 @@ namespace Ryujinx.Modules // Fetch latest build information string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL); - JObject jsonRoot = JObject.Parse(fetchedJson); - JToken assets = jsonRoot["assets"]; + var fetched = JsonHelper.Deserialize(fetchedJson, SerializerContext.GithubReleasesJsonResponse); + _buildVer = fetched.Name; - _buildVer = (string)jsonRoot["name"]; - - foreach (JToken asset in assets) + foreach (var asset in fetched.Assets) { - string assetName = (string)asset["name"]; - string assetState = (string)asset["state"]; - string downloadURL = (string)asset["browser_download_url"]; - - if (assetName.StartsWith("ryujinx") && assetName.EndsWith(_platformExt)) + if (asset.Name.StartsWith("ryujinx") && asset.Name.EndsWith(_platformExt)) { - _buildUrl = downloadURL; + _buildUrl = asset.BrowserDownloadUrl; - if (assetState != "uploaded") + if (asset.State != "uploaded") { if (showVersionUpToDate) { diff --git a/Ryujinx/Ui/Windows/AboutWindow.cs b/Ryujinx/Ui/Windows/AboutWindow.cs index ea827a92..41cf9c01 100644 --- a/Ryujinx/Ui/Windows/AboutWindow.cs +++ b/Ryujinx/Ui/Windows/AboutWindow.cs @@ -31,7 +31,7 @@ namespace Ryujinx.Ui.Windows { string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/"); - _patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize(patreonJsonString)); + _patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize(patreonJsonString, CommonJsonContext.Default.StringArray)); } catch { diff --git a/Ryujinx/Ui/Windows/AmiiboWindow.cs b/Ryujinx/Ui/Windows/AmiiboWindow.cs index 9140a14e..47003237 100644 --- a/Ryujinx/Ui/Windows/AmiiboWindow.cs +++ b/Ryujinx/Ui/Windows/AmiiboWindow.cs @@ -3,6 +3,7 @@ using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Utilities; using Ryujinx.Ui.Common.Configuration; +using Ryujinx.Ui.Common.Models.Amiibo; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; @@ -11,65 +12,15 @@ using System.Linq; using System.Net.Http; using System.Reflection; using System.Text; -using System.Text.Json.Serialization; +using System.Text.Json; using System.Threading.Tasks; +using AmiiboApi = Ryujinx.Ui.Common.Models.Amiibo.AmiiboApi; +using AmiiboJsonSerializerContext = Ryujinx.Ui.Common.Models.Amiibo.AmiiboJsonSerializerContext; namespace Ryujinx.Ui.Windows { public partial class AmiiboWindow : Window { - private struct AmiiboJson - { - [JsonPropertyName("amiibo")] - public List Amiibo { get; set; } - [JsonPropertyName("lastUpdated")] - public DateTime LastUpdated { get; set; } - } - - private struct AmiiboApi - { - [JsonPropertyName("name")] - public string Name { get; set; } - [JsonPropertyName("head")] - public string Head { get; set; } - [JsonPropertyName("tail")] - public string Tail { get; set; } - [JsonPropertyName("image")] - public string Image { get; set; } - [JsonPropertyName("amiiboSeries")] - public string AmiiboSeries { get; set; } - [JsonPropertyName("character")] - public string Character { get; set; } - [JsonPropertyName("gameSeries")] - public string GameSeries { get; set; } - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("release")] - public Dictionary Release { get; set; } - - [JsonPropertyName("gamesSwitch")] - public List GamesSwitch { get; set; } - } - - private class AmiiboApiGamesSwitch - { - [JsonPropertyName("amiiboUsage")] - public List AmiiboUsage { get; set; } - [JsonPropertyName("gameID")] - public List GameId { get; set; } - [JsonPropertyName("gameName")] - public string GameName { get; set; } - } - - private class AmiiboApiUsage - { - [JsonPropertyName("Usage")] - public string Usage { get; set; } - [JsonPropertyName("write")] - public bool Write { get; set; } - } - private const string DEFAULT_JSON = "{ \"amiibo\": [] }"; public string AmiiboId { get; private set; } @@ -96,6 +47,8 @@ namespace Ryujinx.Ui.Windows private List _amiiboList; + private static readonly AmiiboJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public AmiiboWindow() : base($"Ryujinx {Program.Version} - Amiibo") { Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); @@ -127,9 +80,9 @@ namespace Ryujinx.Ui.Windows if (File.Exists(_amiiboJsonPath)) { - amiiboJsonString = File.ReadAllText(_amiiboJsonPath); + amiiboJsonString = await File.ReadAllTextAsync(_amiiboJsonPath); - if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString).LastUpdated)) + if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).LastUpdated)) { amiiboJsonString = await DownloadAmiiboJson(); } @@ -148,7 +101,7 @@ namespace Ryujinx.Ui.Windows } } - _amiiboList = JsonHelper.Deserialize(amiiboJsonString).Amiibo; + _amiiboList = JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).Amiibo; _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); if (LastScannedAmiiboShowAll) diff --git a/Ryujinx/Ui/Windows/ControllerWindow.cs b/Ryujinx/Ui/Windows/ControllerWindow.cs index 0f0fba0b..9b4befd8 100644 --- a/Ryujinx/Ui/Windows/ControllerWindow.cs +++ b/Ryujinx/Ui/Windows/ControllerWindow.cs @@ -115,6 +115,8 @@ namespace Ryujinx.Ui.Windows private bool _mousePressed; private bool _middleMousePressed; + private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + public ControllerWindow(MainWindow mainWindow, PlayerIndex controllerId) : this(mainWindow, new Builder("Ryujinx.Ui.Windows.ControllerWindow.glade"), controllerId) { } private ControllerWindow(MainWindow mainWindow, Builder builder, PlayerIndex controllerId) : base(builder.GetRawOwnedObject("_controllerWin")) @@ -1120,10 +1122,7 @@ namespace Ryujinx.Ui.Windows try { - using (Stream stream = File.OpenRead(path)) - { - config = JsonHelper.Deserialize(stream); - } + config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig); } catch (JsonException) { } } @@ -1145,9 +1144,7 @@ namespace Ryujinx.Ui.Windows if (profileDialog.Run() == (int)ResponseType.Ok) { string path = System.IO.Path.Combine(GetProfileBasePath(), profileDialog.FileName); - string jsonString; - - jsonString = JsonHelper.Serialize(inputConfig, true); + string jsonString = JsonHelper.Serialize(inputConfig, SerializerContext.InputConfig); File.WriteAllText(path, jsonString); } diff --git a/Ryujinx/Ui/Windows/DlcWindow.cs b/Ryujinx/Ui/Windows/DlcWindow.cs index 9fccec19..b22f1593 100644 --- a/Ryujinx/Ui/Windows/DlcWindow.cs +++ b/Ryujinx/Ui/Windows/DlcWindow.cs @@ -7,15 +7,13 @@ using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; using System.IO; -using System.Text; - -using GUI = Gtk.Builder.ObjectAttribute; -using JsonHelper = Ryujinx.Common.Utilities.JsonHelper; +using GUI = Gtk.Builder.ObjectAttribute; namespace Ryujinx.Ui.Windows { @@ -26,6 +24,8 @@ namespace Ryujinx.Ui.Windows private readonly string _dlcJsonPath; private readonly List _dlcContainerList; + private static readonly DownloadableContentJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + #pragma warning disable CS0649, IDE0044 [GUI] Label _baseTitleInfoLabel; [GUI] TreeView _dlcTreeView; @@ -45,7 +45,7 @@ namespace Ryujinx.Ui.Windows try { - _dlcContainerList = JsonHelper.DeserializeFromFile>(_dlcJsonPath); + _dlcContainerList = JsonHelper.DeserializeFromFile(_dlcJsonPath, SerializerContext.ListDownloadableContentContainer); } catch { @@ -260,10 +260,7 @@ namespace Ryujinx.Ui.Windows while (_dlcTreeView.Model.IterNext(ref parentIter)); } - using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough)) - { - dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true))); - } + JsonHelper.SerializeToFile(_dlcJsonPath, _dlcContainerList, SerializerContext.ListDownloadableContentContainer); Dispose(); } diff --git a/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/Ryujinx/Ui/Windows/TitleUpdateWindow.cs index fce751da..c40adc11 100644 --- a/Ryujinx/Ui/Windows/TitleUpdateWindow.cs +++ b/Ryujinx/Ui/Windows/TitleUpdateWindow.cs @@ -7,6 +7,7 @@ using LibHac.Ns; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.Ui.App.Common; @@ -15,10 +16,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; - -using GUI = Gtk.Builder.ObjectAttribute; -using JsonHelper = Ryujinx.Common.Utilities.JsonHelper; +using GUI = Gtk.Builder.ObjectAttribute; +using SpanHelpers = LibHac.Common.SpanHelpers; namespace Ryujinx.Ui.Windows { @@ -32,6 +31,7 @@ namespace Ryujinx.Ui.Windows private TitleUpdateMetadata _titleUpdateWindowData; private readonly Dictionary _radioButtonToPathDictionary; + private static readonly TitleUpdateMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); #pragma warning disable CS0649, IDE0044 [GUI] Label _baseTitleInfoLabel; @@ -54,7 +54,7 @@ namespace Ryujinx.Ui.Windows try { - _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath); + _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath, SerializerContext.TitleUpdateMetadata); } catch { @@ -193,10 +193,7 @@ namespace Ryujinx.Ui.Windows } } - using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough)) - { - dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true))); - } + JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, SerializerContext.TitleUpdateMetadata); _parent.UpdateGameTable();