Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion ResoniteModLoader/DebugInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ internal static void Log() {
Logger.DebugInternal($"Using \"{Assembly.GetEntryAssembly()?.GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkName}\"");
Logger.MsgInternal($".NET Runtime: {RuntimeInformation.FrameworkDescription} on {RuntimeInformation.RuntimeIdentifier}");
Logger.MsgInternal($"Using Harmony v{GetAssemblyVersion(typeof(HarmonyLib.Harmony))}");
Logger.MsgInternal($"Using Json.NET v{GetAssemblyVersion(typeof(Newtonsoft.Json.JsonSerializer))}");
}

private static string? GetAssemblyVersion(Type typeFromAssembly) {
Expand Down
41 changes: 41 additions & 0 deletions ResoniteModLoader/JsonConverters/DynamicJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ResoniteModLoader.JsonConverters;

// Utility class for dynamically calling JSON converters
internal static class DynamicJsonConverter {
private delegate object? ReadDelegate(ref Utf8JsonReader reader, JsonSerializerOptions options);

internal static object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
var converterType = typeof(InnerConverter<>).MakeGenericType([typeToConvert]);
var read = converterType
.GetMethod(nameof(InnerConverter<>.Read))!
.CreateDelegate<ReadDelegate>();

return read(ref reader, options);
}

private delegate void WriteDelegate(Utf8JsonWriter writer, object? value, JsonSerializerOptions options);

internal static void Write(Utf8JsonWriter writer, Type typeToConvert, object? value, JsonSerializerOptions options) {
var converterType = typeof(InnerConverter<>).MakeGenericType([typeToConvert]);
var write = converterType
.GetMethod(nameof(InnerConverter<>.Write))!
.CreateDelegate<WriteDelegate>();

write(writer, value, options);
}

private static class InnerConverter<T> {
public static object? Read(ref Utf8JsonReader reader, JsonSerializerOptions options) {
var converter = (JsonConverter<T>)options.GetConverter(typeof(T));
return converter.Read(ref reader, typeof(T), options);
}

public static void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) {
var converter = (JsonConverter<T>)options.GetConverter(typeof(T));
converter.Write(writer, (T)value!, options);
}
}
}
128 changes: 102 additions & 26 deletions ResoniteModLoader/JsonConverters/EnumConverter.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,117 @@
using Newtonsoft.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ResoniteModLoader.JsonConverters;

// serializes and deserializes enums as strings
internal sealed class EnumConverter : JsonConverter {
public override bool CanConvert(Type objectType) {
return objectType.IsEnum;
internal sealed class EnumConverter : JsonConverterFactory {

public override bool CanConvert(Type typeToConvert) {
var notNullType = Nullable.GetUnderlyingType(typeToConvert);
return (notNullType ?? typeToConvert).IsEnum;
}

public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) {
// handle old behavior where enums were serialized as underlying type
Type underlyingType = Enum.GetUnderlyingType(objectType);
if (TryConvert(reader!.Value!, underlyingType, out object? deserialized)) {
Logger.DebugFuncInternal(() => $"Deserializing a Core Element type: {objectType} from a {reader!.Value!.GetType()}");
return deserialized!;
}
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
if (!CanConvert(typeToConvert))
throw new InvalidOperationException($"Cannot convert type {typeToConvert}");

// handle new behavior where enums are serialized as strings
if (reader.Value is string serialized) {
return Enum.Parse(objectType, serialized);
}
var notNullType = Nullable.GetUnderlyingType(typeToConvert);

throw new ArgumentException($"Could not deserialize a Core Element type: {objectType} from a {reader?.Value?.GetType()}. Expected underlying type was {underlyingType}");
var type = notNullType == null
? typeof(Inner<>).MakeGenericType([typeToConvert])
: typeof(InnerNullable<>).MakeGenericType([notNullType]);
return (JsonConverter?)Activator.CreateInstance(type);
}

public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) {
string? serialized = Enum.GetName(value!.GetType(), value);
writer.WriteValue(serialized);
private sealed class Inner<T> : JsonConverter<T> where T : struct, Enum {
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
switch (reader.TokenType) {
case JsonTokenType.Null:
return default;

case JsonTokenType.String: {
var value = reader.GetString()!;
if (Enum.TryParse<T>(value, false, out T result))
return result;
else
throw new JsonException(
$"{typeToConvert} does not have a variant '{value}'"
);
}

// handle old behavior where enums were serialized as underlying type
case JsonTokenType.Number: {
Type underlyingType = Enum.GetUnderlyingType(typeToConvert);
if (underlyingType == typeof(ulong)) {
// Edge case: ulong can represent more positive values than long
var value = reader.GetUInt64();
return (T)Enum.ToObject(typeof(T), value);
}
else {
var value = reader.GetInt64();
return (T)Enum.ToObject(typeof(T), value);
}
}

default:
throw new JsonException(
$"Expected string or number when parsing {typeToConvert}, found {reader.TokenType}"
);
}
}

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) {
string? serialized = Enum.GetName<T>(value);
writer.WriteStringValue(serialized);
}
}

