Editor/Data/ArchUnitConverter.cs
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Sandbox;
namespace Sunless.Architecture;
// Reads a top-level unit into whatever type claims its kind, and into a bare carrier when nothing does.
//
// [JsonPolymorphic] cannot do this job: JsonDerivedType is an attribute on the BASE type, so core would have to
// name every module's unit type, and System.Text.Json throws on a discriminator it does not recognise with no
// read-side equivalent of JsonUnknownDerivedTypeHandling. After the split an unrecognised kind is the ordinary
// not-installed case, and throwing there means the plan reads null and the next commit writes an empty one over it.
public sealed class ArchUnitConverter : JsonConverter<ArchUnit> {
// Only the base: asking for a subclass must deserialize normally or this converter calls itself forever.
public override bool CanConvert( Type type ) => type == typeof( ArchUnit );
public override ArchUnit Read( ref Utf8JsonReader reader, Type type, JsonSerializerOptions options ) {
using var document = JsonDocument.ParseValue( ref reader );
var element = document.RootElement;
var claimed = ArchKinds.Load().For( KindOf( element ) );
var resolved = claimed?.Payload;
if ( resolved is null || !typeof( ArchUnit ).IsAssignableFrom( resolved ) ) {
return Opaque( element, options );
}
return JsonSerializer.Deserialize( element.GetRawText(), resolved, options ) as ArchUnit;
}
public override void Write( Utf8JsonWriter writer, ArchUnit value, JsonSerializerOptions options ) {
// A unit built in memory by something that forgot to say what it is would otherwise go to disk unnamed and
// read back opaque, which loses it on the round trip it was meant to survive.
if ( !value.Kind.Known && ArchKinds.Load().Claiming( value.GetType() ) is { } manifest ) {
value.Kind = manifest.Kind;
}
JsonSerializer.Serialize( writer, value, value.GetType(), options );
}
// Version 3 wrote a lower-case polymorphic discriminator; every kind elsewhere in the document is the
// capitalised ident, so the two spellings meet here rather than anywhere a module can see.
static ArchKind KindOf( JsonElement element ) {
if ( element.TryGetProperty( "Kind", out var named ) && named.ValueKind == JsonValueKind.String ) {
return new ArchKind( named.GetString() ?? "" );
}
if ( !element.TryGetProperty( "$kind", out var legacy ) || legacy.ValueKind != JsonValueKind.String ) {
return ArchKind.None;
}
return legacy.GetString() switch {
"building" => ArchKind.Building,
"road" => ArchKind.Road,
var other => new ArchKind( other ?? "" )
};
}
static ArchUnit Opaque( JsonElement element, JsonSerializerOptions options ) {
var carrier = JsonSerializer.Deserialize<ArchOpaqueUnit>( element.GetRawText(), options ) ?? new ArchOpaqueUnit();
carrier.Kind = KindOf( element );
return carrier;
}
}