Skip to content

Instantly share code, notes, and snippets.

@spacechase0
Last active January 25, 2021 01:07
Show Gist options
  • Save spacechase0/feb87d5f6ea1d19adc212d58ad6c3e5e to your computer and use it in GitHub Desktop.
Save spacechase0/feb87d5f6ea1d19adc212d58ad6c3e5e to your computer and use it in GitHub Desktop.
Net type serialization
namespace StardewModdingAPI.Toolkit.Serialization
{
public class MyNetConverter : JsonConverter
{
public override bool CanRead => true;
public override bool CanWrite => true;
public override bool CanConvert( Type objectType )
{
//Console.WriteLine( "checking " + objectType );
return typeof( AbstractNetSerializable ).IsAssignableFrom( objectType );
}
// I thought you didn't need this sort of thing for inherited fields? Sigh...
private FieldInfo FindField( Type type, string fieldName )
{
var field = type.GetField( fieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance );
if ( field == null )
{
if ( type.BaseType != null )
return this.FindField( type.BaseType, fieldName );
return null;
}
return field;
}
public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer )
{
//Console.WriteLine( " Doing " + objectType + " " + existingValue + "!" );
// For some reason dictionaries aren't behaving like other things
if ( objectType.Name.Contains( "Dictionary" ) ) // Alternatively could whitelist the dictionary types
{
var field1 = this.FindField( objectType, "dict" );
var field2 = this.FindField( objectType, "dictReassigns" );
object dict = serializer.Deserialize( reader, field1.GetValue( existingValue ).GetType() );
field1.SetValue( existingValue, dict );
var dictR = (System.Collections.IDictionary) field2.GetValue( existingValue );
foreach ( object entry in ( ( System.Collections.IDictionary ) dict ).Keys )
dictR.Add( entry, new NetVersion() );
return existingValue;
}
AbstractNetSerializable ret = ( AbstractNetSerializable ) serializer.Deserialize( reader, objectType );
var existing = existingValue as AbstractNetSerializable;
using MemoryStream stream = new MemoryStream();
using BinaryWriter writer = new BinaryWriter(stream);
using BinaryReader reader_ = new BinaryReader(stream);
ret.WriteFull( writer );
stream.Seek( 0L, SeekOrigin.Begin );
existing.ReadFull( reader_, new NetClock().netVersion );
existing.MarkClean();
return existingValue;
}
public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
{
if ( value != null )
{
var type = value.GetType();
// For some reason dictionaries aren't behaving like other things
if ( type.Name.Contains( "Dictionary" ) ) // Alternatively could whitelist the dictionary types
{
object values = this.FindField( type, "dict" ).GetValue( value );
serializer.Serialize( writer, values );
return;
}
}
serializer.Serialize( writer, value );
}
}
public class MyContractResolver : DefaultContractResolver
{
private bool IsNetType( Type type )
{
return typeof( AbstractNetSerializable ).IsAssignableFrom( type );
}
protected override JsonContract CreateContract( Type objectType )
{
// Force net types to be objects and not convert as `IEnumerable<>`s
if ( this.IsNetType( objectType ) )
{
return this.CreateObjectContract( objectType );
}
//else Console.WriteLine( "not net " + objectType );
return base.CreateContract( objectType );
}
protected override IList<JsonProperty> CreateProperties( Type type, MemberSerialization memberSerialization )
{
string[] netBlacklist = new string[]
{
// AbstractNetSerializable
"dirtyTick", "minNextDirtyTime", "ChangeVersion", "DeltaAggregateTicks",
"needsTick", "childNeedsTick", "parent", "DirtyTick", "Dirty", "NeedsTick",
"ChildNeedsTick", "Root", "Parent", "NetFields",
// NetFieldBase
"_bools", "interpolationStartTick", "previousValue"/*, "targetValue"*/, "InterpolationEnabled",
"ExtrapolationEnabled", "InterpolationWait", "notifyOnTargetValueChange", "TargetValue",
"Value", "fieldChangeEvent", "fieldChangeVisibleEvent",
// NetField, and various other classes have this
"xmlInitialized",
// NetDictionary
"InterpolationWait", "dictReassigns", "outgoingChanges", "incomingChanges",
"IsReadOnly", "Keys", "Values", "Pairs", "FieldDict", "OnValueAdded",
"OnValueRemoved", "OnValueTargetUpdated", "OnConflictResolve", "Item" /* operator [] */,
// NetIntDelta
"networkValue", "DirtyThreshold", "Minimum", "Maximum",
// NetString
"Length", "FilterStringEvent",
// NetRefBase
"Serializer", "deltaType", "reassigned", "OnConflictResolve",
// NetRectangle
"X", "Y", "Width", "Height", "Center", "Top", "Bottom", "Left", "Right",
// NetPoint
"X", "Y",
// NetObjectShrinkList
"Count", "IsReadOnly",
// NetArray
"appendPosition", "Fields", "Count", "Length", "IsReadOnly", "IsFixedSize",
"OnFieldCreate",
// NetList
"initialSize", "resizeFactor", "Count", "Capacity", "IsReadOnly", "OnElementChanged",
"OnArrayReplaced",
// NetVector2
"AxisAlignedMovement", "ExtrapolationSpeed", "MinDeltaForDirectionChange",
"MaxInterpolationDistance", "interpolateXFirst", "isExtrapolating", "isFixingExtrapolation",
"X", "Y",
// NetCollection
"Count", "ISFixedSize", "IsReadOnly", "InterpolationWait", "OnValueAdded",
"OnValueRemoved",
// NetPosition
"SmoothingFudge", "DefaultDeltaAggragateTicks", "ExtrapolationEnabled", "X", "Y",
// NetLocationRef
"_dirty", "_usedLocalLocation", "_gameLocation",
// NetFarmerRef
"UID", "Value",
// NetLocationRef
"Value",
// "NetBuildingRef
"Value",
};
bool isNetType = this.IsNetType( type );
//Console.WriteLine( "test: " + isNetType + " " + type );
var members = this.GetSerializableMembers( type );
var props = new JsonPropertyCollection( type );
foreach ( var member in members )
{
if ( member.GetCustomAttribute( typeof( XmlIgnoreAttribute ) ) != null )
{
continue;
}
//Console.WriteLine( "checking prop " + member );
Type memType = null;
string memName = null;
if ( member is FieldInfo field )
{
memType = field.FieldType;
memName = field.Name;
}
else if ( member is PropertyInfo property )
{
memType = property.PropertyType;
memName = property.Name;
}
if ( isNetType )
{
if ( netBlacklist.Contains( memName ) )
{
continue;
}
}
//Console.WriteLine( "\tdoing prop " + member );
var prop = this.CreateProperty( member, MemberSerialization.Fields );
if ( prop.Ignored )
continue;
if ( prop.PropertyName == "value" || prop.PropertyName == "targetValue" )
prop.TypeNameHandling = TypeNameHandling.Objects;
if ( isNetType )
{
if ( type.Name.StartsWith( "NetRef" ) && prop.PropertyName == "value" ||
!type.Name.StartsWith( "NetRef" ) && prop.PropertyName == "targetValue" )
continue;
}
//Console.WriteLine( "memtype:" + memType );
if ( this.IsNetType( memType ) )
{
//Console.WriteLine( "net type! " + memType );
prop.Converter = prop.ItemConverter = new MyNetConverter();
}
//Console.WriteLine( $"\t\tACTUALLY doing prop {prop.Writable} {prop.Readable}" );
props.Add( prop );
}
return props.OrderBy(p=>p.Order??-1).ToList();
}
protected override List<MemberInfo> GetSerializableMembers( Type objectType )
{
bool isNetType = this.IsNetType( objectType );
if ( isNetType )
{
List<MemberInfo> members = new List<MemberInfo>();
foreach ( var field in objectType.GetFields( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ) )
members.Add( field );
foreach ( var prop in objectType.GetProperties( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ) )
members.Add( prop );
return members;
}
return base.GetSerializableMembers( objectType );
}
}
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
public class JsonHelper
{
/*********
** Accessors
*********/
/// <summary>The JSON settings to use when serializing and deserializing files.</summary>
public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded
Converters = new List<JsonConverter>
{
new SemanticVersionConverter(),
new StringEnumConverter(),
},
ContractResolver = new MyContractResolver(),
ReferenceLoopHandling = ReferenceLoopHandling.Serialize, // For net types
};
// rest of the file here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment