Skip to content

Commit

Permalink
Feat: Simplify a lot serializable type registering
Browse files Browse the repository at this point in the history
  • Loading branch information
tornac1234 committed Feb 28, 2024
1 parent 91bcbac commit 0b97c7d
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 167 deletions.
76 changes: 2 additions & 74 deletions Example mod/SerializableBehaviourExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,10 @@ namespace Nautilus.Examples;
[BepInDependency("com.snmodding.nautilus")]
internal class SerializableBehaviourExample : BaseUnityPlugin
{
// You need to store the generated methods so they can be used in your serialize and deserialize static methods
public static Action<RealtimeCounter, int, ProtoWriter> realtimeCounterSerializeMethodInfo;
public static Func<RealtimeCounter, ProtoReader, RealtimeCounter> realtimeCounterDeserializeMethodInfo;
public static Action<StopwatchSignExample, int, ProtoWriter> stopwatchSignExampleSerializeMethodInfo;
public static Func<StopwatchSignExample, ProtoReader, StopwatchSignExample> stopwatchSignExampleDeserializeMethodInfo;

private void Awake()
{
// We first generate automatically the serialize and deserialize methods for our serializable type
var realtimeCounterMethodInfos = ProtobufSerializerHandler.GenerateSerializeAndDeserializeMethods<RealtimeCounter>();
// And we store them somewhere easily accessible
realtimeCounterSerializeMethodInfo = realtimeCounterMethodInfos.Item1;
realtimeCounterDeserializeMethodInfo = realtimeCounterMethodInfos.Item2;

// Do it for the stopwatch
var stopwatchSignMethodInfos = ProtobufSerializerHandler.GenerateSerializeAndDeserializeMethods<StopwatchSignExample>();
stopwatchSignExampleSerializeMethodInfo = stopwatchSignMethodInfos.Item1;
stopwatchSignExampleDeserializeMethodInfo = stopwatchSignMethodInfos.Item2;

// Don't forget to register your serializable type with the right serialize and deserialize method (those which you created manually)
// You can pick whatever type ID (e.g. 3141592) you want as long it's not used by another mod or by Subnautica itself
ProtobufSerializerHandler.RegisterSerializableType<RealtimeCounter>(3141592, RealtimeCounter.Serialize, RealtimeCounter.Deserialize);
ProtobufSerializerHandler.RegisterSerializableType<StopwatchSignExample>(3141593, StopwatchSignExample.Serialize, StopwatchSignExample.Deserialize);
// Don't forget to register your serializable types
ProtobufSerializerHandler.RegisterAllSerializableTypesInAssembly();

var stopwatchSign = new CustomPrefab(PrefabInfo.WithTechType("StopwatchSign"));
var stopwatchSignTemplate = new CloneTemplate(stopwatchSign.Info, TechType.Sign);
Expand Down Expand Up @@ -91,31 +72,6 @@ public void OnProtoSerialize(ProtobufSerializer serializer)
StartedCounting = DateTimeOffset.UtcNow;
Debug.Log($"Serialized realtime counter with {totalRealtimeMs}ms [first launch at: {DateTimeOffset.FromUnixTimeMilliseconds(firstLaunchUnixTimeMs)}]");
}

// Below are the required static methods for Protobuf serializer to serialize and deserialize this MonoBehaviour
// You can find more examples in ProtobufSerializerPrecompiled just like Serialize11492366 and Deserialize11492366
// Serialize must be a "static void" with parameters (YourType, int, ProtoWriter)
public static void Serialize(RealtimeCounter realtimeCounter, int objTypeId, ProtoWriter writer)
{
// If you only have basic types you only need to use the generated methods but you may need to adapt this with more complex types
// In that situation, please pick the right WireType for your variables and adapt the following Write[Object] call

// In this case, we only have basic types to serialize so we just invoke the generated method
SerializableBehaviourExample.realtimeCounterSerializeMethodInfo.Invoke(realtimeCounter, objTypeId, writer);
}

// Deserialize must be a "static YourType" with parameters (YourType, ProtoReader)
public static RealtimeCounter Deserialize(RealtimeCounter realtimeCounter, ProtoReader reader)
{
// Here you need to use the methods Read[Object] accordingly to what you wrote in Serialize: Write[Object]
// Also the different cases must be using the same field numbers as defined in Serialize

// In this case, we only have basic types to serialize so we just invoke the generated method
realtimeCounter = SerializableBehaviourExample.realtimeCounterDeserializeMethodInfo.Invoke(realtimeCounter, reader);

// Return the same object you've been modifying
return realtimeCounter;
}
}

