Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support protobuf serializer for custom types #523

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Prev Previous commit
Next Next commit
feat: Created an automatic serialize and deserialize method generator…
…, and adapted the example accordingly
  • Loading branch information
tornac1234 committed Feb 19, 2024
commit 52ab1203dc6c26317a8848aa9581afde0a100de8
73 changes: 31 additions & 42 deletions Example mod/SerializableBehaviourExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ 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> serializeMethodInfo;
public static Func<RealtimeCounter, ProtoReader, RealtimeCounter> deserializeMethodInfo;

private void Awake()
{
// Don't forget to register your serializable type
// You can pick whatever type ID you want as long it's not used by another mod or by Subnautica itself
// We first generate automatically the serialize and deserialize methods for our serializable type
var methodInfos = ProtobufSerializerHandler.GenerateSerializeAndDeserializeMethods<RealtimeCounter>();
// And we store them somewhere easily accessible
serializeMethodInfo = methodInfos.Item1;
deserializeMethodInfo = methodInfos.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);
}
}
Expand All @@ -23,26 +33,26 @@ internal class RealtimeCounter : MonoBehaviour, IProtoEventListener
{
public DateTimeOffset StartedCounting { get; private set; } = DateTimeOffset.UtcNow;

// If you plan on distributing updates to your mod and maybe updating the serialized MonoBehaviours' fields,
// If you plan on updating your mod and in particular, updating the serialized MonoBehaviours' fields,
// you will need to update the version number and adapt your data in the OnProtoDeserialize method.
// If you're just making a new serializable MonoBehaviour, set version to 1
// If you don't care about updating your mod, you can just ignore everything concerning the version variable
[ProtoMember(1), NonSerialized]
[ProtobufSerializerHandler.SubnauticaSerialized(1)]
public int version = 2;

// Variable to be serialized
[ProtoMember(2), NonSerialized]
[ProtobufSerializerHandler.SubnauticaSerialized(2)]
public double totalRealtimeMs;

// This is an example of how you can give a default value to your serialized variables
[ProtoMember(3), NonSerialized]
[ProtobufSerializerHandler.SubnauticaSerialized(3)]
public long firstLaunchUnixTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

// Some listeners from the optional interface IProtoEventListener
// 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)
{
// This happens when this MonoBehaviour is loaded for the first time after being updated
// This happens when this MonoBehaviour is loaded for the first time after being updated (serialized version was 1 but it should be 2)
// You can find another example in Subnautica's code, like WaterParkCreature.OnProtoDeserialize
if (version == 1)
{
Expand All @@ -57,57 +67,36 @@ public void OnProtoDeserialize(ProtobufSerializer serializer)
// OnProtoSerialize happens before this MonoBehaviour's GameObject full hierarchy is serialized
public void OnProtoSerialize(ProtobufSerializer serializer)
{
// Ensure the serialized version is the newest one
version = 2;

// Doing some stuff according to your needs
totalRealtimeMs += (DateTimeOffset.UtcNow - StartedCounting).TotalMilliseconds;
StartedCounting = DateTimeOffset.UtcNow;
Debug.Log($"Serialized realtime counter with {totalRealtimeMs}ms [first launch at: {DateTimeOffset.FromUnixTimeMilliseconds(firstLaunchUnixTimeMs)}]");
}

// The required static methods for Protobuf serializer to serialize and deserialize this MonoBehaviour
// 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)
// 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 this will be straightforward but you may need to adapt this with more complex ones
// Please pick the right WireType for your variables and adapt the following Write[Object] call
ProtoWriter.WriteFieldHeader(1, WireType.Variant, writer);
ProtoWriter.WriteInt32(realtimeCounter.version, writer);

ProtoWriter.WriteFieldHeader(2, WireType.Fixed64, writer);
ProtoWriter.WriteDouble(realtimeCounter.totalRealtimeMs, 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

ProtoWriter.WriteFieldHeader(3, WireType.Fixed64, writer);
ProtoWriter.WriteInt64(realtimeCounter.firstLaunchUnixTimeMs, writer);
// In this case, we only have basic types to serialize so we just invoke the generated method
SerializableBehaviourExample.serializeMethodInfo.Invoke(realtimeCounter, objTypeId, writer);
}

// Deserialize must be a static YourType with parameters (YourType, ProtoReader)
// 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
for (int i = reader.ReadFieldHeader(); i > 0; i = reader.ReadFieldHeader())
{
switch (i)
{
case 1:
// We used ProtoWriter.WriteInt32 so here we need reader.ReadInt32
realtimeCounter.version = reader.ReadInt32();
break;

case 2:
realtimeCounter.totalRealtimeMs = reader.ReadInt64();
break;

case 3:
realtimeCounter.firstLaunchUnixTimeMs = reader.ReadInt64();
break;

default:
reader.SkipField();
break;
}
}

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

// Return the same object you've been modifying
return realtimeCounter;
}
Expand Down
71 changes: 70 additions & 1 deletion Nautilus/Handlers/ProtobufSerializerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,75 @@ public static void RegisterSerializableType<T>(int typeId, Action<T, int, ProtoW
SerializerEntries.Add(typeId, new(serializeMethod.Method, deserializeMethod.Method, typeof(T)));
}

/// <summary>
/// Automatically generates a serialize method and a deserialize method to be used in .
/// </summary>
/// <typeparam name="T">Serialized type</typeparam>
public static (Action<T, int, ProtoWriter>, Func<T, ProtoReader, T>) GenerateSerializeAndDeserializeMethods<T>() where T : new()
{
// 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 (Attribute attribute in field.GetCustomAttributes())
{
if (attribute is SubnauticaSerialized subnauticaSerialized)
{
serializedFieldsByTag.Add(subnauticaSerialized.tag, field);
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) =>
{
foreach (KeyValuePair<int, FieldInfo> pair in serializedFieldsByTag.OrderBy(entry => entry.Key))
{
int tag = pair.Key;
FieldInfo field = pair.Value;

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

Func<T, ProtoReader, T> deserialize = new((instance, reader) =>
{
object value = null;
foreach (KeyValuePair<int, FieldInfo> pair in serializedFieldsByTag.OrderBy(entry => entry.Key))
{
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;
});


return (serialize, deserialize);
}

/// <summary>
/// Marks the fields to be detectable by <see cref="GenerateSerializeAndDeserializeMethods{T}"/>.
/// (Encapsulates <see cref="ProtoMemberAttribute"/>)
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class SubnauticaSerialized : ProtoMemberAttribute
{
/// <inheritdoc cref="ProtoMemberAttribute(int, bool)" />
public SubnauticaSerialized(int tag, bool forced = false) : base(tag, forced) { }
}

/// <summary>
/// Data structure which holds the method data for serialization and deserialization.
/// </summary>
Expand All @@ -103,8 +172,8 @@ public SerializerEntry(MethodInfo serializeInfo, MethodInfo deserializeInfo, Typ
if (!deserializeInfo.IsStatic)
{
throw new ArgumentException($"The provided deserialize method '{deserializeInfo.Name}' should be static for type '{type}'");

}

SerializeInfo = serializeInfo;
DeserializeInfo = deserializeInfo;
Type = type;
Expand Down
Loading