private static bool TryConvert(object value, Type newType, out object? converted) {
try {
converted = Convert.ChangeType(value, newType);
return true;
} catch {
converted = null;
return false;
private sealed class InnerNullable<T> : JsonConverter<T?> where T : struct, Enum {
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
switch (reader.TokenType) {
case JsonTokenType.Null:
return null;

case JsonTokenType.String: {
var value = reader.GetString()!;
if (Enum.TryParse<T>(value, false, out T result))
return result;
else
throw new JsonException(
$"{typeToConvert} does not have a variant '{value}'"
);
}

// handle old behavior where enums were serialized as underlying type
case JsonTokenType.Number: {
Type underlyingType = Enum.GetUnderlyingType(typeToConvert);
if (underlyingType == typeof(ulong)) {
// Edge case: ulong can represent more positive values than long
var value = reader.GetUInt64();
return (T?)Enum.ToObject(typeof(T), value);
}
else {
var value = reader.GetInt64();
return (T?)Enum.ToObject(typeof(T), value);
}
}

default:
throw new JsonException(
$"Expected string or number when parsing {typeToConvert}, found {reader.TokenType}"
);
}
}

public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) {
if (value == null) {
writer.WriteNullValue();
}
else {
string? serialized = Enum.GetName<T>((T)value);
writer.WriteStringValue(serialized);
}
}
}

}
47 changes: 32 additions & 15 deletions ResoniteModLoader/JsonConverters/ResonitePrimitiveConverter.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@
using Elements.Core;
using System.Text.Json;
using System.Text.Json.Serialization;

using Newtonsoft.Json;
using Elements.Core;

namespace ResoniteModLoader.JsonConverters;

internal sealed class ResonitePrimitiveConverter : JsonConverter {
internal sealed class ResonitePrimitiveConverter : JsonConverterFactory {

private static readonly Assembly ElementsCore = typeof(floatQ).Assembly;

public override bool CanConvert(Type objectType) {
// handle all non-enum Resonite Primitives in the Elements.Core assembly
return !objectType.IsEnum && ElementsCore.Equals(objectType.Assembly) && Coder.IsEnginePrimitive(objectType);
public override bool CanConvert(Type typeToConvert)
=> !typeToConvert.IsEnum
&& ElementsCore.Equals(typeToConvert.Assembly)
&& Coder.IsEnginePrimitive(typeToConvert);

public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
if (!CanConvert(typeToConvert))
throw new InvalidOperationException($"Cannot convert type {typeToConvert}");

var type = typeof(Inner<>).MakeGenericType([typeToConvert]);
return (JsonConverter?)Activator.CreateInstance(type);
}

public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) {
if (reader.Value is string serialized) {
// use Resonite's built-in decoding if the value was serialized as a string
return typeof(Coder<>).MakeGenericType(objectType).GetMethod("DecodeFromString")!.Invoke(null, [serialized])!;
private sealed class Inner<T> : JsonConverter<T> {
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
var value = reader.GetString();
if (value == null)
return default;

return Coder<T>.DecodeFromString(value);
}

throw new ArgumentException($"Could not deserialize a Core Element type: {objectType} from a {reader?.Value?.GetType()}");
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) {
if (value == null) {
writer.WriteNullValue();
}
else {
var serialized = Coder<T>.EncodeToString(value);
writer.WriteStringValue(serialized);
}
}
}

public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) {
string serialized = (string)typeof(Coder<>).MakeGenericType(value!.GetType()).GetMethod("EncodeToString")!.Invoke(null, [value])!;
writer.WriteValue(serialized);
}
}
Loading