[ProtoContract]
Expand Down Expand Up @@ -146,42 +102,14 @@ private void Update()
timePassed += Time.deltaTime;
}

// Below are two listeners from the optional interface IProtoEventListener:
// OnProtoDeserialize happens after this MonoBehaviour's GameObject full hierarchy is deserialized
public void OnProtoDeserialize(ProtobufSerializer serializer)
{
deserializations++;
}

// OnProtoSerialize happens before this MonoBehaviour's GameObject full hierarchy is serialized
public void OnProtoSerialize(ProtobufSerializer serializer)
{
version = 1;
serializations++;
}

// Below are the required static methods for Protobuf serializer to serialize and deserialize this MonoBehaviour
// You can find more examples in ProtobufSerializerPrecompiled just like Serialize11492366 and Deserialize11492366
// Serialize must be a "static void" with parameters (YourType, int, ProtoWriter)
public static void Serialize(StopwatchSignExample stopwatchSignExample, int objTypeId, ProtoWriter writer)
{
// If you only have basic types you only need to use the generated methods but you may need to adapt this with more complex types
// In that situation, please pick the right WireType for your variables and adapt the following Write[Object] call

// In this case, we only have basic types to serialize so we just invoke the generated method
SerializableBehaviourExample.stopwatchSignExampleSerializeMethodInfo.Invoke(stopwatchSignExample, objTypeId, writer);
}

// Deserialize must be a "static YourType" with parameters (YourType, ProtoReader)
public static StopwatchSignExample Deserialize(StopwatchSignExample stopwatchSignExample, ProtoReader reader)
{
// Here you need to use the methods Read[Object] accordingly to what you wrote in Serialize: Write[Object]
// Also the different cases must be using the same field numbers as defined in Serialize

// In this case, we only have basic types to serialize so we just invoke the generated method
stopwatchSignExample = SerializableBehaviourExample.stopwatchSignExampleDeserializeMethodInfo.Invoke(stopwatchSignExample, reader);

// Return the same object you've been modifying
return stopwatchSignExample;
}
}
181 changes: 90 additions & 91 deletions Nautilus/Handlers/ProtobufSerializerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Nautilus.Handlers;
/// </summary>
public static class ProtobufSerializerHandler
{
internal static Dictionary<int, SerializerEntry> SerializerEntries;
internal static Dictionary<Type, SerializerEntry> SerializerEntries;

static ProtobufSerializerHandler()
{
Expand All @@ -38,14 +38,12 @@ private static void InitializeSerializerEntries(MethodInfo serializeMethodInfo)
{
// The castclass instruction is always 3 steps before the call instruction
Type objectType = (Type) serializeMethodInstructions[i - 3].Value;
// The ldc.i4 instruction is always 2 steps before the call instruction
int associatedTypeId = (int) serializeMethodInstructions[i - 2].Value;
// To get the methodName we remove the 5 first letters and we cut before the opening parenthese
string serializeMethodName = instruction.Value.ToString()[5..].Split('(')[0];
// The deserialize method name will be the same but with Deserialize[...] instead of Serialize[...]
string deserializeMethodName = serializeMethodName.Replace("S", "Des");

SerializerEntries.Add(associatedTypeId, new(objectType, serializeMethodName, deserializeMethodName));
SerializerEntries.Add(objectType, new(objectType, serializeMethodName, deserializeMethodName));
}
}
}
Expand All @@ -56,89 +54,113 @@ private static void InitializeSerializerEntries(MethodInfo serializeMethodInfo)
/// <remarks>
/// This is the only method from <see cref="ProtobufSerializerPrecompiledPatcher"/> which should be used by mods.
/// </remarks>
/// <typeparam name="T">Type of the serializable type</typeparam>
/// <param name="typeId">
/// An arbitrary unique type id which shouldn't be already used by Subnautica nor by any other mod.
/// Look through <see cref="ProtobufSerializerPrecompiled"/> to ensure the id is not already taken.
/// </param>
/// <param name="serializeMethod">
/// A reference to the serialize method.
/// NB: Take inspiration from <see cref="ProtobufSerializerPrecompiled.Serialize11492366"/> to understand how make one.
/// </param>
/// <param name="deserializeMethod">
/// A reference to the serialize method.
/// NB: Take inspiration from <see cref="ProtobufSerializerPrecompiled.Deserialize11492366"/> to understand how make one.
/// </param>
public static void RegisterSerializableType<T>(int typeId, Action<T, int, ProtoWriter> serializeMethod, Func<T, ProtoReader, T> deserializeMethod)
/// <param name="serializableType">Type of the serializable type</param>
public static void RegisterSerializableType(Type serializableType)
{
if (SerializerEntries.ContainsKey(typeId))
{
throw new DuplicateTypeIdException(typeId, typeof(T));
}
ProtobufSerializerPrecompiled.knownTypes.Add(typeof(T), typeId);
SerializerEntries.Add(typeId, new(serializeMethod.Method, deserializeMethod.Method, typeof(T)));
ProtobufSerializerPrecompiled.knownTypes.Add(serializableType, 0);

Dictionary<int, FieldInfo> serializedFieldsByTag = GetSerializedFieldsByTag(serializableType);

SerializerEntries.Add(serializableType, new(serializableType, ((Delegate) Serialize).Method, ((Delegate) Deserialize).Method, serializedFieldsByTag));
InternalLogger.Log($"Registered serializable type {serializableType}");
}

