Use source generated json serializers in order to improve code trimming (#4094)

* 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

---------

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
This commit is contained in:
Andrey Sukharev 2023-03-22 01:41:19 +03:00 committed by GitHub
parent 17620d18db
commit 4ce4299ca2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 887 additions and 609 deletions

View File

@ -63,6 +63,10 @@ dotnet_code_quality_unused_parameters = all:suggestion
#### C# Coding Conventions #### #### C# Coding Conventions ####
# Namespace preferences
csharp_style_namespace_declarations = block_scoped:warning
resharper_csharp_namespace_body = block_scoped
# var preferences # var preferences
csharp_style_var_elsewhere = false:silent csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent csharp_style_var_for_built_in_types = false:silent

View File

@ -1,6 +1,7 @@
namespace ARMeilleure.Decoders; namespace ARMeilleure.Decoders
interface IOpCode32Exception
{ {
int Id { get; } interface IOpCode32Exception
{
int Id { get; }
}
} }

View File

@ -130,7 +130,7 @@ namespace Ryujinx.Ava.Common.Locale
{ {
var localeStrings = new Dictionary<LocaleKeys, string>(); var localeStrings = new Dictionary<LocaleKeys, string>();
string languageJson = EmbeddedResources.ReadAllText($"Ryujinx.Ava/Assets/Locales/{languageCode}.json"); string languageJson = EmbeddedResources.ReadAllText($"Ryujinx.Ava/Assets/Locales/{languageCode}.json");
var strings = JsonHelper.Deserialize<Dictionary<string, string>>(languageJson); var strings = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary);
foreach (var item in strings) foreach (var item in strings)
{ {

View File

@ -4,13 +4,14 @@ using FluentAvalonia.UI.Controls;
using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip; using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json.Linq;
using Ryujinx.Ava; using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using Ryujinx.Ui.Common.Models.Github;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -31,6 +32,7 @@ namespace Ryujinx.Modules
internal static class Updater internal static class Updater
{ {
private const string GitHubApiURL = "https://api.github.com"; 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 HomeDir = AppDomain.CurrentDomain.BaseDirectory;
private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update"); 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 buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL); string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL);
JObject jsonRoot = JObject.Parse(fetchedJson); var fetched = JsonHelper.Deserialize(fetchedJson, SerializerContext.GithubReleasesJsonResponse);
JToken assets = jsonRoot["assets"]; _buildVer = fetched.Name;
_buildVer = (string)jsonRoot["name"]; foreach (var asset in fetched.Assets)
foreach (JToken asset in assets)
{ {
string assetName = (string)asset["name"]; if (asset.Name.StartsWith("test-ava-ryujinx") && asset.Name.EndsWith(_platformExt))
string assetState = (string)asset["state"];
string downloadURL = (string)asset["browser_download_url"];
if (assetName.StartsWith("test-ava-ryujinx") && assetName.EndsWith(_platformExt))
{ {
_buildUrl = downloadURL; _buildUrl = asset.BrowserDownloadUrl;
if (assetState != "uploaded") if (asset.State != "uploaded")
{ {
if (showVersionUpToDate) if (showVersionUpToDate)
{ {

View File

@ -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<AmiiboApi> 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<string, string> Release { get; set; }
[JsonPropertyName("gamesSwitch")] public List<AmiiboApiGamesSwitch> 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<AmiiboApiUsage> AmiiboUsage { get; set; }
[JsonPropertyName("gameID")] public List<string> 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; }
}
}
}

View File

@ -122,7 +122,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/"); string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
Supporters = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString)) + "\n\n"; Supporters = string.Join(", ", JsonHelper.Deserialize(patreonJsonString, CommonJsonContext.Default.StringArray) + "\n\n");
} }
catch catch
{ {

View File

@ -4,11 +4,11 @@ using Avalonia.Media.Imaging;
using Avalonia.Threading; using Avalonia.Threading;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Ui.Common.Models.Amiibo;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@ -17,6 +17,7 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using AmiiboJsonSerializerContext = Ryujinx.Ui.Common.Models.Amiibo.AmiiboJsonSerializerContext;
namespace Ryujinx.Ava.UI.ViewModels namespace Ryujinx.Ava.UI.ViewModels
{ {
@ -31,8 +32,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private readonly StyleableWindow _owner; private readonly StyleableWindow _owner;
private Bitmap _amiiboImage; private Bitmap _amiiboImage;
private List<Amiibo.AmiiboApi> _amiiboList; private List<AmiiboApi> _amiiboList;
private AvaloniaList<Amiibo.AmiiboApi> _amiibos; private AvaloniaList<AmiiboApi> _amiibos;
private ObservableCollection<string> _amiiboSeries; private ObservableCollection<string> _amiiboSeries;
private int _amiiboSelectedIndex; private int _amiiboSelectedIndex;
@ -42,6 +43,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private bool _useRandomUuid; private bool _useRandomUuid;
private string _usage; private string _usage;
private static readonly AmiiboJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public AmiiboWindowViewModel(StyleableWindow owner, string lastScannedAmiiboId, string titleId) public AmiiboWindowViewModel(StyleableWindow owner, string lastScannedAmiiboId, string titleId)
{ {
_owner = owner; _owner = owner;
@ -52,9 +55,9 @@ namespace Ryujinx.Ava.UI.ViewModels
Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo")); Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
_amiiboJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json"); _amiiboJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json");
_amiiboList = new List<Amiibo.AmiiboApi>(); _amiiboList = new List<AmiiboApi>();
_amiiboSeries = new ObservableCollection<string>(); _amiiboSeries = new ObservableCollection<string>();
_amiibos = new AvaloniaList<Amiibo.AmiiboApi>(); _amiibos = new AvaloniaList<AmiiboApi>();
_amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png"); _amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png");
@ -94,7 +97,7 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
} }
public AvaloniaList<Amiibo.AmiiboApi> AmiiboList public AvaloniaList<AmiiboApi> AmiiboList
{ {
get => _amiibos; get => _amiibos;
set set
@ -187,9 +190,9 @@ namespace Ryujinx.Ava.UI.ViewModels
if (File.Exists(_amiiboJsonPath)) if (File.Exists(_amiiboJsonPath))
{ {
amiiboJsonString = File.ReadAllText(_amiiboJsonPath); amiiboJsonString = await File.ReadAllTextAsync(_amiiboJsonPath);
if (await NeedsUpdate(JsonHelper.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).LastUpdated)) if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).LastUpdated))
{ {
amiiboJsonString = await DownloadAmiiboJson(); amiiboJsonString = await DownloadAmiiboJson();
} }
@ -206,7 +209,7 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
} }
_amiiboList = JsonHelper.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).Amiibo; _amiiboList = JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).Amiibo;
_amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
ParseAmiiboData(); ParseAmiiboData();
@ -223,7 +226,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
if (!ShowAllAmiibo) if (!ShowAllAmiibo)
{ {
foreach (Amiibo.AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch) foreach (AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
{ {
if (game != null) if (game != null)
{ {
@ -255,7 +258,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void SelectLastScannedAmiibo() 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); SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries);
AmiiboSelectedIndex = AmiiboList.IndexOf(scanned); AmiiboSelectedIndex = AmiiboList.IndexOf(scanned);
@ -270,7 +273,7 @@ namespace Ryujinx.Ava.UI.ViewModels
return; return;
} }
List<Amiibo.AmiiboApi> amiiboSortedList = _amiiboList List<AmiiboApi> amiiboSortedList = _amiiboList
.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex]) .Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex])
.OrderBy(amiibo => amiibo.Name).ToList(); .OrderBy(amiibo => amiibo.Name).ToList();
@ -280,7 +283,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
if (!_showAllAmiibo) if (!_showAllAmiibo)
{ {
foreach (Amiibo.AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch) foreach (AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
{ {
if (game != null) if (game != null)
{ {
@ -314,7 +317,7 @@ namespace Ryujinx.Ava.UI.ViewModels
return; return;
} }
Amiibo.AmiiboApi selected = _amiibos[_amiiboSelectedIndex]; AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image; string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image;
@ -326,11 +329,11 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
bool writable = false; bool writable = false;
foreach (Amiibo.AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch) foreach (AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
{ {
if (item.GameId.Contains(TitleId)) if (item.GameId.Contains(TitleId))
{ {
foreach (Amiibo.AmiiboApiUsage usageItem in item.AmiiboUsage) foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
{ {
usageString += Environment.NewLine + usageString += Environment.NewLine +
$"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}"; $"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";

View File

@ -51,6 +51,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private bool _isLoaded; private bool _isLoaded;
private readonly UserControl _owner; private readonly UserControl _owner;
private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public IGamepadDriver AvaloniaKeyboardDriver { get; } public IGamepadDriver AvaloniaKeyboardDriver { get; }
public IGamepad SelectedGamepad { get; private set; } public IGamepad SelectedGamepad { get; private set; }
@ -706,10 +708,7 @@ namespace Ryujinx.Ava.UI.ViewModels
try try
{ {
using (Stream stream = File.OpenRead(path)) config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig);
{
config = JsonHelper.Deserialize<InputConfig>(stream);
}
} }
catch (JsonException) { } catch (JsonException) { }
catch (InvalidOperationException) catch (InvalidOperationException)
@ -775,7 +774,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.ControllerType = Controllers[_controller].Type; config.ControllerType = Controllers[_controller].Type;
string jsonString = JsonHelper.Serialize(config, true); string jsonString = JsonHelper.Serialize(config, SerializerContext.InputConfig);
await File.WriteAllTextAsync(path, jsonString); await File.WriteAllTextAsync(path, jsonString);

View File

@ -21,7 +21,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Path = System.IO.Path; using Path = System.IO.Path;
@ -41,6 +40,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private ulong _titleId; private ulong _titleId;
private string _titleName; private string _titleName;
private static readonly DownloadableContentJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public AvaloniaList<DownloadableContentModel> DownloadableContents public AvaloniaList<DownloadableContentModel> DownloadableContents
{ {
get => _downloadableContents; get => _downloadableContents;
@ -100,7 +101,7 @@ namespace Ryujinx.Ava.UI.ViewModels
try try
{ {
_downloadableContentContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_downloadableContentJsonPath); _downloadableContentContainerList = JsonHelper.DeserializeFromFile(_downloadableContentJsonPath, SerializerContext.ListDownloadableContentContainer);
} }
catch catch
{ {
@ -330,10 +331,7 @@ namespace Ryujinx.Ava.UI.ViewModels
_downloadableContentContainerList.Add(container); _downloadableContentContainerList.Add(container);
} }
using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough)) JsonHelper.SerializeToFile(_downloadableContentJsonPath, _downloadableContentContainerList, SerializerContext.ListDownloadableContentContainer);
{
downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true)));
}
} }
} }

View File

@ -25,226 +25,228 @@ using System.Text;
using Path = System.IO.Path; using Path = System.IO.Path;
using SpanHelpers = LibHac.Common.SpanHelpers; using SpanHelpers = LibHac.Common.SpanHelpers;
namespace Ryujinx.Ava.UI.ViewModels; namespace Ryujinx.Ava.UI.ViewModels
public class TitleUpdateViewModel : BaseModel
{ {
public TitleUpdateMetadata _titleUpdateWindowData; public class TitleUpdateViewModel : BaseModel
public readonly string _titleUpdateJsonPath;
private VirtualFileSystem _virtualFileSystem { get; }
private ulong _titleId { get; }
private string _titleName { get; }
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
private AvaloniaList<object> _views = new();
private object _selectedUpdate;
public AvaloniaList<TitleUpdateModel> TitleUpdates
{ {
get => _titleUpdates; public TitleUpdateMetadata _titleUpdateWindowData;
set public readonly string _titleUpdateJsonPath;
private VirtualFileSystem _virtualFileSystem { get; }
private ulong _titleId { get; }
private string _titleName { get; }
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
private AvaloniaList<object> _views = new();
private object _selectedUpdate;
private static readonly TitleUpdateMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public AvaloniaList<TitleUpdateModel> TitleUpdates
{ {
_titleUpdates = value; get => _titleUpdates;
OnPropertyChanged(); set
}
}
public AvaloniaList<object> 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<TitleUpdateMetadata>(_titleUpdateJsonPath);
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {_titleId} at {_titleUpdateJsonPath}");
_titleUpdateWindowData = new TitleUpdateMetadata
{ {
Selected = "", _titleUpdates = value;
Paths = new List<string>() OnPropertyChanged();
};
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];
} }
} }
}
private void AddUpdate(string path) public AvaloniaList<object> Views
{
if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
{ {
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 try
{ {
(Nca patchNca, Nca controlNca) = ApplicationLoader.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<string>()
};
using UniqueRef<IFile> nacpFile = new(); Save();
}
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); LoadUpdates();
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); }
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 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) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
if (controlNca != null && patchNca != null)
{
ApplicationControlProperty controlData = new();
using UniqueRef<IFile> 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 () => 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) public void RemoveUpdate(TitleUpdateModel update)
{
TitleUpdates.Remove(update);
SortUpdates();
}
public async void Add()
{
OpenFileDialog dialog = new()
{ {
Title = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle], TitleUpdates.Remove(update);
AllowMultiple = true
};
dialog.Filters.Add(new FileDialogFilter SortUpdates();
}
public async void Add()
{ {
Name = "NSP", OpenFileDialog dialog = new()
Extensions = { "nsp" }
});
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
string[] files = await dialog.ShowAsync(desktop.MainWindow);
if (files != null)
{ {
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()
}
public void Save()
{
_titleUpdateWindowData.Paths.Clear();
_titleUpdateWindowData.Selected = "";
foreach (TitleUpdateModel update in TitleUpdates)
{ {
_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);
}
} }
} }

View File

@ -42,7 +42,7 @@ namespace Ryujinx.Ava.UI.Views.Main
{ {
string languageCode = Path.GetFileNameWithoutExtension(locale).Split('.').Last(); string languageCode = Path.GetFileNameWithoutExtension(locale).Split('.').Last();
string languageJson = EmbeddedResources.ReadAllText($"{localePath}/{languageCode}{localeExt}"); string languageJson = EmbeddedResources.ReadAllText($"{localePath}/{languageCode}{localeExt}");
var strings = JsonHelper.Deserialize<Dictionary<string, string>>(languageJson); var strings = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary);
if (!strings.TryGetValue("Language", out string languageName)) if (!strings.TryGetValue("Language", out string languageName))
{ {

View File

@ -1,7 +1,7 @@
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ui.Common.Models.Amiibo;
namespace Ryujinx.Ava.UI.Windows namespace Ryujinx.Ava.UI.Windows
{ {
@ -35,14 +35,14 @@ namespace Ryujinx.Ava.UI.Windows
} }
public bool IsScanned { get; set; } public bool IsScanned { get; set; }
public Amiibo.AmiiboApi ScannedAmiibo { get; set; } public AmiiboApi ScannedAmiibo { get; set; }
public AmiiboWindowViewModel ViewModel { get; set; } public AmiiboWindowViewModel ViewModel { get; set; }
private void ScanButton_Click(object sender, RoutedEventArgs e) private void ScanButton_Click(object sender, RoutedEventArgs e)
{ {
if (ViewModel.AmiiboSelectedIndex > -1) if (ViewModel.AmiiboSelectedIndex > -1)
{ {
Amiibo.AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex]; AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
ScannedAmiibo = amiibo; ScannedAmiibo = amiibo;
IsScanned = true; IsScanned = true;
Close(); Close();

View File

@ -6,11 +6,8 @@ using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using System.IO;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Button = Avalonia.Controls.Button; using Button = Avalonia.Controls.Button;

View File

@ -1,5 +1,9 @@
namespace Ryujinx.Common.Configuration using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<AspectRatio>))]
public enum AspectRatio public enum AspectRatio
{ {
Fixed4x3, Fixed4x3,

View File

@ -1,5 +1,9 @@
namespace Ryujinx.Common.Configuration using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<BackendThreading>))]
public enum BackendThreading public enum BackendThreading
{ {
Auto, Auto,

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(List<DownloadableContentContainer>))]
public partial class DownloadableContentJsonSerializerContext : JsonSerializerContext
{
}
}

View File

@ -1,5 +1,9 @@
namespace Ryujinx.Common.Configuration using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<GraphicsBackend>))]
public enum GraphicsBackend public enum GraphicsBackend
{ {
Vulkan, Vulkan,

View File

@ -1,5 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<GraphicsDebugLevel>))]
public enum GraphicsDebugLevel public enum GraphicsDebugLevel
{ {
None, None,

View File

@ -1,4 +1,5 @@
using System; using Ryujinx.Common.Utilities;
using System;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -6,6 +7,8 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
{ {
class JsonMotionConfigControllerConverter : JsonConverter<MotionConfigController> class JsonMotionConfigControllerConverter : JsonConverter<MotionConfigController>
{ {
private static readonly MotionConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static MotionInputBackendType GetMotionInputBackendType(ref Utf8JsonReader reader) private static MotionInputBackendType GetMotionInputBackendType(ref Utf8JsonReader reader)
{ {
// Temporary reader to get the backend type // Temporary reader to get the backend type
@ -52,8 +55,8 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
return motionBackendType switch return motionBackendType switch
{ {
MotionInputBackendType.GamepadDriver => (MotionConfigController)JsonSerializer.Deserialize(ref reader, typeof(StandardMotionConfigController), options), MotionInputBackendType.GamepadDriver => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardMotionConfigController),
MotionInputBackendType.CemuHook => (MotionConfigController)JsonSerializer.Deserialize(ref reader, typeof(CemuHookMotionConfigController), options), MotionInputBackendType.CemuHook => JsonSerializer.Deserialize(ref reader, SerializerContext.CemuHookMotionConfigController),
_ => throw new InvalidOperationException($"Unknown backend type {motionBackendType}"), _ => throw new InvalidOperationException($"Unknown backend type {motionBackendType}"),
}; };
} }
@ -63,10 +66,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
switch (value.MotionBackend) switch (value.MotionBackend)
{ {
case MotionInputBackendType.GamepadDriver: case MotionInputBackendType.GamepadDriver:
JsonSerializer.Serialize(writer, value as StandardMotionConfigController, options); JsonSerializer.Serialize(writer, value as StandardMotionConfigController, SerializerContext.StandardMotionConfigController);
break; break;
case MotionInputBackendType.CemuHook: case MotionInputBackendType.CemuHook:
JsonSerializer.Serialize(writer, value as CemuHookMotionConfigController, options); JsonSerializer.Serialize(writer, value as CemuHookMotionConfigController, SerializerContext.CemuHookMotionConfigController);
break; break;
default: default:
throw new ArgumentException($"Unknown motion backend type {value.MotionBackend}"); throw new ArgumentException($"Unknown motion backend type {value.MotionBackend}");

View File

@ -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 class MotionConfigController
{ {
public MotionInputBackendType MotionBackend { get; set; } public MotionInputBackendType MotionBackend { get; set; }

View File

@ -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
{
}
}

View File

@ -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<MotionInputBackendType>))]
public enum MotionInputBackendType : byte public enum MotionInputBackendType : byte
{ {
Invalid, Invalid,

View File

@ -1,9 +1,12 @@
using Ryujinx.Common.Utilities;
using System; using System;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid namespace Ryujinx.Common.Configuration.Hid
{ {
[Flags]
// This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
[Flags]
[JsonConverter(typeof(TypedStringEnumConverter<ControllerType>))]
public enum ControllerType : int public enum ControllerType : int
{ {
None, None,

View File

@ -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<InputBackendType>))]
public enum InputBackendType public enum InputBackendType
{ {
Invalid, Invalid,

View File

@ -1,8 +1,10 @@
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid namespace Ryujinx.Common.Configuration.Hid
{ {
[JsonConverter(typeof(JsonInputConfigConverter))]
public class InputConfig : INotifyPropertyChanged public class InputConfig : INotifyPropertyChanged
{ {
/// <summary> /// <summary>

View File

@ -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
{
}
}

View File

@ -1,13 +1,16 @@
using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Utilities;
using System; using System;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid namespace Ryujinx.Common.Configuration.Hid
{ {
class JsonInputConfigConverter : JsonConverter<InputConfig> public class JsonInputConfigConverter : JsonConverter<InputConfig>
{ {
private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static InputBackendType GetInputBackendType(ref Utf8JsonReader reader) private static InputBackendType GetInputBackendType(ref Utf8JsonReader reader)
{ {
// Temporary reader to get the backend type // Temporary reader to get the backend type
@ -54,8 +57,8 @@ namespace Ryujinx.Common.Configuration.Hid
return backendType switch return backendType switch
{ {
InputBackendType.WindowKeyboard => (InputConfig)JsonSerializer.Deserialize(ref reader, typeof(StandardKeyboardInputConfig), options), InputBackendType.WindowKeyboard => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardKeyboardInputConfig),
InputBackendType.GamepadSDL2 => (InputConfig)JsonSerializer.Deserialize(ref reader, typeof(StandardControllerInputConfig), options), InputBackendType.GamepadSDL2 => JsonSerializer.Deserialize(ref reader, SerializerContext.StandardControllerInputConfig),
_ => throw new InvalidOperationException($"Unknown backend type {backendType}"), _ => throw new InvalidOperationException($"Unknown backend type {backendType}"),
}; };
} }
@ -65,10 +68,10 @@ namespace Ryujinx.Common.Configuration.Hid
switch (value.Backend) switch (value.Backend)
{ {
case InputBackendType.WindowKeyboard: case InputBackendType.WindowKeyboard:
JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, options); JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, SerializerContext.StandardKeyboardInputConfig);
break; break;
case InputBackendType.GamepadSDL2: case InputBackendType.GamepadSDL2:
JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, options); JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, SerializerContext.StandardControllerInputConfig);
break; break;
default: default:
throw new ArgumentException($"Unknown backend type {value.Backend}"); throw new ArgumentException($"Unknown backend type {value.Backend}");

View File

@ -1,6 +1,10 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid namespace Ryujinx.Common.Configuration.Hid
{ {
// This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical // This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
[JsonConverter(typeof(TypedStringEnumConverter<PlayerIndex>))]
public enum PlayerIndex : int public enum PlayerIndex : int
{ {
Player1 = 0, Player1 = 0,

View File

@ -1,5 +1,9 @@
namespace Ryujinx.Common.Configuration using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{ {
[JsonConverter(typeof(TypedStringEnumConverter<MemoryManagerMode>))]
public enum MemoryManagerMode : byte public enum MemoryManagerMode : byte
{ {
SoftwarePageTable, SoftwarePageTable,

View File

@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(TitleUpdateMetadata))]
public partial class TitleUpdateMetadataJsonSerializerContext : JsonSerializerContext
{
}
}

View File

@ -1,22 +1,20 @@
using System; using System.Text;
using System.Reflection;
using System.Text;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging
{ {
internal class DefaultLogFormatter : ILogFormatter internal class DefaultLogFormatter : ILogFormatter
{ {
private static readonly ObjectPool<StringBuilder> _stringBuilderPool = SharedPools.Default<StringBuilder>(); private static readonly ObjectPool<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
public string Format(LogEventArgs args) public string Format(LogEventArgs args)
{ {
StringBuilder sb = _stringBuilderPool.Allocate(); StringBuilder sb = StringBuilderPool.Allocate();
try try
{ {
sb.Clear(); 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]}| "); sb.Append($" |{args.Level.ToString()[0]}| ");
if (args.ThreadName != null) if (args.ThreadName != null)
@ -27,53 +25,17 @@ namespace Ryujinx.Common.Logging
sb.Append(args.Message); sb.Append(args.Message);
if (args.Data != null) if (args.Data is not null)
{ {
PropertyInfo[] props = args.Data.GetType().GetProperties(); sb.Append(' ');
DynamicObjectFormatter.Format(sb, args.Data);
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('}');
} }
return sb.ToString(); return sb.ToString();
} }
finally finally
{ {
_stringBuilderPool.Release(sb); StringBuilderPool.Release(sb);
} }
} }
} }

View File

@ -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<StringBuilder> StringBuilderPool = SharedPools.Default<StringBuilder>();
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('}');
}
}
}

View File

@ -1,5 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging
{ {
[JsonConverter(typeof(TypedStringEnumConverter<LogClass>))]
public enum LogClass public enum LogClass
{ {
Application, Application,

View File

@ -11,15 +11,7 @@ namespace Ryujinx.Common.Logging
public readonly string Message; public readonly string Message;
public readonly object Data; public readonly object Data;
public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message) public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data = null)
{
Level = level;
Time = time;
ThreadName = threadName;
Message = message;
}
public LogEventArgs(LogLevel level, TimeSpan time, string threadName, string message, object data)
{ {
Level = level; Level = level;
Time = time; Time = time;

View File

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

View File

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Logging
{
[JsonSerializable(typeof(LogEventArgsJson))]
internal partial class LogEventJsonSerializerContext : JsonSerializerContext
{
}
}

View File

@ -1,5 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging
{ {
[JsonConverter(typeof(TypedStringEnumConverter<LogLevel>))]
public enum LogLevel public enum LogLevel
{ {
Debug, Debug,

View File

@ -1,5 +1,5 @@
using System.IO; using Ryujinx.Common.Utilities;
using System.Text.Json; using System.IO;
namespace Ryujinx.Common.Logging namespace Ryujinx.Common.Logging
{ {
@ -25,12 +25,8 @@ namespace Ryujinx.Common.Logging
public void Log(object sender, LogEventArgs e) public void Log(object sender, LogEventArgs e)
{ {
string text = JsonSerializer.Serialize(e); var logEventArgsJson = LogEventArgsJson.FromLogEventArgs(e);
JsonHelper.SerializeToStream(_stream, logEventArgsJson, LogEventJsonSerializerContext.Default.LogEventArgsJson);
using (BinaryWriter writer = new BinaryWriter(_stream))
{
writer.Write(text);
}
} }
public void Dispose() public void Dispose()

View File

@ -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<string, string>), TypeInfoPropertyName = "StringDictionary")]
public partial class CommonJsonContext : JsonSerializerContext
{
}
}

View File

@ -1,15 +1,62 @@
using Ryujinx.Common.Configuration.Hid; using System.IO;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using System.IO;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata;
namespace Ryujinx.Common.Utilities namespace Ryujinx.Common.Utilities
{ {
public class JsonHelper public class JsonHelper
{ {
public static JsonNamingPolicy SnakeCase { get; } private static readonly JsonNamingPolicy SnakeCasePolicy = new SnakeCaseNamingPolicy();
private const int DefaultFileWriteBufferSize = 4096;
/// <summary>
/// Creates new serializer options with default settings.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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>(T value, JsonTypeInfo<T> typeInfo)
{
return JsonSerializer.Serialize(value, typeInfo);
}
public static T Deserialize<T>(string value, JsonTypeInfo<T> typeInfo)
{
return JsonSerializer.Deserialize(value, typeInfo);
}
public static void SerializeToFile<T>(string filePath, T value, JsonTypeInfo<T> typeInfo)
{
using FileStream file = File.Create(filePath, DefaultFileWriteBufferSize, FileOptions.WriteThrough);
JsonSerializer.Serialize(file, value, typeInfo);
}
public static T DeserializeFromFile<T>(string filePath, JsonTypeInfo<T> typeInfo)
{
using FileStream file = File.OpenRead(filePath);
return JsonSerializer.Deserialize(file, typeInfo);
}
public static void SerializeToStream<T>(Stream stream, T value, JsonTypeInfo<T> typeInfo)
{
JsonSerializer.Serialize(stream, value, typeInfo);
}
private class SnakeCaseNamingPolicy : JsonNamingPolicy private class SnakeCaseNamingPolicy : JsonNamingPolicy
{ {
@ -20,7 +67,7 @@ namespace Ryujinx.Common.Utilities
return name; return name;
} }
StringBuilder builder = new StringBuilder(); StringBuilder builder = new();
for (int i = 0; i < name.Length; i++) for (int i = 0; i < name.Length; i++)
{ {
@ -34,7 +81,7 @@ namespace Ryujinx.Common.Utilities
} }
else else
{ {
builder.Append("_"); builder.Append('_');
builder.Append(char.ToLowerInvariant(c)); builder.Append(char.ToLowerInvariant(c));
} }
} }
@ -47,64 +94,5 @@ namespace Ryujinx.Common.Utilities
return builder.ToString(); 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<T>(Stream stream)
{
using (BinaryReader reader = new BinaryReader(stream))
{
return JsonSerializer.Deserialize<T>(reader.ReadBytes((int)(stream.Length - stream.Position)), GetDefaultSerializerOptions());
}
}
public static T DeserializeFromFile<T>(string path)
{
return Deserialize<T>(File.ReadAllText(path));
}
public static T Deserialize<T>(string json)
{
return JsonSerializer.Deserialize<T>(json, GetDefaultSerializerOptions());
}
public static void Serialize<TValue>(Stream stream, TValue obj, bool prettyPrint = false)
{
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.Write(SerializeToUtf8Bytes(obj, prettyPrint));
}
}
public static string Serialize<TValue>(TValue obj, bool prettyPrint = false)
{
return JsonSerializer.Serialize(obj, GetDefaultSerializerOptions(prettyPrint));
}
public static byte[] SerializeToUtf8Bytes<T>(T obj, bool prettyPrint = false)
{
return JsonSerializer.SerializeToUtf8Bytes(obj, GetDefaultSerializerOptions(prettyPrint));
}
} }
} }

View File

@ -0,0 +1,34 @@
#nullable enable
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Utilities
{
/// <summary>
/// Specifies that value of <see cref="TEnum"/> will be serialized as string in JSONs
/// </summary>
/// <remarks>
/// Trimming friendly alternative to <see cref="JsonStringEnumConverter"/>.
/// Get rid of this converter if dotnet supports similar functionality out of the box.
/// </remarks>
/// <typeparam name="TEnum">Type of enum to serialize</typeparam>
public sealed class TypedStringEnumConverter<TEnum> : JsonConverter<TEnum> 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<TEnum>(enumValue);
}
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
}

View File

@ -13,6 +13,7 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Executables;
@ -24,14 +25,13 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json;
using static Ryujinx.HLE.HOS.ModLoader; using static Ryujinx.HLE.HOS.ModLoader;
using ApplicationId = LibHac.Ncm.ApplicationId; using ApplicationId = LibHac.Ncm.ApplicationId;
using Path = System.IO.Path; using Path = System.IO.Path;
namespace Ryujinx.HLE.HOS namespace Ryujinx.HLE.HOS
{ {
using JsonHelper = Common.Utilities.JsonHelper;
public class ApplicationLoader public class ApplicationLoader
{ {
// Binaries from exefs are loaded into mem in this order. Do not change. // Binaries from exefs are loaded into mem in this order. Do not change.
@ -57,6 +57,10 @@ namespace Ryujinx.HLE.HOS
private string _displayVersion; private string _displayVersion;
private BlitStruct<ApplicationControlProperty> _controlData; private BlitStruct<ApplicationControlProperty> _controlData;
private static readonly JsonSerializerOptions SerializerOptions = JsonHelper.GetDefaultSerializerOptions();
private static readonly DownloadableContentJsonSerializerContext ContentSerializerContext = new(SerializerOptions);
private static readonly TitleUpdateMetadataJsonSerializerContext TitleSerializerContext = new(SerializerOptions);
public BlitStruct<ApplicationControlProperty> ControlData => _controlData; public BlitStruct<ApplicationControlProperty> ControlData => _controlData;
public string TitleName => _titleName; public string TitleName => _titleName;
public string DisplayVersion => _displayVersion; public string DisplayVersion => _displayVersion;
@ -197,7 +201,7 @@ namespace Ryujinx.HLE.HOS
if (File.Exists(titleUpdateMetadataPath)) if (File.Exists(titleUpdateMetadataPath))
{ {
updatePath = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(titleUpdateMetadataPath).Selected; updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, TitleSerializerContext.TitleUpdateMetadata).Selected;
if (File.Exists(updatePath)) if (File.Exists(updatePath))
{ {
@ -411,7 +415,7 @@ namespace Ryujinx.HLE.HOS
if (File.Exists(titleAocMetadataPath)) if (File.Exists(titleAocMetadataPath))
{ {
List<DownloadableContentContainer> dlcContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(titleAocMetadataPath); List<DownloadableContentContainer> dlcContainerList = JsonHelper.DeserializeFromFile(titleAocMetadataPath, ContentSerializerContext.ListDownloadableContentContainer);
foreach (DownloadableContentContainer downloadableContentContainer in dlcContainerList) foreach (DownloadableContentContainer downloadableContentContainer in dlcContainerList)
{ {

View File

@ -1,11 +1,11 @@
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Account.Acc.Types;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json.Serialization;
namespace Ryujinx.HLE.HOS.Services.Account.Acc 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 readonly string _profilesJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "Profiles.json");
private struct ProfilesJson private static readonly ProfilesJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
{
[JsonPropertyName("profiles")]
public List<UserProfileJson> 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; }
}
public UserId LastOpened { get; set; } public UserId LastOpened { get; set; }
@ -47,7 +25,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
{ {
try try
{ {
ProfilesJson profilesJson = JsonHelper.DeserializeFromFile<ProfilesJson>(_profilesJsonPath); ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath, SerializerContext.ProfilesJson);
foreach (var profile in profilesJson.Profiles) 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);
} }
} }
} }

View File

@ -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
{
}
}

View File

@ -1,5 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.HLE.HOS.Services.Account.Acc namespace Ryujinx.HLE.HOS.Services.Account.Acc
{ {
[JsonConverter(typeof(TypedStringEnumConverter<AccountState>))]
public enum AccountState public enum AccountState
{ {
Closed, Closed,

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Services.Account.Acc.Types
{
internal struct ProfilesJson
{
public List<UserProfileJson> Profiles { get; set; }
public string LastOpened { get; set; }
}
}

View File

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

View File

@ -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
{
}
}

View File

@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.Services.Mii; using Ryujinx.HLE.HOS.Services.Mii;
using Ryujinx.HLE.HOS.Services.Mii.Types; using Ryujinx.HLE.HOS.Services.Mii.Types;
@ -8,8 +9,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Text.Json;
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{ {
@ -17,6 +16,8 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{ {
private static uint _openedApplicationAreaId; private static uint _openedApplicationAreaId;
private static readonly AmiiboJsonSerializerContext SerializerContext = AmiiboJsonSerializerContext.Default;
public static byte[] GenerateUuid(string amiiboId, bool useRandomUuid) public static byte[] GenerateUuid(string amiiboId, bool useRandomUuid)
{ {
if (useRandomUuid) if (useRandomUuid)
@ -173,7 +174,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
if (File.Exists(filePath)) if (File.Exists(filePath))
{ {
virtualAmiiboFile = JsonSerializer.Deserialize<VirtualAmiiboFile>(File.ReadAllText(filePath), new JsonSerializerOptions(JsonSerializerDefaults.General)); virtualAmiiboFile = JsonHelper.DeserializeFromFile(filePath, SerializerContext.VirtualAmiiboFile);
} }
else else
{ {
@ -197,8 +198,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
private static void SaveAmiiboFile(VirtualAmiiboFile virtualAmiiboFile) private static void SaveAmiiboFile(VirtualAmiiboFile virtualAmiiboFile)
{ {
string filePath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json"); string filePath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json");
JsonHelper.SerializeToFile(filePath, virtualAmiiboFile, SerializerContext.VirtualAmiiboFile);
File.WriteAllText(filePath, JsonSerializer.Serialize(virtualAmiiboFile));
} }
} }
} }

View File

@ -56,6 +56,8 @@ namespace Ryujinx.Headless.SDL2
private static bool _enableKeyboard; private static bool _enableKeyboard;
private static bool _enableMouse; private static bool _enableMouse;
private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
static void Main(string[] args) static void Main(string[] args)
{ {
Version = ReleaseInformation.GetVersion(); Version = ReleaseInformation.GetVersion();
@ -285,10 +287,7 @@ namespace Ryujinx.Headless.SDL2
try try
{ {
using (Stream stream = File.OpenRead(path)) config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig);
{
config = JsonHelper.Deserialize<InputConfig>(stream);
}
} }
catch (JsonException) catch (JsonException)
{ {

View File

@ -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
{
}
}

View File

@ -10,6 +10,7 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
@ -22,7 +23,6 @@ using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using JsonHelper = Ryujinx.Common.Utilities.JsonHelper;
using Path = System.IO.Path; using Path = System.IO.Path;
namespace Ryujinx.Ui.App.Common namespace Ryujinx.Ui.App.Common
@ -42,6 +42,8 @@ namespace Ryujinx.Ui.App.Common
private Language _desiredTitleLanguage; private Language _desiredTitleLanguage;
private CancellationTokenSource _cancellationToken; private CancellationTokenSource _cancellationToken;
private static readonly ApplicationJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public ApplicationLibrary(VirtualFileSystem virtualFileSystem) public ApplicationLibrary(VirtualFileSystem virtualFileSystem)
{ {
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
@ -490,14 +492,12 @@ namespace Ryujinx.Ui.App.Common
appMetadata = new ApplicationMetadata(); appMetadata = new ApplicationMetadata();
using FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough); JsonHelper.SerializeToFile(metadataFile, appMetadata, SerializerContext.ApplicationMetadata);
JsonHelper.Serialize(stream, appMetadata, true);
} }
try try
{ {
appMetadata = JsonHelper.DeserializeFromFile<ApplicationMetadata>(metadataFile); appMetadata = JsonHelper.DeserializeFromFile(metadataFile, SerializerContext.ApplicationMetadata);
} }
catch (JsonException) catch (JsonException)
{ {
@ -510,9 +510,7 @@ namespace Ryujinx.Ui.App.Common
{ {
modifyFunction(appMetadata); modifyFunction(appMetadata);
using FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough); JsonHelper.SerializeToFile(metadataFile, appMetadata, SerializerContext.ApplicationMetadata);
JsonHelper.Serialize(stream, appMetadata, true);
} }
return appMetadata; return appMetadata;

View File

@ -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<AudioBackend>))]
public enum AudioBackend public enum AudioBackend
{ {
Dummy, Dummy,

View File

@ -5,7 +5,7 @@ using Ryujinx.Common.Utilities;
using Ryujinx.Ui.Common.Configuration.System; using Ryujinx.Ui.Common.Configuration.System;
using Ryujinx.Ui.Common.Configuration.Ui; using Ryujinx.Ui.Common.Configuration.Ui;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.Text.Json.Nodes;
namespace Ryujinx.Ui.Common.Configuration namespace Ryujinx.Ui.Common.Configuration
{ {
@ -321,14 +321,14 @@ namespace Ryujinx.Ui.Common.Configuration
/// </summary> /// </summary>
/// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks> /// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
/// TODO: Remove this when those older versions aren't in use anymore. /// TODO: Remove this when those older versions aren't in use anymore.
public List<object> KeyboardConfig { get; set; } public List<JsonObject> KeyboardConfig { get; set; }
/// <summary> /// <summary>
/// Legacy controller control bindings /// Legacy controller control bindings
/// </summary> /// </summary>
/// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks> /// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
/// TODO: Remove this when those older versions aren't in use anymore. /// TODO: Remove this when those older versions aren't in use anymore.
public List<object> ControllerConfig { get; set; } public List<JsonObject> ControllerConfig { get; set; }
/// <summary> /// <summary>
/// Input configurations /// Input configurations
@ -354,11 +354,12 @@ namespace Ryujinx.Ui.Common.Configuration
/// Loads a configuration file from disk /// Loads a configuration file from disk
/// </summary> /// </summary>
/// <param name="path">The path to the JSON configuration file</param> /// <param name="path">The path to the JSON configuration file</param>
/// <param name="configurationFileFormat">Parsed configuration file</param>
public static bool TryLoad(string path, out ConfigurationFileFormat configurationFileFormat) public static bool TryLoad(string path, out ConfigurationFileFormat configurationFileFormat)
{ {
try try
{ {
configurationFileFormat = JsonHelper.DeserializeFromFile<ConfigurationFileFormat>(path); configurationFileFormat = JsonHelper.DeserializeFromFile(path, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat);
return configurationFileFormat.Version != 0; return configurationFileFormat.Version != 0;
} }
@ -376,8 +377,7 @@ namespace Ryujinx.Ui.Common.Configuration
/// <param name="path">The path to the JSON configuration file</param> /// <param name="path">The path to the JSON configuration file</param>
public void SaveConfig(string path) public void SaveConfig(string path)
{ {
using FileStream fileStream = File.Create(path, 4096, FileOptions.WriteThrough); JsonHelper.SerializeToFile(path, this, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat);
JsonHelper.Serialize(fileStream, this, true);
} }
} }
} }

View File

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

View File

@ -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
{
}
}

View File

@ -9,6 +9,7 @@ using Ryujinx.Ui.Common.Configuration.Ui;
using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.Helper;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace Ryujinx.Ui.Common.Configuration namespace Ryujinx.Ui.Common.Configuration
{ {
@ -631,8 +632,8 @@ namespace Ryujinx.Ui.Common.Configuration
EnableKeyboard = Hid.EnableKeyboard, EnableKeyboard = Hid.EnableKeyboard,
EnableMouse = Hid.EnableMouse, EnableMouse = Hid.EnableMouse,
Hotkeys = Hid.Hotkeys, Hotkeys = Hid.Hotkeys,
KeyboardConfig = new List<object>(), KeyboardConfig = new List<JsonObject>(),
ControllerConfig = new List<object>(), ControllerConfig = new List<JsonObject>(),
InputConfig = Hid.InputConfig, InputConfig = Hid.InputConfig,
GraphicsBackend = Graphics.GraphicsBackend, GraphicsBackend = Graphics.GraphicsBackend,
PreferredGpu = Graphics.PreferredGpu PreferredGpu = Graphics.PreferredGpu

View File

@ -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<Language>))]
public enum Language public enum Language
{ {
Japanese, Japanese,

View File

@ -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<Region>))]
public enum Region public enum Region
{ {
Japan, Japan,

View File

@ -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<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<string, string> Release { get; set; }
[JsonPropertyName("gamesSwitch")]
public List<AmiiboApiGamesSwitch> 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);
}
}
}

View File

@ -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<AmiiboApiUsage> AmiiboUsage { get; set; }
[JsonPropertyName("gameID")]
public List<string> GameId { get; set; }
[JsonPropertyName("gameName")]
public string GameName { get; set; }
}
}

View File

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

View File

@ -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<AmiiboApi> Amiibo { get; set; }
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace Ryujinx.Ui.Common.Models.Amiibo
{
[JsonSerializable(typeof(AmiiboJson))]
public partial class AmiiboJsonSerializerContext : JsonSerializerContext
{
}
}

View File

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

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Ryujinx.Ui.Common.Models.Github
{
public class GithubReleasesJsonResponse
{
public string Name { get; set; }
public List<GithubReleaseAssetJsonResponse> Assets { get; set; }
}
}

View File

@ -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
{
}
}

View File

@ -2,14 +2,14 @@ using Gtk;
using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip; using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json.Linq;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.Ui; using Ryujinx.Ui;
using Ryujinx.Ui.Common.Models.Github;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -38,6 +38,8 @@ namespace Ryujinx.Modules
private static string _buildUrl; private static string _buildUrl;
private static long _buildSize; 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. // 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" }; private static readonly string[] WindowsDependencyDirs = new string[] { "bin", "etc", "lib", "share" };
@ -107,22 +109,16 @@ namespace Ryujinx.Modules
// Fetch latest build information // Fetch latest build information
string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL); string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL);
JObject jsonRoot = JObject.Parse(fetchedJson); var fetched = JsonHelper.Deserialize(fetchedJson, SerializerContext.GithubReleasesJsonResponse);
JToken assets = jsonRoot["assets"]; _buildVer = fetched.Name;
_buildVer = (string)jsonRoot["name"]; foreach (var asset in fetched.Assets)
foreach (JToken asset in assets)
{ {
string assetName = (string)asset["name"]; if (asset.Name.StartsWith("ryujinx") && asset.Name.EndsWith(_platformExt))
string assetState = (string)asset["state"];
string downloadURL = (string)asset["browser_download_url"];
if (assetName.StartsWith("ryujinx") && assetName.EndsWith(_platformExt))
{ {
_buildUrl = downloadURL; _buildUrl = asset.BrowserDownloadUrl;
if (assetState != "uploaded") if (asset.State != "uploaded")
{ {
if (showVersionUpToDate) if (showVersionUpToDate)
{ {

View File

@ -31,7 +31,7 @@ namespace Ryujinx.Ui.Windows
{ {
string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/"); string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
_patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString)); _patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize(patreonJsonString, CommonJsonContext.Default.StringArray));
} }
catch catch
{ {

View File

@ -3,6 +3,7 @@ using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Models.Amiibo;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -11,65 +12,15 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json.Serialization; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using AmiiboApi = Ryujinx.Ui.Common.Models.Amiibo.AmiiboApi;
using AmiiboJsonSerializerContext = Ryujinx.Ui.Common.Models.Amiibo.AmiiboJsonSerializerContext;
namespace Ryujinx.Ui.Windows namespace Ryujinx.Ui.Windows
{ {
public partial class AmiiboWindow : Window public partial class AmiiboWindow : Window
{ {
private struct AmiiboJson
{
[JsonPropertyName("amiibo")]
public List<AmiiboApi> 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<string, string> Release { get; set; }
[JsonPropertyName("gamesSwitch")]
public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
}
private class AmiiboApiGamesSwitch
{
[JsonPropertyName("amiiboUsage")]
public List<AmiiboApiUsage> AmiiboUsage { get; set; }
[JsonPropertyName("gameID")]
public List<string> 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\": [] }"; private const string DEFAULT_JSON = "{ \"amiibo\": [] }";
public string AmiiboId { get; private set; } public string AmiiboId { get; private set; }
@ -96,6 +47,8 @@ namespace Ryujinx.Ui.Windows
private List<AmiiboApi> _amiiboList; private List<AmiiboApi> _amiiboList;
private static readonly AmiiboJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public AmiiboWindow() : base($"Ryujinx {Program.Version} - Amiibo") public AmiiboWindow() : base($"Ryujinx {Program.Version} - Amiibo")
{ {
Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"); 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)) if (File.Exists(_amiiboJsonPath))
{ {
amiiboJsonString = File.ReadAllText(_amiiboJsonPath); amiiboJsonString = await File.ReadAllTextAsync(_amiiboJsonPath);
if (await NeedsUpdate(JsonHelper.Deserialize<AmiiboJson>(amiiboJsonString).LastUpdated)) if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).LastUpdated))
{ {
amiiboJsonString = await DownloadAmiiboJson(); amiiboJsonString = await DownloadAmiiboJson();
} }
@ -148,7 +101,7 @@ namespace Ryujinx.Ui.Windows
} }
} }
_amiiboList = JsonHelper.Deserialize<AmiiboJson>(amiiboJsonString).Amiibo; _amiiboList = JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).Amiibo;
_amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
if (LastScannedAmiiboShowAll) if (LastScannedAmiiboShowAll)

View File

@ -115,6 +115,8 @@ namespace Ryujinx.Ui.Windows
private bool _mousePressed; private bool _mousePressed;
private bool _middleMousePressed; 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) { } 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")) private ControllerWindow(MainWindow mainWindow, Builder builder, PlayerIndex controllerId) : base(builder.GetRawOwnedObject("_controllerWin"))
@ -1120,10 +1122,7 @@ namespace Ryujinx.Ui.Windows
try try
{ {
using (Stream stream = File.OpenRead(path)) config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig);
{
config = JsonHelper.Deserialize<InputConfig>(stream);
}
} }
catch (JsonException) { } catch (JsonException) { }
} }
@ -1145,9 +1144,7 @@ namespace Ryujinx.Ui.Windows
if (profileDialog.Run() == (int)ResponseType.Ok) if (profileDialog.Run() == (int)ResponseType.Ok)
{ {
string path = System.IO.Path.Combine(GetProfileBasePath(), profileDialog.FileName); string path = System.IO.Path.Combine(GetProfileBasePath(), profileDialog.FileName);
string jsonString; string jsonString = JsonHelper.Serialize(inputConfig, SerializerContext.InputConfig);
jsonString = JsonHelper.Serialize(inputConfig, true);
File.WriteAllText(path, jsonString); File.WriteAllText(path, jsonString);
} }

View File

@ -7,15 +7,13 @@ using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using GUI = Gtk.Builder.ObjectAttribute;
using GUI = Gtk.Builder.ObjectAttribute;
using JsonHelper = Ryujinx.Common.Utilities.JsonHelper;
namespace Ryujinx.Ui.Windows namespace Ryujinx.Ui.Windows
{ {
@ -26,6 +24,8 @@ namespace Ryujinx.Ui.Windows
private readonly string _dlcJsonPath; private readonly string _dlcJsonPath;
private readonly List<DownloadableContentContainer> _dlcContainerList; private readonly List<DownloadableContentContainer> _dlcContainerList;
private static readonly DownloadableContentJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
#pragma warning disable CS0649, IDE0044 #pragma warning disable CS0649, IDE0044
[GUI] Label _baseTitleInfoLabel; [GUI] Label _baseTitleInfoLabel;
[GUI] TreeView _dlcTreeView; [GUI] TreeView _dlcTreeView;
@ -45,7 +45,7 @@ namespace Ryujinx.Ui.Windows
try try
{ {
_dlcContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_dlcJsonPath); _dlcContainerList = JsonHelper.DeserializeFromFile(_dlcJsonPath, SerializerContext.ListDownloadableContentContainer);
} }
catch catch
{ {
@ -260,10 +260,7 @@ namespace Ryujinx.Ui.Windows
while (_dlcTreeView.Model.IterNext(ref parentIter)); while (_dlcTreeView.Model.IterNext(ref parentIter));
} }
using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough)) JsonHelper.SerializeToFile(_dlcJsonPath, _dlcContainerList, SerializerContext.ListDownloadableContentContainer);
{
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true)));
}
Dispose(); Dispose();
} }

View File

@ -7,6 +7,7 @@ using LibHac.Ns;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.Ui.Widgets; using Ryujinx.Ui.Widgets;
@ -14,10 +15,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using GUI = Gtk.Builder.ObjectAttribute;
using SpanHelpers = LibHac.Common.SpanHelpers;
using GUI = Gtk.Builder.ObjectAttribute;
using JsonHelper = Ryujinx.Common.Utilities.JsonHelper;
namespace Ryujinx.Ui.Windows namespace Ryujinx.Ui.Windows
{ {
@ -31,6 +30,7 @@ namespace Ryujinx.Ui.Windows
private TitleUpdateMetadata _titleUpdateWindowData; private TitleUpdateMetadata _titleUpdateWindowData;
private readonly Dictionary<RadioButton, string> _radioButtonToPathDictionary; private readonly Dictionary<RadioButton, string> _radioButtonToPathDictionary;
private static readonly TitleUpdateMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
#pragma warning disable CS0649, IDE0044 #pragma warning disable CS0649, IDE0044
[GUI] Label _baseTitleInfoLabel; [GUI] Label _baseTitleInfoLabel;
@ -53,7 +53,7 @@ namespace Ryujinx.Ui.Windows
try try
{ {
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath); _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath, SerializerContext.TitleUpdateMetadata);
} }
catch catch
{ {
@ -192,10 +192,7 @@ namespace Ryujinx.Ui.Windows
} }
} }
using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough)) JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, SerializerContext.TitleUpdateMetadata);
{
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
}
_parent.UpdateGameTable(); _parent.UpdateGameTable();