From 58f588ef7ccb9f0b3314745ae024a007bf6c563e Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Fri, 31 May 2024 19:55:54 +0200 Subject: [PATCH 1/8] Switch from Newtonsoft.Json to System.Text.Json --- Directory.Packages.props | 1 + src/BizHawk.Client.Common/OpenAdvanced.cs | 9 +- src/BizHawk.Client.Common/RecentFiles.cs | 2 +- src/BizHawk.Client.Common/config/Config.cs | 12 +-- .../config/ConfigExtensions.cs | 20 ++--- .../config/ConfigService.cs | 81 +++++++++-------- .../config/FeedbackBind.cs | 2 +- src/BizHawk.Client.Common/config/PathEntry.cs | 3 +- .../config/PathEntryCollection.cs | 4 +- .../config/ToolDialogSettings.cs | 8 +- .../lua/LuaDocumentation.cs | 8 +- .../movie/tasproj/TasBranch.cs | 32 ++----- .../movie/tasproj/TasLagLog.cs | 11 ++- .../movie/tasproj/TasMovie.IO.cs | 13 ++- .../RetroAchievements/RAIntegration.Update.cs | 5 +- src/BizHawk.Client.EmuHawk/UpdateChecker.cs | 7 +- .../tools/ToolManager.cs | 6 +- .../Array2DJsonConverter.cs | 87 +++++++++++++++++++ .../BizHawk.Emulation.Common.csproj | 2 +- .../ByteArrayAsNormalArrayJsonConverter.cs | 44 ++++++++++ src/BizHawk.Emulation.Common/TextState.cs | 4 - .../U8ArrayAsNormalJSONListConverter.cs | 63 -------------- .../Atari/2600/Atari2600.ISettable.cs | 6 -- .../Atari/A7800Hawk/A7800Hawk.ISettable.cs | 2 +- .../Consoles/Atari/Stella/Stella.ISettable.cs | 6 -- .../Consoles/Atari/lynx/Lynx.IStatable.cs | 8 +- .../Consoles/Coleco/ColecoVision.ISettable.cs | 2 +- .../GCE/Vectrex/VectrexHawk.ISettable.cs | 5 +- .../Intellivision/Intellivision.ISettable.cs | 2 +- .../Nintendo/GBHawk/GBHawk.ISettable.cs | 5 +- .../GBHawkLink/GBHawkLink.ISettable.cs | 7 +- .../GBHawkLink3x/GBHawkLink3x.ISettable.cs | 9 +- .../GBHawkLink4x/GBHawkLink4x.ISettable.cs | 11 +-- .../Nintendo/Gameboy/Gambatte.IStatable.cs | 9 +- .../Gameboy/GambatteLink.IStatable.cs | 10 +-- .../N64/N64SyncSettings.Controller.cs | 3 - .../Nintendo/NDS/MelonDS.ISettable.cs | 15 +--- .../Consoles/Nintendo/NES/NES.ISettable.cs | 11 ++- .../Consoles/Nintendo/NES/NESControllers.cs | 6 +- .../Nintendo/QuickNES/QuickNES.ISettable.cs | 4 - .../Nintendo/SameBoy/SameBoy.ISettable.cs | 6 +- .../Consoles/Sega/gpgx64/GPGX.ISettable.cs | 8 -- .../Consoles/Sony/PSX/Octoshock.cs | 5 +- .../WonderSwan/WonderSwan.IStatable.cs | 9 +- .../config/SerializationStabilityTests.cs | 19 ++-- 45 files changed, 279 insertions(+), 313 deletions(-) create mode 100644 src/BizHawk.Emulation.Common/Array2DJsonConverter.cs create mode 100644 src/BizHawk.Emulation.Common/ByteArrayAsNormalArrayJsonConverter.cs delete mode 100644 src/BizHawk.Emulation.Common/U8ArrayAsNormalJSONListConverter.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 57fba17f291..98182c0fd5d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -40,6 +40,7 @@ + diff --git a/src/BizHawk.Client.Common/OpenAdvanced.cs b/src/BizHawk.Client.Common/OpenAdvanced.cs index a33b5cb1de2..f82c5a0142f 100644 --- a/src/BizHawk.Client.Common/OpenAdvanced.cs +++ b/src/BizHawk.Client.Common/OpenAdvanced.cs @@ -1,9 +1,8 @@ using System.IO; +using System.Text.Json; using BizHawk.Common.StringExtensions; -using Newtonsoft.Json; - //this file contains some cumbersome self-"serialization" in order to gain a modicum of control over what the serialized output looks like //I don't want them to look like crufty json @@ -94,12 +93,12 @@ public struct Token public void Deserialize(string str) { - token = JsonConvert.DeserializeObject(str); + token = JsonSerializer.Deserialize(str); } public void Serialize(TextWriter tw) { - tw.Write(JsonConvert.SerializeObject(token)); + tw.Write(JsonSerializer.Serialize(token)); } public string CorePath @@ -186,4 +185,4 @@ public void Serialize(TextWriter tw) tw.Write(Path); } } -} \ No newline at end of file +} diff --git a/src/BizHawk.Client.Common/RecentFiles.cs b/src/BizHawk.Client.Common/RecentFiles.cs index 5f37582d1b0..e0c092070f4 100644 --- a/src/BizHawk.Client.Common/RecentFiles.cs +++ b/src/BizHawk.Client.Common/RecentFiles.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BizHawk.Client.Common { diff --git a/src/BizHawk.Client.Common/config/Config.cs b/src/BizHawk.Client.Common/config/Config.cs index 978734440c2..8d1faaef1ec 100644 --- a/src/BizHawk.Client.Common/config/Config.cs +++ b/src/BizHawk.Client.Common/config/Config.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Drawing; using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; using BizHawk.Bizware.Graphics; using BizHawk.Common; @@ -9,9 +11,6 @@ using BizHawk.Emulation.Common; using BizHawk.Emulation.Cores; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - namespace BizHawk.Client.Common { public class Config @@ -368,8 +367,10 @@ public void SetWindowScaleFor(string sysID, int windowScale) public bool VideoWriterAudioSyncEffective; // Emulation core settings - internal Dictionary CoreSettings { get; set; } = new Dictionary(); - internal Dictionary CoreSyncSettings { get; set; } = new Dictionary(); + [JsonInclude] + internal Dictionary CoreSettings { get; set; } = new(); + [JsonInclude] + internal Dictionary CoreSyncSettings { get; set; } = new(); public Dictionary CommonToolSettings { get; set; } = new Dictionary(); public Dictionary> CustomToolSettings { get; set; } = new Dictionary>(); @@ -405,6 +406,7 @@ public void SetWindowScaleFor(string sysID, int windowScale) public bool GbAsSgb { get; set; } public string LibretroCore { get; set; } + [JsonPropertyOrder(-1)] public Dictionary PreferredCores = GenDefaultCorePreferences(); public bool DontTryOtherCores { get; set; } diff --git a/src/BizHawk.Client.Common/config/ConfigExtensions.cs b/src/BizHawk.Client.Common/config/ConfigExtensions.cs index 17176e6e436..c443aae4657 100644 --- a/src/BizHawk.Client.Common/config/ConfigExtensions.cs +++ b/src/BizHawk.Client.Common/config/ConfigExtensions.cs @@ -1,33 +1,23 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json; using BizHawk.Common.StringExtensions; using BizHawk.Emulation.Common; -using Newtonsoft.Json.Linq; - namespace BizHawk.Client.Common { public static class ConfigExtensions { - private class TypeNameEncapsulator - { - public object o; - } - private static JToken Serialize(object o) + private static JsonElement Serialize(object o) { - var tne = new TypeNameEncapsulator { o = o }; - return JToken.FromObject(tne, ConfigService.Serializer)["o"]; - - // Maybe todo: This code is identical to the code above, except that it does not emit the legacy "$type" - // parameter that we no longer need here. Leaving that in to make bisecting during this dev phase easier, and such. - // return JToken.FromObject(o, ConfigService.Serializer); + return JsonSerializer.SerializeToElement(o, ConfigService.SerializerOptions); } - private static object Deserialize(JToken j, Type type) + private static object Deserialize(JsonElement json, Type type) { try { - return j?.ToObject(type, ConfigService.Serializer); + return json.Deserialize(type, ConfigService.SerializerOptions); } catch { diff --git a/src/BizHawk.Client.Common/config/ConfigService.cs b/src/BizHawk.Client.Common/config/ConfigService.cs index fda1ba59ed8..ac120c23df9 100644 --- a/src/BizHawk.Client.Common/config/ConfigService.cs +++ b/src/BizHawk.Client.Common/config/ConfigService.cs @@ -1,37 +1,45 @@ +using System.Globalization; using System.IO; -using System.Reflection; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using BizHawk.Common; - -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; - -#pragma warning disable 618 +using BizHawk.Emulation.Common; namespace BizHawk.Client.Common { - public static class ConfigService + internal class FloatConverter : JsonConverter { - internal static readonly JsonSerializer Serializer; + public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetSingle(); - static ConfigService() + public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) { - Serializer = new JsonSerializer - { - MissingMemberHandling = MissingMemberHandling.Ignore, - TypeNameHandling = TypeNameHandling.Auto, - ConstructorHandling = ConstructorHandling.Default, - - // because of the peculiar setup of Binding.cs and PathEntry.cs - ObjectCreationHandling = ObjectCreationHandling.Replace, - - ContractResolver = new DefaultContractResolver - { - DefaultMembersSearchFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic - }, - }; +#if NETCOREAPP + writer.WriteNumberValue(value); +#else + // gotta love the fact .net framework can't even format floats correctly by default + // can't use G7 here because it may be too low accuracy, and can't use G8 because it may be too high, see 1.0000003f or 0.8f + writer.WriteRawValue(value.ToString("R", NumberFormatInfo.InvariantInfo)); +#endif } + } + + public static class ConfigService + { + internal static readonly JsonSerializerOptions SerializerOptions = new() + { + IncludeFields = true, + AllowTrailingCommas = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true, + Converters = + { + new FloatConverter(), // this serializes floats with minimum required precision, e.g. 1.8000000012 -> 1.8 + new ByteArrayAsNormalArrayJsonConverter() // this preserves the old behaviour of e,g, 0x1234ABCD --> [18,52,171,205]; omitting it will use base64 e.g. "EjSrzQ==" + } + }; public static bool IsFromSameVersion(string filepath, out string msg) { @@ -48,7 +56,7 @@ public static bool IsFromSameVersion(string filepath, out string msg) string cfgVersionStr = null; try { - cfgVersionStr = JObject.Parse(File.ReadAllText(filepath))["LastWrittenFrom"]?.Value(); + cfgVersionStr = JsonNode.Parse(File.ReadAllText(filepath))["LastWrittenFrom"]?.GetValue(); } catch (Exception) { @@ -84,16 +92,15 @@ public static bool IsFromSameVersion(string filepath, out string msg) /// internal error public static T Load(string filepath) where T : new() { - T config = default(T); + T config = default; try { var file = new FileInfo(filepath); if (file.Exists) { - using var reader = file.OpenText(); - var r = new JsonTextReader(reader); - config = (T)Serializer.Deserialize(r, typeof(T)); + using var reader = file.OpenRead(); + config = JsonSerializer.Deserialize(reader, SerializerOptions); } } catch (Exception ex) @@ -106,12 +113,10 @@ public static bool IsFromSameVersion(string filepath, out string msg) public static void Save(string filepath, object config) { - var file = new FileInfo(filepath); try { - using var writer = file.CreateText(); - var w = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - Serializer.Serialize(w, config); + using var writer = File.Create(filepath); + JsonSerializer.Serialize(writer, config, SerializerOptions); } catch { @@ -127,9 +132,7 @@ private class TypeNameEncapsulator public static object LoadWithType(string serialized) { - using var tr = new StringReader(serialized); - using var jr = new JsonTextReader(tr); - var tne = (TypeNameEncapsulator)Serializer.Deserialize(jr, typeof(TypeNameEncapsulator)); + var tne = JsonSerializer.Deserialize(serialized, SerializerOptions); // in the case of trying to deserialize nothing, tne will be nothing // we want to return nothing @@ -138,12 +141,8 @@ public static object LoadWithType(string serialized) public static string SaveWithType(object o) { - using var sw = new StringWriter(); - using var jw = new JsonTextWriter(sw) { Formatting = Formatting.None }; var tne = new TypeNameEncapsulator { o = o }; - Serializer.Serialize(jw, tne, typeof(TypeNameEncapsulator)); - sw.Flush(); - return sw.ToString(); + return JsonSerializer.Serialize(tne, SerializerOptions); } } } diff --git a/src/BizHawk.Client.Common/config/FeedbackBind.cs b/src/BizHawk.Client.Common/config/FeedbackBind.cs index 207e94d763a..d7095194fbf 100644 --- a/src/BizHawk.Client.Common/config/FeedbackBind.cs +++ b/src/BizHawk.Client.Common/config/FeedbackBind.cs @@ -1,6 +1,6 @@ #nullable enable -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BizHawk.Client.Common { diff --git a/src/BizHawk.Client.Common/config/PathEntry.cs b/src/BizHawk.Client.Common/config/PathEntry.cs index 2759c56fa15..af54507d412 100644 --- a/src/BizHawk.Client.Common/config/PathEntry.cs +++ b/src/BizHawk.Client.Common/config/PathEntry.cs @@ -1,11 +1,10 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BizHawk.Client.Common { public sealed class PathEntry { public string Type { get; set; } - [JsonIgnore] private string _path; public string Path { diff --git a/src/BizHawk.Client.Common/config/PathEntryCollection.cs b/src/BizHawk.Client.Common/config/PathEntryCollection.cs index cba2e3a97cf..43c65d972d6 100644 --- a/src/BizHawk.Client.Common/config/PathEntryCollection.cs +++ b/src/BizHawk.Client.Common/config/PathEntryCollection.cs @@ -1,13 +1,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json.Serialization; using BizHawk.Common.CollectionExtensions; using BizHawk.Common.PathExtensions; using BizHawk.Emulation.Common; -using Newtonsoft.Json; - namespace BizHawk.Client.Common { public class PathEntryCollection @@ -164,7 +163,6 @@ public void ResolveWithDefaults() [JsonIgnore] public string FirmwaresPathFragment => this[GLOBAL, "Firmware"].Path; - [JsonIgnore] internal string TempFilesFragment => this[GLOBAL, "Temp Files"].Path; public static readonly Lazy> Defaults = new(() => new[] diff --git a/src/BizHawk.Client.Common/config/ToolDialogSettings.cs b/src/BizHawk.Client.Common/config/ToolDialogSettings.cs index 39f44327aca..7501ef40e9a 100644 --- a/src/BizHawk.Client.Common/config/ToolDialogSettings.cs +++ b/src/BizHawk.Client.Common/config/ToolDialogSettings.cs @@ -1,14 +1,18 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; - -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BizHawk.Client.Common { public class ToolDialogSettings { + // one may wonder why the public property is getting JsonIgnored and the private one JsonIncluded + [JsonInclude] + [JsonPropertyOrder(-2)] private int? _wndx; + [JsonInclude] + [JsonPropertyOrder(-1)] private int? _wndy; public ToolDialogSettings() diff --git a/src/BizHawk.Client.Common/lua/LuaDocumentation.cs b/src/BizHawk.Client.Common/lua/LuaDocumentation.cs index fb6d7dd6cd5..75a7854173a 100644 --- a/src/BizHawk.Client.Common/lua/LuaDocumentation.cs +++ b/src/BizHawk.Client.Common/lua/LuaDocumentation.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Reflection; using System.Text; -using Newtonsoft.Json; +using System.Text.Json; namespace BizHawk.Client.Common { @@ -117,18 +117,14 @@ public SublimeCompletions() Scope = "source.lua - string"; } - [JsonProperty(PropertyName = "scope")] public string Scope { get; set; } - [JsonProperty(PropertyName = "completions")] public List Completions { get; set; } = new List(); public class Completion { - [JsonProperty(PropertyName = "trigger")] public string Trigger { get; set; } - [JsonProperty(PropertyName = "contents")] public string Contents { get; set; } } } @@ -184,7 +180,7 @@ public string ToSublime2CompletionList() sc.Completions.Add(completion); } - return JsonConvert.SerializeObject(sc); + return JsonSerializer.Serialize(sc, new JsonSerializerOptions {PropertyNamingPolicy = JsonNamingPolicy.CamelCase}); } public string ToNotepadPlusPlusAutoComplete() diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs b/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs index 56cb358f213..575e5bd5b92 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; - -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Nodes; using BizHawk.Bizware.Graphics; using BizHawk.Common.IOExtensions; @@ -137,7 +137,7 @@ public void Save(ZipStateSaver bs) var nusertext = new IndexedStateLump(BinaryStateLump.BranchUserText); foreach (var b in this) { - bs.PutLump(nheader, tw => tw.WriteLine(JsonConvert.SerializeObject(b.ForSerial))); + bs.PutLump(nheader, tw => tw.WriteLine(JsonSerializer.Serialize(b.ForSerial))); bs.PutLump(ncore, (Stream s) => s.Write(b.CoreData, 0, b.CoreData.Length)); @@ -194,29 +194,15 @@ public void Load(ZipStateLoader bl, ITasMovie movie) if (!bl.GetLump(nheader, abort: false, tr => { - var header = (dynamic)JsonConvert.DeserializeObject(tr.ReadLine()); - b.Frame = (int)header.Frame; + var header = JsonSerializer.Deserialize(tr.ReadLine()); + b.Frame = header["Frame"]!.GetValue(); - var timestamp = header.TimeStamp; + var timestamp = header["TimeStamp"]; - if (timestamp != null) - { - b.TimeStamp = (DateTime)timestamp; - } - else - { - b.TimeStamp = DateTime.Now; - } + b.TimeStamp = timestamp?.GetValue() ?? DateTime.Now; - var identifier = header.UniqueIdentifier; - if (identifier != null) - { - b.Uuid = (Guid)identifier; - } - else - { - b.Uuid = Guid.NewGuid(); - } + var identifier = header["UniqueIdentifier"]; + b.Uuid = identifier?.GetValue() ?? Guid.NewGuid(); })) { return; diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasLagLog.cs b/src/BizHawk.Client.Common/movie/tasproj/TasLagLog.cs index 9e784ead9b6..9a68a1f626d 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasLagLog.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasLagLog.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; using BizHawk.Common.CollectionExtensions; -using Newtonsoft.Json; - namespace BizHawk.Client.Common { public class TasLagLog @@ -63,14 +62,14 @@ public void InsertHistoryAt(int frame, bool isLag) public void Save(TextWriter tw) { - tw.WriteLine(JsonConvert.SerializeObject(_lagLog)); - tw.WriteLine(JsonConvert.SerializeObject(_wasLag)); + tw.WriteLine(JsonSerializer.Serialize(_lagLog)); + tw.WriteLine(JsonSerializer.Serialize(_wasLag)); } public void Load(TextReader tr) { - _lagLog = JsonConvert.DeserializeObject>(tr.ReadLine()); - _wasLag = JsonConvert.DeserializeObject>(tr.ReadLine()); + _lagLog = JsonSerializer.Deserialize>(tr.ReadLine()); + _wasLag = JsonSerializer.Deserialize>(tr.ReadLine()); } public bool? History(int frame) diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs index a8c49517292..e359d8dbb40 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs @@ -1,10 +1,9 @@ -using System.IO; +using System.IO; using System.Linq; +using System.Text.Json; using BizHawk.Common.StringExtensions; -using Newtonsoft.Json; - namespace BizHawk.Client.Common { internal partial class TasMovie @@ -22,7 +21,7 @@ private void AddTasProjLumps(ZipStateSaver bs, bool isBackup = false) { // at this point, TasStateManager may be null if we're currently importing a .bk2 - var settings = JsonConvert.SerializeObject(TasStateManager?.Settings ?? Session.Settings.DefaultTasStateManagerSettings); + var settings = JsonSerializer.Serialize(TasStateManager?.Settings ?? Session.Settings.DefaultTasStateManagerSettings); bs.PutLump(BinaryStateLump.StateHistorySettings, tw => tw.WriteLine(settings)); bs.PutLump(BinaryStateLump.LagLog, tw => LagLog.Save(tw)); bs.PutLump(BinaryStateLump.Markers, tw => tw.WriteLine(Markers.ToString())); @@ -43,7 +42,7 @@ private void AddTasProjLumps(ZipStateSaver bs, bool isBackup = false) Branches.Save(bs); } - bs.PutLump(BinaryStateLump.Session, tw => tw.WriteLine(JsonConvert.SerializeObject(TasSession))); + bs.PutLump(BinaryStateLump.Session, tw => tw.WriteLine(JsonSerializer.Serialize(TasSession))); if (!isBackup && TasStateManager is not null) { @@ -135,7 +134,7 @@ private void LoadTasprojExtras(ZipStateLoader bl) var json = tr.ReadToEnd(); try { - TasSession = JsonConvert.DeserializeObject(json); + TasSession = JsonSerializer.Deserialize(json); Branches.Current = TasSession.CurrentBranch; } catch @@ -150,7 +149,7 @@ private void LoadTasprojExtras(ZipStateLoader bl) var json = tr.ReadToEnd(); try { - settings = JsonConvert.DeserializeObject(json); + settings = JsonSerializer.Deserialize(json); } catch { diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.Update.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.Update.cs index 668df2be2da..7f7bcdc6949 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.Update.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.Update.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Runtime.InteropServices; - -using Newtonsoft.Json; +using System.Text.Json; using BizHawk.BizInvoke; using BizHawk.Common; @@ -62,7 +61,7 @@ public static bool CheckUpdateRA(IDialogParent dialogParent) try { var http = new HttpCommunication(null, "https://retroachievements.org/dorequest.php?r=latestintegration", null); - var info = JsonConvert.DeserializeObject>(http.ExecGet()); + var info = JsonSerializer.Deserialize>(http.ExecGet()); if (info.TryGetValue("Success", out var success) && (bool)success) { var lastestVer = new Version((string)info["LatestVersion"]); diff --git a/src/BizHawk.Client.EmuHawk/UpdateChecker.cs b/src/BizHawk.Client.EmuHawk/UpdateChecker.cs index b1bff70609f..0fe48c3b684 100644 --- a/src/BizHawk.Client.EmuHawk/UpdateChecker.cs +++ b/src/BizHawk.Client.EmuHawk/UpdateChecker.cs @@ -1,12 +1,11 @@ using System.IO; using System.Net; +using System.Text.Json.Nodes; using System.Threading; using BizHawk.Client.Common; using BizHawk.Common; -using Newtonsoft.Json.Linq; - namespace BizHawk.Client.EmuHawk { public static class UpdateChecker @@ -73,9 +72,9 @@ private static void CheckInternal() { try { - JObject response = JObject.Parse(DownloadURLAsString(_latestVersionInfoURL)); + var response = JsonNode.Parse(DownloadURLAsString(_latestVersionInfoURL)); - LatestVersion = ValidateVersionNumberString((string)response["name"]); + LatestVersion = ValidateVersionNumberString(response["name"].GetValue()); } catch { diff --git a/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs b/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs index 4db66f30adc..3b2915f026f 100644 --- a/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs +++ b/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs @@ -391,8 +391,8 @@ private static bool HasCustomConfig(IToolForm tool) private static void InstallCustomConfig(IToolForm tool, Dictionary data) { Type type = tool.GetType(); - var props = type.GetPropertiesWithAttrib(typeof(ConfigPersistAttribute)).ToList(); - if (props.Count == 0) + var props = type.GetPropertiesWithAttrib(typeof(ConfigPersistAttribute)).ToArray(); + if (props.Length == 0) { return; } @@ -425,7 +425,7 @@ private static void InstallCustomConfig(IToolForm tool, Dictionary SaveCustomConfig(tool, data, props); } - private static void SaveCustomConfig(IToolForm tool, Dictionary data, List props) + private static void SaveCustomConfig(IToolForm tool, Dictionary data, PropertyInfo[] props) { data.Clear(); foreach (var prop in props) diff --git a/src/BizHawk.Emulation.Common/Array2DJsonConverter.cs b/src/BizHawk.Emulation.Common/Array2DJsonConverter.cs new file mode 100644 index 00000000000..168165cc55b --- /dev/null +++ b/src/BizHawk.Emulation.Common/Array2DJsonConverter.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BizHawk.Emulation.Common +{ + // heavily inspired by https://stackoverflow.com/questions/66280645/how-can-i-serialize-a-double-2d-array-to-json-using-system-text-json + public class Array2DJsonConverter : JsonConverter + { + public override void Write(Utf8JsonWriter writer, T[,]? array, JsonSerializerOptions options) + { + if (array is null) + { + writer.WriteNullValue(); + return; + } + + int rowsFirstIndex = array.GetLowerBound(0); + int rowsLastIndex = array.GetUpperBound(0); + var arrayConverter = (JsonConverter)options.GetConverter(typeof(T[])); + + writer.WriteStartArray(); + for (int i = rowsFirstIndex; i <= rowsLastIndex; i++) + { + arrayConverter.Write(writer, array.SliceRow(i).ToArray(), options); + } + writer.WriteEndArray(); + } + + public override T[,]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.Null) return null; + if (reader.TokenType is not JsonTokenType.StartArray) + throw new JsonException($"Unexpected token when reading bytes: expected {nameof(JsonTokenType.StartArray)}, got {reader.TokenType}"); + + var listConverter = (JsonConverter)options.GetConverter(typeof(T[])); + List fullList = new(); + while (reader.Read()) + switch (reader.TokenType) + { + // handle both base64 string and array formats here and trust the converter to handle it + case JsonTokenType.String: + case JsonTokenType.StartArray: + var result = listConverter.Read(ref reader, typeof(T[]), options)!; + fullList.Add(result); + continue; + case JsonTokenType.EndArray: + return fullList.To2D(); + case JsonTokenType.Comment: + continue; + default: + throw new JsonException($"Unexpected token when reading bytes: {reader.TokenType}"); + } + + throw new JsonException("Unexpected end when reading bytes"); + } + } + + internal static class ArrayExtensions + { + public static T[,] To2D(this IList source) + { + int firstDimension = source.Count; + int secondDimension = source.FirstOrDefault()?.Length ?? 0; + + // sanity check; the input must consist of arrays of the same size + if (source.Any(row => row.Length != secondDimension)) + throw new InvalidOperationException(); + + var result = new T[firstDimension, secondDimension]; + for (int i = 0; i < firstDimension; i++) + for (int j = 0; j < source[i].Length; j++) + result[i, j] = source[i][j]; + + return result; + } + + public static IEnumerable SliceRow(this T[,] array, int row) + { + for (int i = array.GetLowerBound(1); i <= array.GetUpperBound(1); i++) + { + yield return array[row, i]; + } + } + } +} diff --git a/src/BizHawk.Emulation.Common/BizHawk.Emulation.Common.csproj b/src/BizHawk.Emulation.Common/BizHawk.Emulation.Common.csproj index 8790dd2cae6..f811bf49e58 100644 --- a/src/BizHawk.Emulation.Common/BizHawk.Emulation.Common.csproj +++ b/src/BizHawk.Emulation.Common/BizHawk.Emulation.Common.csproj @@ -8,8 +8,8 @@ - + diff --git a/src/BizHawk.Emulation.Common/ByteArrayAsNormalArrayJsonConverter.cs b/src/BizHawk.Emulation.Common/ByteArrayAsNormalArrayJsonConverter.cs new file mode 100644 index 00000000000..1ecae3cde73 --- /dev/null +++ b/src/BizHawk.Emulation.Common/ByteArrayAsNormalArrayJsonConverter.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BizHawk.Emulation.Common +{ + /// based on this SO answer + public sealed class ByteArrayAsNormalArrayJsonConverter : JsonConverter + { + public override byte[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.Null) return null; + if (reader.TokenType is not JsonTokenType.StartArray) throw new JsonException($"Unexpected token when reading bytes: expected {nameof(JsonTokenType.StartArray)}, got {reader.TokenType}"); + + List list = new(); + while (reader.Read()) switch (reader.TokenType) + { + case JsonTokenType.Number: + list.Add(reader.GetByte()); + continue; + case JsonTokenType.EndArray: + return list.ToArray(); + case JsonTokenType.Comment: + continue; + default: + throw new JsonException($"Unexpected token when reading bytes: {reader.TokenType}"); + } + throw new JsonException("Unexpected end when reading bytes"); + } + + public override void Write(Utf8JsonWriter writer, byte[]? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartArray(); + foreach (byte b in value) writer.WriteNumberValue(b); + writer.WriteEndArray(); + } + } +} diff --git a/src/BizHawk.Emulation.Common/TextState.cs b/src/BizHawk.Emulation.Common/TextState.cs index d2374142752..e8fa77ce5e7 100644 --- a/src/BizHawk.Emulation.Common/TextState.cs +++ b/src/BizHawk.Emulation.Common/TextState.cs @@ -5,8 +5,6 @@ using BizHawk.Common.CollectionExtensions; -using Newtonsoft.Json; - namespace BizHawk.Emulation.Common { // managed counterpart to unmanaged serialization code in GB and WSWAN cores @@ -42,10 +40,8 @@ public bool ShouldSerializeObjects() public readonly Node Root = new Node(); - [JsonIgnore] private Stack Nodes; - [JsonIgnore] private Node Current => Nodes.Peek(); public void Prepare() diff --git a/src/BizHawk.Emulation.Common/U8ArrayAsNormalJSONListConverter.cs b/src/BizHawk.Emulation.Common/U8ArrayAsNormalJSONListConverter.cs deleted file mode 100644 index 083e50876c0..00000000000 --- a/src/BizHawk.Emulation.Common/U8ArrayAsNormalJSONListConverter.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; - -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace BizHawk.Emulation.Common -{ - /// seems unnecessary, but suggested by official docs so sure why not - public sealed class U8ArrayAsNormalJSONListResolver : DefaultContractResolver - { - public static readonly U8ArrayAsNormalJSONListResolver INSTANCE = new(); - - protected override JsonContract CreateContract(Type objectType) - { - var contract = base.CreateContract(objectType); - if (objectType == typeof(byte[])) contract.Converter = U8ArrayAsNormalJSONListConverter.INSTANCE; - return contract; - } - } - - /// based on this SO answer - public sealed class U8ArrayAsNormalJSONListConverter : JsonConverter - { - public static readonly U8ArrayAsNormalJSONListConverter INSTANCE = new(); - - public override byte[]? ReadJson(JsonReader reader, Type objectType, byte[]? existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.TokenType is JsonToken.Null) return null; - if (reader.TokenType is not JsonToken.StartArray) throw new Exception($"Unexpected token when reading bytes: expected {nameof(JsonToken.StartArray)}, got {reader.TokenType}"); - List list = new(); - while (reader.Read()) switch (reader.TokenType) - { - case JsonToken.Integer: - list.Add(reader.Value switch - { - byte b => b, - long l and >= byte.MinValue and <= byte.MaxValue => unchecked((byte) l), - var o => throw new Exception($"Integer literal outside u8 range: {o}") - }); - continue; - case JsonToken.EndArray: - return list.ToArray(); - case JsonToken.Comment: - continue; - default: - throw new Exception($"Unexpected token when reading bytes: {reader.TokenType}"); - } - throw new Exception("Unexpected end when reading bytes"); - } - - public override void WriteJson(JsonWriter writer, byte[]? value, JsonSerializer serializer) - { - if (value is null) - { - writer.WriteNull(); - return; - } - writer.WriteStartArray(); - for (var i = 0; i < value.Length; i++) writer.WriteValue(value[i]); - writer.WriteEndArray(); - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Consoles/Atari/2600/Atari2600.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Atari/2600/Atari2600.ISettable.cs index 37635102def..92e0ecde61c 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Atari/2600/Atari2600.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Atari/2600/Atari2600.ISettable.cs @@ -1,8 +1,6 @@ using System.ComponentModel; using System.Drawing; -using Newtonsoft.Json; - using BizHawk.Emulation.Common; using BizHawk.Common; @@ -44,16 +42,12 @@ public PutSettingsDirtyBits PutSyncSettings(A2600SyncSettings o) [CoreSettings] public class A2600Settings { - [JsonIgnore] private int _ntscTopLine; - [JsonIgnore] private int _ntscBottomLine; - [JsonIgnore] private int _palTopLine; - [JsonIgnore] private int _palBottomLine; [DisplayName("Show Background")] diff --git a/src/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/A7800Hawk.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/A7800Hawk.ISettable.cs index b9c06464ed3..1ec80f89f24 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/A7800Hawk.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Atari/A7800Hawk/A7800Hawk.ISettable.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; using BizHawk.Common; using BizHawk.Emulation.Common; diff --git a/src/BizHawk.Emulation.Cores/Consoles/Atari/Stella/Stella.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Atari/Stella/Stella.ISettable.cs index 10c6a8db935..09c87727ab3 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Atari/Stella/Stella.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Atari/Stella/Stella.ISettable.cs @@ -1,8 +1,6 @@ using System.ComponentModel; using System.Drawing; -using Newtonsoft.Json; - using BizHawk.Emulation.Common; using BizHawk.Common; using BizHawk.Emulation.Cores.Consoles.Atari.Stella; @@ -40,16 +38,12 @@ public PutSettingsDirtyBits PutSyncSettings(A2600SyncSettings o) [CoreSettings] public class A2600Settings { - [JsonIgnore] private int _ntscTopLine; - [JsonIgnore] private int _ntscBottomLine; - [JsonIgnore] private int _palTopLine; - [JsonIgnore] private int _palBottomLine; [DisplayName("Show Background")] diff --git a/src/BizHawk.Emulation.Cores/Consoles/Atari/lynx/Lynx.IStatable.cs b/src/BizHawk.Emulation.Cores/Consoles/Atari/lynx/Lynx.IStatable.cs index df0d12f4565..88a0d0e133c 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Atari/lynx/Lynx.IStatable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Atari/lynx/Lynx.IStatable.cs @@ -1,5 +1,5 @@ using System.IO; -using Newtonsoft.Json; +using System.Text.Json; using BizHawk.Emulation.Common; @@ -19,12 +19,12 @@ public void SaveStateText(TextWriter writer) s.ExtraData.LagCount = LagCount; s.ExtraData.Frame = Frame; - _ser.Serialize(writer, s); + writer.Write(JsonSerializer.Serialize(s, _options)); } public void LoadStateText(TextReader reader) { - var s = (TextState)_ser.Deserialize(reader, typeof(TextState)); + var s = JsonSerializer.Deserialize>(reader.ReadToEnd(), _options); s.Prepare(); var ff = s.GetFunctionPointersLoad(); LibLynx.TxtStateLoad(Core, ref ff); @@ -69,7 +69,7 @@ public void LoadStateBinary(BinaryReader reader) Frame = reader.ReadInt32(); } - private readonly JsonSerializer _ser = new JsonSerializer { Formatting = Formatting.Indented }; + private readonly JsonSerializerOptions _options = new() { WriteIndented = true }; private readonly byte[] _saveBuff; private class TextStateData diff --git a/src/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISettable.cs index 8ea7707b973..5906a0db7b3 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.ISettable.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; using BizHawk.Common; using BizHawk.Emulation.Common; diff --git a/src/BizHawk.Emulation.Cores/Consoles/GCE/Vectrex/VectrexHawk.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/GCE/Vectrex/VectrexHawk.ISettable.cs index 8c41a7c0712..a0fcc7823d3 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/GCE/Vectrex/VectrexHawk.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/GCE/Vectrex/VectrexHawk.ISettable.cs @@ -1,6 +1,5 @@ using System.ComponentModel; - -using Newtonsoft.Json; +using System.Text.Json.Serialization; using BizHawk.Common; using BizHawk.Emulation.Common; @@ -35,6 +34,7 @@ public PutSettingsDirtyBits PutSyncSettings(VectrexSyncSettings o) [CoreSettings] public class VectrexSyncSettings { + // TODO: should Port2 also be JsonIgnored here? [JsonIgnore] public string Port1 = VectrexHawkControllerDeck.DefaultControllerName; public string Port2 = VectrexHawkControllerDeck.DefaultControllerName; @@ -45,7 +45,6 @@ public enum ControllerType Analog } - [JsonIgnore] private ControllerType _VectrexController1; private ControllerType _VectrexController2; diff --git a/src/BizHawk.Emulation.Cores/Consoles/Intellivision/Intellivision.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Intellivision/Intellivision.ISettable.cs index ad9f177cfcc..f150510ee77 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Intellivision/Intellivision.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Intellivision/Intellivision.ISettable.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; using BizHawk.Common; using BizHawk.Emulation.Common; diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.ISettable.cs index da9375d6f66..9936ddd4b00 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawk/GBHawk.ISettable.cs @@ -1,6 +1,5 @@ using System.ComponentModel; - -using Newtonsoft.Json; +using System.Text.Json.Serialization; using BizHawk.Common; using BizHawk.Emulation.Common; @@ -125,9 +124,7 @@ public int RTCOffset [DefaultValue(true)] public bool Use_SRAM { get; set; } - [JsonIgnore] private int _RTCInitialTime; - [JsonIgnore] private int _RTCOffset; [JsonIgnore] public ushort _DivInitialTime = 8; diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink/GBHawkLink.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink/GBHawkLink.ISettable.cs index 3f87bf2e5cc..974f29f7045 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink/GBHawkLink.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink/GBHawkLink.ISettable.cs @@ -1,6 +1,5 @@ using System.ComponentModel; - -using Newtonsoft.Json; +using System.Text.Json.Serialization; using BizHawk.Common; using BizHawk.Emulation.Common; @@ -130,13 +129,9 @@ public int RTCOffset_R [DefaultValue(true)] public bool Use_SRAM { get; set; } - [JsonIgnore] private int _RTCInitialTime_L; - [JsonIgnore] private int _RTCInitialTime_R; - [JsonIgnore] private int _RTCOffset_L; - [JsonIgnore] private int _RTCOffset_R; [JsonIgnore] public ushort _DivInitialTime_L = 8; diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink3x/GBHawkLink3x.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink3x/GBHawkLink3x.ISettable.cs index 2505b2354c1..7452fb3d1af 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink3x/GBHawkLink3x.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink3x/GBHawkLink3x.ISettable.cs @@ -1,6 +1,5 @@ using System.ComponentModel; - -using Newtonsoft.Json; +using System.Text.Json.Serialization; using BizHawk.Common; using BizHawk.Emulation.Common; @@ -147,17 +146,11 @@ public int RTCOffset_R [DefaultValue(true)] public bool Use_SRAM { get; set; } - [JsonIgnore] private int _RTCInitialTime_L; - [JsonIgnore] private int _RTCInitialTime_C; - [JsonIgnore] private int _RTCInitialTime_R; - [JsonIgnore] private int _RTCOffset_L; - [JsonIgnore] private int _RTCOffset_C; - [JsonIgnore] private int _RTCOffset_R; [JsonIgnore] public ushort _DivInitialTime_L = 8; diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ISettable.cs index b89cd3c0cb8..13119c9a651 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/GBHawkLink4x/GBHawkLink4x.ISettable.cs @@ -1,6 +1,5 @@ using System.ComponentModel; - -using Newtonsoft.Json; +using System.Text.Json.Serialization; using BizHawk.Common; using BizHawk.Emulation.Common; @@ -176,21 +175,13 @@ public int RTCOffset_D [DefaultValue(true)] public bool Use_SRAM { get; set; } - [JsonIgnore] private int _RTCInitialTime_A; - [JsonIgnore] private int _RTCInitialTime_B; - [JsonIgnore] private int _RTCInitialTime_C; - [JsonIgnore] private int _RTCInitialTime_D; - [JsonIgnore] private int _RTCOffset_A; - [JsonIgnore] private int _RTCOffset_B; - [JsonIgnore] private int _RTCOffset_C; - [JsonIgnore] private int _RTCOffset_D; [JsonIgnore] public ushort _DivInitialTime_A = 8; diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.IStatable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.IStatable.cs index 3350697e027..3b6e5ccb7de 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.IStatable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/Gambatte.IStatable.cs @@ -1,8 +1,7 @@ //#define USE_UPSTREAM_STATES // really more for testing due to needing to use these anyways for initial state code. could potentially be used outright for states using System.IO; - -using Newtonsoft.Json; +using System.Text.Json; using BizHawk.Emulation.Common; @@ -15,12 +14,12 @@ public partial class Gameboy : IStatable, ITextStatable public void SaveStateText(TextWriter writer) { var s = SaveState(); - _ser.Serialize(writer, s); + writer.Write(JsonSerializer.Serialize(s, _options)); } public void LoadStateText(TextReader reader) { - var s = (TextState)_ser.Deserialize(reader, typeof(TextState)); + var s = JsonSerializer.Deserialize>(reader.ReadToEnd(), _options); LoadState(s); reader.ReadToEnd(); } @@ -94,7 +93,7 @@ private void NewSaveCoreSetBuff() => _stateBuf = new byte[LibGambatte.gambatte_newstatelen(GambatteState)]; #endif - private readonly JsonSerializer _ser = new() { Formatting = Formatting.Indented }; + private readonly JsonSerializerOptions _options = new() { WriteIndented = true }; // other data in the text state besides core internal class TextStateData diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IStatable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IStatable.cs index 3817e78acac..220a3cd5e72 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IStatable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/Gameboy/GambatteLink.IStatable.cs @@ -1,6 +1,5 @@ using System.IO; - -using Newtonsoft.Json; +using System.Text.Json; using BizHawk.Emulation.Common; @@ -24,12 +23,12 @@ public bool AvoidRewind public void SaveStateText(TextWriter writer) { - ser.Serialize(writer, new GBLSerialized(this)); + writer.Write(JsonSerializer.Serialize(new GBLSerialized(this), _options)); } public void LoadStateText(TextReader reader) { - var s = (GBLSerialized)ser.Deserialize(reader, typeof(GBLSerialized)); + var s = JsonSerializer.Deserialize(reader.ReadToEnd(), _options); if (s.NumCores != _numCores) { throw new InvalidOperationException("Core number mismatch!"); @@ -49,7 +48,6 @@ public void LoadStateText(TextReader reader) _linkShiftSignal = s.LinkShiftSignal; _linkSpaced = s.LinkSpaced; _linkSpaceSignal = s.LinkSpaceSignal; - reader.ReadToEnd(); } public void SaveStateBinary(BinaryWriter writer) @@ -97,7 +95,7 @@ public void LoadStateBinary(BinaryReader reader) _linkSpaceSignal = reader.ReadBoolean(); } - private readonly JsonSerializer ser = new JsonSerializer { Formatting = Formatting.Indented }; + private readonly JsonSerializerOptions _options = new() { WriteIndented = true }; private class GBLSerialized { diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/N64/N64SyncSettings.Controller.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/N64/N64SyncSettings.Controller.cs index 3dc259f0521..b27de5725e1 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/N64/N64SyncSettings.Controller.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/N64/N64SyncSettings.Controller.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using Newtonsoft.Json; namespace BizHawk.Emulation.Cores.Nintendo.N64 { @@ -25,7 +24,6 @@ public enum N64ControllerPakType TRANSFER_PAK = 4 } - [JsonIgnore] private N64ControllerPakType _type = N64ControllerPakType.NO_PAK; /// @@ -44,7 +42,6 @@ public N64ControllerPakType PakType set => _type = value; } - [JsonIgnore] private bool _isConnected = true; /// diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/MelonDS.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/MelonDS.ISettable.cs index b0a22006f4c..1ce0831eacf 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/MelonDS.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/MelonDS.ISettable.cs @@ -1,8 +1,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; - using BizHawk.Common; using BizHawk.Emulation.Common; @@ -61,7 +59,6 @@ public class NDSSettings [TypeConverter(typeof(DescribableEnumConverter))] public ScreenRotationKind ScreenRotation { get; set; } - [JsonIgnore] private int _screengap; [DisplayName("Screen Gap")] @@ -185,7 +182,6 @@ public enum ThreeDeeRendererType : int [DefaultValue(true)] public bool ThreadedRendering { get; set; } - [JsonIgnore] private int _glScaleFactor; [DisplayName("OpenGL Scale Factor")] @@ -207,8 +203,7 @@ public int GLScaleFactor [DefaultValue(false)] public bool GLHiResCoordinates { get; set; } - [JsonIgnore] - private DateTime _initaltime; + private DateTime _initialtime; [DisplayName("Initial Time")] [Description("Initial time of emulation. Not used if Use Real Time is true")] @@ -216,8 +211,8 @@ public int GLScaleFactor [TypeConverter(typeof(BizDateTimeConverter))] public DateTime InitialTime { - get => _initaltime; - set => _initaltime = value < minDate ? minDate : (value > maxDate ? maxDate : value); + get => _initialtime; + set => _initialtime = value < minDate ? minDate : (value > maxDate ? maxDate : value); } [DisplayName("Use Real Time")] @@ -264,7 +259,6 @@ public enum StartUp : int [TypeConverter(typeof(DescribableEnumConverter))] public StartUp FirmwareStartUp { get; set; } - [JsonIgnore] private string _firmwareusername; [DisplayName("Firmware Username")] @@ -310,10 +304,8 @@ public enum Month : int December, } - [JsonIgnore] private Month _firmwarebirthdaymonth; - [JsonIgnore] private int _firmwarebirthdayday; [DisplayName("Firmware Birthday Month")] @@ -373,7 +365,6 @@ public enum Color : int [TypeConverter(typeof(DescribableEnumConverter))] public Color FirmwareFavouriteColour { get; set; } - [JsonIgnore] private string _firmwaremessage; [DisplayName("Firmware Message")] diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.ISettable.cs index 2a4aa1873bd..7f2ab8c1b8f 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.ISettable.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using BizHawk.Common; using BizHawk.Emulation.Common; -using Newtonsoft.Json; - namespace BizHawk.Emulation.Cores.Nintendo.NES { public partial class NES : ISettable @@ -61,10 +60,9 @@ public enum Region public NESControlSettings Controls = new NESControlSettings(); - [JsonIgnore] private byte[]/*?*/ _initialWRamStatePattern; - [JsonConverter(typeof(U8ArrayAsNormalJSONListConverter))] // this preserves the old behaviour of e,g, 0x1234ABCD --> [18,52,171,205]; omitting it will use base64 e.g. "EjSrzQ==" + [JsonConverter(typeof(ByteArrayAsNormalArrayJsonConverter))] // this preserves the old behaviour of e,g, 0x1234ABCD --> [18,52,171,205]; omitting it will use base64 e.g. "EjSrzQ==" public byte[] InitialWRamStatePattern { get => _initialWRamStatePattern ?? [ ]; @@ -150,6 +148,7 @@ public class NESSettings public int PAL_TopLine = 0; public int PAL_BottomLine = 239; + [JsonConverter(typeof(Array2DJsonConverter))] public byte[,] Palette; public int APU_vol = 1; @@ -166,7 +165,7 @@ public NESSettings() Palette = (byte[,])Palettes.QuickNESPalette.Clone(); } - [Newtonsoft.Json.JsonConstructor] + [JsonConstructor] public NESSettings(byte[,] Palette) { if (Palette == null) diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NESControllers.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NESControllers.cs index bd4c8937f6d..0620029619f 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NESControllers.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NESControllers.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; + using BizHawk.Emulation.Common; using BizHawk.Common; -using Newtonsoft.Json; namespace BizHawk.Emulation.Cores.Nintendo.NES { @@ -1065,16 +1065,13 @@ public static IList GetNesPortValues() return new List(NesPortDevices.Keys).AsReadOnly(); } - [JsonIgnore] private bool _Famicom; public bool Famicom { get => _Famicom; set => _Famicom = value; } - [JsonIgnore] private string _NesLeftPort; - [JsonIgnore] private string _NesRightPort; public string NesLeftPort { @@ -1098,7 +1095,6 @@ public string NesRightPort throw new InvalidOperationException(); } } - [JsonIgnore] private string _FamicomExpPort; public string FamicomExpPort { diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.ISettable.cs index d0b84df2d04..7a03aae4c0a 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.ISettable.cs @@ -1,8 +1,6 @@ using System.ComponentModel; using System.Runtime.InteropServices; -using Newtonsoft.Json; - using BizHawk.Common; using BizHawk.Emulation.Common; @@ -61,7 +59,6 @@ public int NumSprites set => _NumSprites = Math.Min(64, Math.Max(0, value)); } - [JsonIgnore] private int _NumSprites; [DefaultValue(false)] @@ -87,7 +84,6 @@ public byte[] Palette } } - [JsonIgnore] private byte[] _Palette; public QuickNESSettings Clone() diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/SameBoy/SameBoy.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/SameBoy/SameBoy.ISettable.cs index 93b9ce2c4d5..e44074c00bf 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/SameBoy/SameBoy.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/SameBoy/SameBoy.ISettable.cs @@ -1,7 +1,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; - -using Newtonsoft.Json; +using System.Text.Json.Serialization; using BizHawk.Common; using BizHawk.Emulation.Common; @@ -61,6 +60,7 @@ public enum GBPaletteType : uint CUSTOM, } + [JsonInclude] private int[] _customPal; [DisplayName("GB Mono Palette")] @@ -93,7 +93,6 @@ public enum ColorCorrectionMode : uint [TypeConverter(typeof(DescribableEnumConverter))] public ColorCorrectionMode ColorCorrection { get; set; } - [JsonIgnore] private int _lighttemperature; [DisplayName("Ambient Light Temperature")] @@ -126,7 +125,6 @@ public enum HighPassFilterMode : uint [TypeConverter(typeof(DescribableEnumConverter))] public HighPassFilterMode HighPassFilter { get; set; } - [JsonIgnore] private int _interferencevolume; [DisplayName("Audio Interference Volume")] diff --git a/src/BizHawk.Emulation.Cores/Consoles/Sega/gpgx64/GPGX.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Sega/gpgx64/GPGX.ISettable.cs index c43a2e187cc..8be3680cbe6 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Sega/gpgx64/GPGX.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Sega/gpgx64/GPGX.ISettable.cs @@ -3,7 +3,6 @@ using BizHawk.Common; using BizHawk.Emulation.Common; -using Newtonsoft.Json; namespace BizHawk.Emulation.Cores.Consoles.Sega.gpgx { @@ -108,7 +107,6 @@ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo c public class GPGXSettings { [DeepEqualsIgnore] - [JsonIgnore] private bool _DrawBGA; [DisplayName("Background Layer A")] @@ -121,7 +119,6 @@ public bool DrawBGA } [DeepEqualsIgnore] - [JsonIgnore] private bool _DrawBGB; [DisplayName("Background Layer B")] @@ -134,7 +131,6 @@ public bool DrawBGB } [DeepEqualsIgnore] - [JsonIgnore] private bool _DrawBGW; [DisplayName("Background Layer W")] @@ -147,7 +143,6 @@ public bool DrawBGW } [DeepEqualsIgnore] - [JsonIgnore] private bool _DrawObj; [DisplayName("Sprite Layer")] @@ -160,7 +155,6 @@ public bool DrawObj } [DeepEqualsIgnore] - [JsonIgnore] private bool _PadScreen320; [DisplayName("Pad screen to 320")] @@ -173,7 +167,6 @@ public bool PadScreen320 } [DeepEqualsIgnore] - [JsonIgnore] private bool _Backdrop; [DisplayName("Use custom backdrop color")] @@ -186,7 +179,6 @@ public bool Backdrop } [DeepEqualsIgnore] - [JsonIgnore] private bool _noSpriteLimit; [DisplayName("Remove Per-Line Sprite Limit")] diff --git a/src/BizHawk.Emulation.Cores/Consoles/Sony/PSX/Octoshock.cs b/src/BizHawk.Emulation.Cores/Consoles/Sony/PSX/Octoshock.cs index 31a8c88adee..339fa55d920 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Sony/PSX/Octoshock.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Sony/PSX/Octoshock.cs @@ -15,8 +15,7 @@ using System.IO; using System.Collections.Generic; using System.Linq; - -using Newtonsoft.Json; +using System.Text.Json; using BizHawk.Emulation.Common; using BizHawk.Common; @@ -1065,7 +1064,7 @@ public class SyncSettings { public SyncSettings Clone() { - return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(this)); + return JsonSerializer.Deserialize(JsonSerializer.Serialize(this)); // what am I witnessing } public bool EnableLEC; diff --git a/src/BizHawk.Emulation.Cores/Consoles/WonderSwan/WonderSwan.IStatable.cs b/src/BizHawk.Emulation.Cores/Consoles/WonderSwan/WonderSwan.IStatable.cs index 31066bd3219..edd31ab04df 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/WonderSwan/WonderSwan.IStatable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/WonderSwan/WonderSwan.IStatable.cs @@ -1,7 +1,6 @@ using System.IO; using System.Runtime.InteropServices; - -using Newtonsoft.Json; +using System.Text.Json; using BizHawk.Emulation.Common; @@ -12,7 +11,7 @@ partial class WonderSwan: ITextStatable private void InitIStatable() => savebuff = new byte[BizSwan.bizswan_binstatesize(Core)]; - private readonly JsonSerializer ser = new() { Formatting = Formatting.Indented }; + private readonly JsonSerializerOptions _options = new() { WriteIndented = true }; [StructLayout(LayoutKind.Sequential)] private class TextStateData @@ -43,12 +42,12 @@ public void SaveStateText(TextWriter writer) var ff = s.GetFunctionPointersSave(); BizSwan.bizswan_txtstatesave(Core, ref ff); SaveTextStateData(s.ExtraData); - ser.Serialize(writer, s); + writer.Write(JsonSerializer.Serialize(s, _options)); } public void LoadStateText(TextReader reader) { - var s = (TextState)ser.Deserialize(reader, typeof(TextState)); + var s = JsonSerializer.Deserialize>(reader.ReadToEnd(), _options); if (s is not null) { s.Prepare(); diff --git a/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs b/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs index 2e41605caa9..df7fc3acb63 100644 --- a/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs +++ b/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs @@ -1,13 +1,12 @@ using System.Collections.Generic; using System.Drawing; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; using BizHawk.Client.Common; using BizHawk.Common; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - namespace BizHawk.Tests.Client.Common.config { [TestClass] @@ -29,7 +28,7 @@ public sealed class SerializationStabilityTests typeof(DateTime), typeof(Dictionary<,>), typeof(int), - typeof(JToken), + typeof(JsonElement), typeof(List<>), typeof(Nullable<>), typeof(object), @@ -44,7 +43,7 @@ public sealed class SerializationStabilityTests { [typeof(AnalogBind)] = @"{""Value"":""X1 LeftThumbX Axis"",""Mult"":0.8,""Deadzone"":0.1}", [typeof(CheatConfig)] = $@"{{""DisableOnLoad"":false,""LoadFileByGame"":true,""AutoSaveOnClose"":true,""Recent"":{RECENT_SER}}}", - [typeof(FeedbackBind)] = @"{""Channels"":""Left+Right"",""GamepadPrefix"":""X1 "",""Prescale"":1.0}", + [typeof(FeedbackBind)] = @"{""Channels"":""Left+Right"",""GamepadPrefix"":""X1 "",""Prescale"":1}", [typeof(MessagePosition)] = @"{""X"":0,""Y"":0,""Anchor"":0}", [typeof(MovieConfig)] = $@"{{""MovieEndAction"":3,""EnableBackupMovies"":true,""MoviesOnDisk"":false,""MovieCompressionLevel"":2,""VBAStyleMovieLoadState"":false,""DefaultTasStateManagerSettings"":{ZWINDER_SER}}}", [typeof(PathEntry)] = PATHENTRY_SER, @@ -84,11 +83,17 @@ static void CheckAll(string? groupDesc = null) CheckAll(); } + private static readonly JsonSerializerOptions SerializerOptions = new(ConfigService.SerializerOptions) + { + WriteIndented = false + }; + [TestMethod] public void TestRoundTripSerialization() { - static object Deser(string s, Type type) => JToken.Parse(s).ToObject(type, ConfigService.Serializer)!; - static string Ser(object o) => JToken.FromObject(o, ConfigService.Serializer).ToString(Formatting.None); + static object? Deser(string s, Type type) => JsonSerializer.Deserialize(s, type, SerializerOptions); + static string Ser(object? o) => JsonSerializer.Serialize(o, SerializerOptions); + foreach (var (type, s) in KnownGoodFromBizHawk) { if (s == "TODO") continue; From 6f81a4324a46c6edae70994cb7df6ab4a58473c4 Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Fri, 31 May 2024 21:42:26 +0200 Subject: [PATCH 2/8] fix TAStudio loading --- .../config/ConfigService.cs | 33 ++++++++++++++----- .../savestates/SavestateFile.cs | 2 +- .../CustomControls/InputRoll/InputRoll.cs | 19 ++++++----- src/BizHawk.Client.EmuHawk/MainForm.cs | 2 +- .../tools/BasicBot/BasicBot.cs | 2 +- .../config/SerializationStabilityTests.cs | 9 ++--- 6 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/BizHawk.Client.Common/config/ConfigService.cs b/src/BizHawk.Client.Common/config/ConfigService.cs index ac120c23df9..7706b4bc123 100644 --- a/src/BizHawk.Client.Common/config/ConfigService.cs +++ b/src/BizHawk.Client.Common/config/ConfigService.cs @@ -28,12 +28,11 @@ public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOpt public static class ConfigService { - internal static readonly JsonSerializerOptions SerializerOptions = new() + private static readonly JsonSerializerOptions NonIndentedSerializerOptions = new() { IncludeFields = true, AllowTrailingCommas = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - WriteIndented = true, Converters = { new FloatConverter(), // this serializes floats with minimum required precision, e.g. 1.8000000012 -> 1.8 @@ -41,6 +40,13 @@ public static class ConfigService } }; + private static readonly JsonSerializerOptions IndentedSerializerOptions = new(NonIndentedSerializerOptions) + { + WriteIndented = true + }; + + internal static JsonSerializerOptions SerializerOptions => NonIndentedSerializerOptions; + public static bool IsFromSameVersion(string filepath, out string msg) { const string MSGFMT_NEWER = "Your config file ({0}) is from a newer version of EmuHawk, {2} (this is {1}). It may fail to load."; @@ -100,7 +106,7 @@ public static bool IsFromSameVersion(string filepath, out string msg) if (file.Exists) { using var reader = file.OpenRead(); - config = JsonSerializer.Deserialize(reader, SerializerOptions); + config = JsonSerializer.Deserialize(reader, IndentedSerializerOptions); } } catch (Exception ex) @@ -116,7 +122,7 @@ public static void Save(string filepath, object config) try { using var writer = File.Create(filepath); - JsonSerializer.Serialize(writer, config, SerializerOptions); + JsonSerializer.Serialize(writer, config, IndentedSerializerOptions); } catch { @@ -130,13 +136,24 @@ private class TypeNameEncapsulator public object o; } - public static object LoadWithType(string serialized) + public static T LoadWithType(string serialized) { var tne = JsonSerializer.Deserialize(serialized, SerializerOptions); - // in the case of trying to deserialize nothing, tne will be nothing - // we want to return nothing - return tne?.o; + if (tne?.o is JsonElement jsonElement) + return jsonElement.Deserialize(SerializerOptions); + + return default; + } + + public static object LoadWithType(string serialized, Type deserializedType) + { + var tne = JsonSerializer.Deserialize(serialized, SerializerOptions); + + if (tne?.o is JsonElement jsonElement) + return jsonElement.Deserialize(deserializedType, SerializerOptions); + + return null; } public static string SaveWithType(object o) diff --git a/src/BizHawk.Client.Common/savestates/SavestateFile.cs b/src/BizHawk.Client.Common/savestates/SavestateFile.cs index a2af4d74a1f..a5c86d7be5c 100644 --- a/src/BizHawk.Client.Common/savestates/SavestateFile.cs +++ b/src/BizHawk.Client.Common/savestates/SavestateFile.cs @@ -181,7 +181,7 @@ public bool Load(string path, IDialogParent dialogParent) if (!string.IsNullOrWhiteSpace(userData)) { - var bag = (Dictionary)ConfigService.LoadWithType(userData); + var bag = ConfigService.LoadWithType>(userData); _userBag.Clear(); foreach (var (k, v) in bag) _userBag.Add(k, v); } diff --git a/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/InputRoll.cs b/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/InputRoll.cs index c1da1815617..f06559732c7 100644 --- a/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/InputRoll.cs +++ b/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/InputRoll.cs @@ -659,16 +659,19 @@ public string UserSettingsSerialized() public void LoadSettingsSerialized(string settingsJson) { - var settings = ConfigService.LoadWithType(settingsJson); - - // TODO: don't silently fail, inform the user somehow - if (settings is InputRollSettings rollSettings) + try { - _columns = rollSettings.Columns; + InputRollSettings settings = ConfigService.LoadWithType(settingsJson); + + _columns = settings.Columns; _columns.ChangedCallback = ColumnChangedCallback; - HorizontalOrientation = rollSettings.HorizontalOrientation; - LagFramesToHide = rollSettings.LagFramesToHide; - HideWasLagFrames = rollSettings.HideWasLagFrames; + HorizontalOrientation = settings.HorizontalOrientation; + LagFramesToHide = settings.LagFramesToHide; + HideWasLagFrames = settings.HideWasLagFrames; + } + catch + { + // TODO: don't silently fail, inform the user somehow } } diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 21f53d04efa..7486ad5ccb5 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -2365,7 +2365,7 @@ private void CoreSyncSettings(object sender, RomLoader.SettingsLoadArgs e) { if (!string.IsNullOrWhiteSpace(MovieSession.QueuedSyncSettings)) { - e.Settings = ConfigService.LoadWithType(MovieSession.QueuedSyncSettings); + e.Settings = ConfigService.LoadWithType(MovieSession.QueuedSyncSettings, e.SettingsType); } else { diff --git a/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs b/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs index 1c74d610344..fd46a934e35 100644 --- a/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs +++ b/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs @@ -572,7 +572,7 @@ private bool LoadBotFile(string path) BotData botData; try { - botData = (BotData) ConfigService.LoadWithType(File.ReadAllText(path)); + botData = ConfigService.LoadWithType(File.ReadAllText(path)); } catch (Exception e) { diff --git a/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs b/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs index df7fc3acb63..2be0cd7ad58 100644 --- a/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs +++ b/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs @@ -83,16 +83,11 @@ static void CheckAll(string? groupDesc = null) CheckAll(); } - private static readonly JsonSerializerOptions SerializerOptions = new(ConfigService.SerializerOptions) - { - WriteIndented = false - }; - [TestMethod] public void TestRoundTripSerialization() { - static object? Deser(string s, Type type) => JsonSerializer.Deserialize(s, type, SerializerOptions); - static string Ser(object? o) => JsonSerializer.Serialize(o, SerializerOptions); + static object? Deser(string s, Type type) => JsonSerializer.Deserialize(s, type, ConfigService.SerializerOptions); + static string Ser(object? o) => JsonSerializer.Serialize(o, ConfigService.SerializerOptions); foreach (var (type, s) in KnownGoodFromBizHawk) { From 48545bf9cdb761d9c9101b5334ee8dce3f8d8905 Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Sat, 1 Jun 2024 14:25:37 +0200 Subject: [PATCH 3/8] Move JsonConverters, support TypeConverter, fix custom tool settings load --- .../config/ConfigService.cs | 25 ++------- .../tools/ToolManager.cs | 18 ++----- .../{ => Json}/Array2DJsonConverter.cs | 2 +- .../ByteArrayAsNormalArrayJsonConverter.cs | 2 +- .../Json/FloatConverter.cs | 22 ++++++++ .../Json/TypeConverterJsonAdapter.cs | 52 +++++++++++++++++++ .../Consoles/Nintendo/NES/NES.ISettable.cs | 1 + 7 files changed, 84 insertions(+), 38 deletions(-) rename src/BizHawk.Emulation.Common/{ => Json}/Array2DJsonConverter.cs (98%) rename src/BizHawk.Emulation.Common/{ => Json}/ByteArrayAsNormalArrayJsonConverter.cs (97%) create mode 100644 src/BizHawk.Emulation.Common/Json/FloatConverter.cs create mode 100644 src/BizHawk.Emulation.Common/Json/TypeConverterJsonAdapter.cs diff --git a/src/BizHawk.Client.Common/config/ConfigService.cs b/src/BizHawk.Client.Common/config/ConfigService.cs index 7706b4bc123..df04bb247b5 100644 --- a/src/BizHawk.Client.Common/config/ConfigService.cs +++ b/src/BizHawk.Client.Common/config/ConfigService.cs @@ -1,31 +1,13 @@ -using System.Globalization; using System.IO; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using BizHawk.Common; -using BizHawk.Emulation.Common; +using BizHawk.Emulation.Common.Json; namespace BizHawk.Client.Common { - internal class FloatConverter : JsonConverter - { - public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetSingle(); - - public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) - { -#if NETCOREAPP - writer.WriteNumberValue(value); -#else - // gotta love the fact .net framework can't even format floats correctly by default - // can't use G7 here because it may be too low accuracy, and can't use G8 because it may be too high, see 1.0000003f or 0.8f - writer.WriteRawValue(value.ToString("R", NumberFormatInfo.InvariantInfo)); -#endif - } - } - public static class ConfigService { private static readonly JsonSerializerOptions NonIndentedSerializerOptions = new() @@ -36,7 +18,8 @@ public static class ConfigService Converters = { new FloatConverter(), // this serializes floats with minimum required precision, e.g. 1.8000000012 -> 1.8 - new ByteArrayAsNormalArrayJsonConverter() // this preserves the old behaviour of e,g, 0x1234ABCD --> [18,52,171,205]; omitting it will use base64 e.g. "EjSrzQ==" + new ByteArrayAsNormalArrayJsonConverter(), // this preserves the old behaviour of e,g, 0x1234ABCD --> [18,52,171,205]; omitting it will use base64 e.g. "EjSrzQ==" + new TypeConverterJsonAdapterFactory(), // allows serialization using `[TypeConverter]` attributes } }; @@ -45,7 +28,7 @@ public static class ConfigService WriteIndented = true }; - internal static JsonSerializerOptions SerializerOptions => NonIndentedSerializerOptions; + public static JsonSerializerOptions SerializerOptions => NonIndentedSerializerOptions; public static bool IsFromSameVersion(string filepath, out string msg) { diff --git a/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs b/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs index 3b2915f026f..6a017377237 100644 --- a/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs +++ b/src/BizHawk.Client.EmuHawk/tools/ToolManager.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; -using System.ComponentModel; +using System.Text.Json; using System.Windows.Forms; using BizHawk.Client.Common; @@ -401,21 +401,9 @@ private static void InstallCustomConfig(IToolForm tool, Dictionary : JsonConverter diff --git a/src/BizHawk.Emulation.Common/ByteArrayAsNormalArrayJsonConverter.cs b/src/BizHawk.Emulation.Common/Json/ByteArrayAsNormalArrayJsonConverter.cs similarity index 97% rename from src/BizHawk.Emulation.Common/ByteArrayAsNormalArrayJsonConverter.cs rename to src/BizHawk.Emulation.Common/Json/ByteArrayAsNormalArrayJsonConverter.cs index 1ecae3cde73..0011fa9014f 100644 --- a/src/BizHawk.Emulation.Common/ByteArrayAsNormalArrayJsonConverter.cs +++ b/src/BizHawk.Emulation.Common/Json/ByteArrayAsNormalArrayJsonConverter.cs @@ -2,7 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace BizHawk.Emulation.Common +namespace BizHawk.Emulation.Common.Json { /// based on this SO answer public sealed class ByteArrayAsNormalArrayJsonConverter : JsonConverter diff --git a/src/BizHawk.Emulation.Common/Json/FloatConverter.cs b/src/BizHawk.Emulation.Common/Json/FloatConverter.cs new file mode 100644 index 00000000000..106d3e64b1a --- /dev/null +++ b/src/BizHawk.Emulation.Common/Json/FloatConverter.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BizHawk.Emulation.Common.Json +{ + public class FloatConverter : JsonConverter + { + public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetSingle(); + + public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) + { +#if NETCOREAPP + writer.WriteNumberValue(value); +#else + // gotta love the fact .net framework can't even format floats correctly by default + // can't use G7 here because it may be too low accuracy, and can't use G8 because it may be too high, see 1.0000003f or 0.8f + writer.WriteRawValue(value.ToString("R", NumberFormatInfo.InvariantInfo)); +#endif + } + } +} diff --git a/src/BizHawk.Emulation.Common/Json/TypeConverterJsonAdapter.cs b/src/BizHawk.Emulation.Common/Json/TypeConverterJsonAdapter.cs new file mode 100644 index 00000000000..75562c46d2d --- /dev/null +++ b/src/BizHawk.Emulation.Common/Json/TypeConverterJsonAdapter.cs @@ -0,0 +1,52 @@ +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +// System.Text.Json does not respect `[TypeConverter]`s by default, so it's required to use a JsonConverter that acts as an adapter +// see also https://github.com/dotnet/runtime/issues/38812 or https://github.com/dotnet/runtime/issues/1761 +namespace BizHawk.Emulation.Common.Json +{ + /// + /// Adapter between and . + /// + public class TypeConverterJsonAdapter : JsonConverter + { + /// + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var converter = TypeDescriptor.GetConverter(typeToConvert); + return (T)converter.ConvertFromString(reader.GetString())!; + } + + /// + public override void Write(Utf8JsonWriter writer, T objectToWrite, JsonSerializerOptions options) + { + var converter = TypeDescriptor.GetConverter(objectToWrite!); + writer.WriteStringValue(converter.ConvertToString(objectToWrite!)); + } + + /// + public override bool CanConvert(Type typeToConvert) => typeToConvert.GetCustomAttributes(inherit: true).Any(); + } + + /// + public class TypeConverterJsonAdapter : TypeConverterJsonAdapter; + + /// + /// A factory used to create various instances. + /// + public class TypeConverterJsonAdapterFactory : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) => typeToConvert.GetCustomAttributes(inherit: true).Any(); + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var converterType = typeof(TypeConverterJsonAdapter<>).MakeGenericType(typeToConvert); + return (JsonConverter)Activator.CreateInstance(converterType); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.ISettable.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.ISettable.cs index 7f2ab8c1b8f..44134ee8ca2 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.ISettable.cs @@ -4,6 +4,7 @@ using BizHawk.Common; using BizHawk.Emulation.Common; +using BizHawk.Emulation.Common.Json; namespace BizHawk.Emulation.Cores.Nintendo.NES { From edcae6b3dd1811c944aae97a923ad43b5452d087 Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Sat, 1 Jun 2024 19:00:46 +0200 Subject: [PATCH 4/8] Fix RollColumn deserialization --- .../CustomControls/InputRoll/RollColumn.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/RollColumn.cs b/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/RollColumn.cs index d64b47acae2..df67836dede 100644 --- a/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/RollColumn.cs +++ b/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/RollColumn.cs @@ -1,5 +1,7 @@ #nullable enable +using System.Text.Json.Serialization; + namespace BizHawk.Client.EmuHawk { public class RollColumn @@ -28,7 +30,7 @@ public class RollColumn /// public bool Rotatable { get; set; } -// [JsonConstructor] + [JsonConstructor] private RollColumn() { Name = default!; From f4b0238fd6cb6932815bded78257bfa94fea128f Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Sat, 1 Jun 2024 19:53:02 +0200 Subject: [PATCH 5/8] More deserialization fixes --- .../CustomControls/InputRoll/RollColumn.cs | 7 ++++--- .../tools/TAStudio/TAStudioPalette.cs | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/RollColumn.cs b/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/RollColumn.cs index df67836dede..ed72b528696 100644 --- a/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/RollColumn.cs +++ b/src/BizHawk.Client.EmuHawk/CustomControls/InputRoll/RollColumn.cs @@ -31,10 +31,11 @@ public class RollColumn public bool Rotatable { get; set; } [JsonConstructor] - private RollColumn() + private RollColumn(string name, string text, ColumnType type) { - Name = default!; - Text = default!; + Name = name; + Text = text; + Type = type; } public RollColumn(string name, int widthUnscaled, ColumnType type, string text) diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioPalette.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioPalette.cs index 016c183830e..de18c1f2bc3 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioPalette.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioPalette.cs @@ -1,4 +1,5 @@ using System.Drawing; +using System.Text.Json.Serialization; namespace BizHawk.Client.EmuHawk { @@ -49,6 +50,7 @@ public readonly struct TAStudioPalette public readonly Color AnalogEdit_Col; + [JsonConstructor] public TAStudioPalette( // Color currentFrame_FrameCol, Color currentFrame_InputLog, From 49d6bbf20e2c4fc91ccf9b891ca90142f54a560d Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Thu, 6 Jun 2024 15:30:11 +0200 Subject: [PATCH 6/8] Fix tests and analyzers --- src/BizHawk.Client.Common/RecentFiles.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/BizHawk.Client.Common/RecentFiles.cs b/src/BizHawk.Client.Common/RecentFiles.cs index e0c092070f4..2b459ef4f37 100644 --- a/src/BizHawk.Client.Common/RecentFiles.cs +++ b/src/BizHawk.Client.Common/RecentFiles.cs @@ -6,6 +6,8 @@ namespace BizHawk.Client.Common { public class RecentFiles { + [JsonInclude] + [JsonPropertyOrder(-1)] // ReSharper disable once FieldCanBeMadeReadOnly.Local private List recentlist; From 77cda915d913c50a41a28125f9d6407c2a951e25 Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Fri, 7 Jun 2024 19:45:08 +0200 Subject: [PATCH 7/8] Add serialization tests for the added JsonConverters --- .../config/SerializationStabilityTests.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs b/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs index 2be0cd7ad58..51228e41e8d 100644 --- a/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs +++ b/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Drawing; +using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using BizHawk.Client.Common; using BizHawk.Common; +using BizHawk.Emulation.Common.Json; namespace BizHawk.Tests.Client.Common.config { @@ -95,5 +97,75 @@ public void TestRoundTripSerialization() Assert.AreEqual(s, Ser(Deser(s, type)), $"{type} failed serialization round-trip"); } } + + [TestMethod] + [DataRow("0.8")] + [DataRow("1.00000036")] + [DataRow("1.8")] + public void TestRoundTripSerializationFloatConverter(string floatValue) + { + float deserialized = JsonSerializer.Deserialize(floatValue, ConfigService.SerializerOptions); + string serialized = JsonSerializer.Serialize(deserialized, ConfigService.SerializerOptions); + Assert.AreEqual(floatValue, serialized); + } + + [TestMethod] + [DataRow("[1,2,3]")] + [DataRow("[]")] + [DataRow("null")] + [DataRow("[255,0,127,128,1]")] + public void TestRoundTripSerializationByteArrayConverter(string byteArrayValue) + { + byte[]? deserialized = JsonSerializer.Deserialize(byteArrayValue, ConfigService.SerializerOptions); + string serialized = JsonSerializer.Serialize(deserialized, ConfigService.SerializerOptions); + Assert.AreEqual(byteArrayValue, serialized); + } + + [TestMethod] + public void TestSerializationTypeConverter() + { + var color = Color.FromArgb(200, 255, 13, 42); + string serialized = JsonSerializer.Serialize(color, ConfigService.SerializerOptions); + Assert.AreEqual("\"200; 255; 13; 42\"", serialized); + + var newColor = JsonSerializer.Deserialize(serialized, ConfigService.SerializerOptions); + Assert.AreEqual(color, newColor); + } + + private static bool Equals(T[,] array1, T[,] array2) + { + return array1.Rank == array2.Rank + && Enumerable.Range(0, array1.Rank).All(dimension => array1.GetLength(dimension) == array2.GetLength(dimension)) + && array1.Cast().SequenceEqual(array2.Cast()); + } + + [TestMethod] + public void TestSerialization2DArrayConverter() + { + var options = new JsonSerializerOptions + { + Converters = { new Array2DJsonConverter() } + }; + var optionsWithByteArrayConverter = new JsonSerializerOptions + { + Converters = { new Array2DJsonConverter(), new ByteArrayAsNormalArrayJsonConverter() } + }; + + byte[,] testByteArray = + { + { 1, 2, 3 }, + { 255, 0, 128 } + }; + + string serialized = JsonSerializer.Serialize(testByteArray, options); + Assert.AreEqual("[\"AQID\",\"/wCA\"]", serialized); + byte[,] deserialized = JsonSerializer.Deserialize(serialized, options)!; + Assert.IsTrue(Equals(testByteArray, deserialized)); + + string serialized2 = JsonSerializer.Serialize(testByteArray, optionsWithByteArrayConverter); + Assert.AreEqual("[[1,2,3],[255,0,128]]", serialized2); + byte[,] deserialized2 = JsonSerializer.Deserialize(serialized2, optionsWithByteArrayConverter)!; + Assert.IsTrue(Equals(testByteArray, deserialized2)); + } } } From ad4ad58540f631d6eae867d5d70d22da5dd72af8 Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Fri, 7 Jun 2024 20:11:05 +0200 Subject: [PATCH 8/8] Fix typeconverter culture handling I guess tests are actually useful? --- .../Json/TypeConverterJsonAdapter.cs | 7 ++++--- .../Client.Common/config/SerializationStabilityTests.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/BizHawk.Emulation.Common/Json/TypeConverterJsonAdapter.cs b/src/BizHawk.Emulation.Common/Json/TypeConverterJsonAdapter.cs index 75562c46d2d..21b10cb8522 100644 --- a/src/BizHawk.Emulation.Common/Json/TypeConverterJsonAdapter.cs +++ b/src/BizHawk.Emulation.Common/Json/TypeConverterJsonAdapter.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Reflection; using System.Text.Json; @@ -17,14 +18,14 @@ public class TypeConverterJsonAdapter : JsonConverter public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var converter = TypeDescriptor.GetConverter(typeToConvert); - return (T)converter.ConvertFromString(reader.GetString())!; + return (T)converter.ConvertFromString(null!, CultureInfo.InvariantCulture, reader.GetString())!; } /// public override void Write(Utf8JsonWriter writer, T objectToWrite, JsonSerializerOptions options) { var converter = TypeDescriptor.GetConverter(objectToWrite!); - writer.WriteStringValue(converter.ConvertToString(objectToWrite!)); + writer.WriteStringValue(converter.ConvertToString(null!, CultureInfo.InvariantCulture, objectToWrite!)); } /// diff --git a/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs b/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs index 51228e41e8d..c4104245a92 100644 --- a/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs +++ b/src/BizHawk.Tests/Client.Common/config/SerializationStabilityTests.cs @@ -126,7 +126,7 @@ public void TestSerializationTypeConverter() { var color = Color.FromArgb(200, 255, 13, 42); string serialized = JsonSerializer.Serialize(color, ConfigService.SerializerOptions); - Assert.AreEqual("\"200; 255; 13; 42\"", serialized); + Assert.AreEqual("\"200, 255, 13, 42\"", serialized); var newColor = JsonSerializer.Deserialize(serialized, ConfigService.SerializerOptions); Assert.AreEqual(color, newColor);