/// <summary>
/// Automatically generates a serialize method and a deserialize method to be used in .
/// Searches for all types marked with attribute <see cref="ProtoContractAttribute"/> in the executing assembly,
/// and registers them as serializable by <see cref="RegisterSerializableType"/>.
/// </summary>
/// <typeparam name="T">Serialized type</typeparam>
public static (Action<T, int, ProtoWriter>, Func<T, ProtoReader, T>) GenerateSerializeAndDeserializeMethods<T>() where T : new()
public static void RegisterAllSerializableTypesInAssembly()
{
// Extract the fields and to be serialized (marked with SubnauticaSerialized)
Type type = typeof(T);
FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

Dictionary<int, FieldInfo> serializedFieldsByTag = new();

foreach (FieldInfo field in fields)
foreach (Type type in Assembly.GetCallingAssembly().GetTypes())
{
foreach (Attribute attribute in field.GetCustomAttributes())
foreach (Attribute attribute in type.GetCustomAttributes())
{
if (attribute is SubnauticaSerialized subnauticaSerialized)
if (attribute is ProtoContractAttribute)
{
serializedFieldsByTag.Add(subnauticaSerialized.tag, field);
RegisterSerializableType(type);
break;
}
}
}
}

// Generate serialized and deserialize methods using the default serializing and deserializing utilities
// provided by the internal methods TrySerializeAuxiliaryType and TryDeserializeAuxiliaryType from TypeModel
Action<T, int, ProtoWriter> serialize = new((instance, objectId, writer) =>
/// <summary>
/// Automatically serializes a registered type from <see cref="SerializerEntries"/> based on its type <typeparamref name="T"/>.
/// </summary>
/// <remarks>
/// Should only be called by <see cref="ProtobufSerializerPrecompiledPatcher.OptimizedSerializePrefix"/>
/// </remarks>
public static void Serialize(object instance, int objId, ProtoWriter writer)
{
if (!SerializerEntries.TryGetValue(instance.GetType(), out SerializerEntry serializerEntry))
{
foreach (KeyValuePair<int, FieldInfo> pair in serializedFieldsByTag.OrderBy(entry => entry.Key))
{
int tag = pair.Key;
FieldInfo field = pair.Value;
return;
}

object value = field.GetValue(instance);
writer.model.TrySerializeAuxiliaryType(writer, field.FieldType, DataFormat.Default, tag, value, false);
foreach (KeyValuePair<int, FieldInfo> pair in serializerEntry.SerializedFieldsByTag)
{
int tag = pair.Key;
FieldInfo field = pair.Value;

object value = field.GetValue(instance);
writer.model.TrySerializeAuxiliaryType(writer, field.FieldType, DataFormat.Default, tag, value, false);
}
}

/// <summary>
/// Automatically deserializes a registered type from <see cref="SerializerEntries"/> based on its type <typeparamref name="T"/>.
/// </summary>
/// <remarks>
/// Should only be called by <see cref="ProtobufSerializerPrecompiledPatcher.OptimizedDeserializePrefix"/>
/// </remarks>
public static object Deserialize(object instance, ProtoReader reader)
{
if (!SerializerEntries.TryGetValue(instance.GetType(), out SerializerEntry serializerEntry))
{
return default;
}

object value = null;
foreach (KeyValuePair<int, FieldInfo> pair in serializerEntry.SerializedFieldsByTag)
{
int tag = pair.Key;
FieldInfo field = pair.Value;
if (reader.model.TryDeserializeAuxiliaryType(reader, DataFormat.Default, tag, field.FieldType, ref value, true, true, false, false))
{
field.SetValue(instance, value);
}
});
}

return instance;
}

Func<T, ProtoReader, T> deserialize = new((instance, reader) =>
/// <summary>
/// Automatically generates an ordered list of fields to serialize
/// </summary>
/// <param name="serializableType">Serialized type</param>
private static Dictionary<int, FieldInfo> GetSerializedFieldsByTag(Type serializableType)
{
// Extract the fields and to be serialized (marked with SubnauticaSerialized)
FieldInfo[] fields = serializableType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

Dictionary<int, FieldInfo> serializedFieldsByTag = new();

foreach (FieldInfo field in fields)
{
object value = null;
foreach (KeyValuePair<int, FieldInfo> pair in serializedFieldsByTag.OrderBy(entry => entry.Key))
foreach (Attribute attribute in field.GetCustomAttributes())
{
int tag = pair.Key;
FieldInfo field = pair.Value;
if (reader.model.TryDeserializeAuxiliaryType(reader, DataFormat.Default, tag, field.FieldType, ref value, true, true, false, false))
if (attribute is SubnauticaSerialized subnauticaSerialized)
{
field.SetValue(instance, value);
serializedFieldsByTag.Add(subnauticaSerialized.tag, field);
break;
}
}
return instance;
});


return (serialize, deserialize);
}
return serializedFieldsByTag;
}

/// <summary>
/// Marks the fields to be detectable by <see cref="GenerateSerializeAndDeserializeMethods{T}"/>.
/// Marks the fields to be detectable by <see cref="GetSerializedFieldsByTag"/>.
/// (Encapsulates <see cref="ProtoMemberAttribute"/>)
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
Expand All @@ -153,57 +175,34 @@ public SubnauticaSerialized(int tag, bool forced = false) : base(tag, forced) {
/// </summary>
internal class SerializerEntry
{
internal Type Type;
internal MethodInfo SerializeInfo;
internal MethodInfo DeserializeInfo;
internal Type Type;
internal IOrderedEnumerable<KeyValuePair<int, FieldInfo>> SerializedFieldsByTag;

/// <summary>
/// Constructor to be commonly used when adding a new serializable type.
/// </summary>
/// <param name="serializeInfo">A static method ran when Subnautica serializes the <see cref="Type"/></param>
/// <param name="deserializeInfo">A static method ran when Subnautica deserializes the <see cref="Type"/></param>
/// <param name="serializedFieldsByTag">A dictionary containing all fields to be serialized, linked to their tag</param>
/// <param name="type">The type to be serializable</param>
public SerializerEntry(MethodInfo serializeInfo, MethodInfo deserializeInfo, Type type)
public SerializerEntry(Type type, MethodInfo serializeInfo, MethodInfo deserializeInfo, Dictionary<int, FieldInfo> serializedFieldsByTag)
{
if (!serializeInfo.IsStatic)
{
throw new ArgumentException($"The provided serialize method '{serializeInfo.Name}' should be static for type '{type}'");
}
if (!deserializeInfo.IsStatic)
{
throw new ArgumentException($"The provided deserialize method '{deserializeInfo.Name}' should be static for type '{type}'");
}

Type = type;
SerializeInfo = serializeInfo;
DeserializeInfo = deserializeInfo;
Type = type;
SerializedFieldsByTag = serializedFieldsByTag.OrderBy(entry => entry.Key);
}

/// <summary>
/// Constructor to be used for Subnautica's default known types only.
/// </summary>
public SerializerEntry(Type type, string serializeMethodName, string deserializeMethodName)
{
Type = type;
SerializeInfo = ReflectionHelper.GetInstanceMethod<ProtobufSerializerPrecompiled>(serializeMethodName);
DeserializeInfo = ReflectionHelper.GetInstanceMethod<ProtobufSerializerPrecompiled>(deserializeMethodName);
Type = type;
}
}

/// <summary>
/// The exception that is thrown when a <see cref="SerializerEntry"/> is attempted to be added when an existing one of the same type already exists.
/// </summary>
public class DuplicateTypeIdException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="DuplicateTypeIdException"/> class with default properties.
/// </summary>
/// <param name="typeId">Type id of the already registered serializer entry.</param>
/// <param name="type">Type of the requested</param>
public DuplicateTypeIdException(int typeId, Type type) : base
($"Cannot register serializable type '{type}' with id '{typeId}' because it is already used by either Subnautica or another instance.")
{

}
}
}
Loading

0 comments on commit 0b97c7d

Please sign in to comment.