Skip to content

Instantly share code, notes, and snippets.

@gotmachine
Created March 18, 2024 21:47
Show Gist options
  • Save gotmachine/33e64cc31be76824c15f5a4f945efb4f to your computer and use it in GitHub Desktop.
Save gotmachine/33e64cc31be76824c15f5a4f945efb4f to your computer and use it in GitHub Desktop.
Custom serialization for partmodules
[AttributeUsage(AttributeTargets.Field)]
public class PartModuleSerializeAttribute : Attribute { }
public class SerializablePartModule : PartModule, ISerializationCallbackReceiver
{
private static readonly Dictionary<Type, SerializablePartModuleInfo> accessCache = new Dictionary<Type, SerializablePartModuleInfo>();
private static readonly BinaryFormatter bf = new BinaryFormatter();
[SerializeField] private byte[] serializedData;
[SerializeField] private int[] offsets;
public bool IsPrefab => part.partInfo == null || part.partInfo.partPrefab == part;
public void OnBeforeSerialize()
{
if (!accessCache.TryGetValue(GetType(), out SerializablePartModuleInfo pmInfo))
{
pmInfo = new SerializablePartModuleInfo(this);
accessCache[GetType()] = pmInfo;
}
if (offsets == null)
offsets = new int[pmInfo.fieldCount];
for (int i = 0; i < pmInfo.fieldCount; i++)
{
byte[] data;
using (MemoryStream ms = new MemoryStream())
{
object value = pmInfo.fields[i].getValue.Invoke(this);
if (value == null)
{
data = Array.Empty<byte>();
}
else
{
bf.Serialize(ms, value);
data = ms.ToArray();
}
}
if (i == 0)
{
serializedData = data;
}
else if (data.Length > 0)
{
byte[] previousData = serializedData;
serializedData = new byte[previousData.Length + data.Length];
Array.Copy(previousData, serializedData, previousData.Length);
Array.Copy(data, 0, serializedData, previousData.Length, data.Length);
}
offsets[i] = data.Length;
}
}
public void OnAfterDeserialize()
{
if (serializedData == null)
return;
if (!accessCache.TryGetValue(GetType(), out SerializablePartModuleInfo pmInfo))
return;
int currentIndex = 0;
for (int i = 0; i < pmInfo.fieldCount; i++)
{
int offset = offsets[i];
object value;
if (offset == 0)
{
value = null;
}
else
{
using MemoryStream ms = new MemoryStream(serializedData, currentIndex, offset);
value = bf.Deserialize(ms);
}
pmInfo.fields[i].setValue.Invoke(this, value);
currentIndex += offset;
}
offsets = null;
serializedData = null;
}
private class KSPSerializableFieldInfo
{
public Action<object, object> setValue;
public Func<object, object> getValue;
}
private class SerializablePartModuleInfo
{
public List<KSPSerializableFieldInfo> fields = new List<KSPSerializableFieldInfo>();
public readonly int fieldCount;
public SerializablePartModuleInfo(SerializablePartModule instance)
{
foreach (FieldInfo fieldInfo in instance.GetType().GetFields())
{
if (!Attribute.IsDefined(fieldInfo, typeof(PartModuleSerializeAttribute)))
continue;
if (!Attribute.IsDefined(fieldInfo, typeof(NonSerializedAttribute)))
{
Debug.LogError($"Field '{fieldInfo.Name}' in PartModule '{instance.GetType().Name}' must have the [NonSerialized] attribute for the [BinarySerialize] attribute to work");
continue;
}
if (!fieldInfo.IsPublic)
{
Debug.LogError($"Field '{fieldInfo.Name}' in PartModule '{instance.GetType().Name}' must be public for the [BinarySerialize] attribute to work");
continue;
}
KSPSerializableFieldInfo serializableFieldInfo = new KSPSerializableFieldInfo();
string getterName = $"{instance.GetType().Name}_{fieldInfo.Name}_Getter";
DynamicMethod fieldGetter = new DynamicMethod(getterName, typeof(object), new[] { typeof(object) }, true);
ILGenerator fieldGetteril = fieldGetter.GetILGenerator();
fieldGetteril.Emit(OpCodes.Ldarg_0);
fieldGetteril.Emit(OpCodes.Ldfld, fieldInfo);
fieldGetteril.Emit(OpCodes.Ret);
serializableFieldInfo.getValue = (Func<object, object>)fieldGetter.CreateDelegate(typeof(Func<object, object>));
string setterName = $"{instance.GetType().Name}_{fieldInfo.Name}_Setter";
DynamicMethod fieldSetter = new DynamicMethod(setterName, null, new[] { typeof(object), typeof(object) }, true);
ILGenerator fieldSetterIl = fieldSetter.GetILGenerator();
fieldSetterIl.Emit(OpCodes.Ldarg_0);
fieldSetterIl.Emit(OpCodes.Ldarg_1);
fieldSetterIl.Emit(OpCodes.Stfld, fieldInfo);
fieldSetterIl.Emit(OpCodes.Ret);
serializableFieldInfo.setValue = (Action<object, object>)fieldSetter.CreateDelegate(typeof(Action<object, object>));
fields.Add(serializableFieldInfo);
}
fieldCount = fields.Count;
}
}
}
// Example usage
public class NotSharedDataModule : SerializablePartModule
{
[NonSerialized, PartModuleSerialize]
public Dictionary<int, DataClassNotShared> dataDictNotShared;
[NonSerialized, PartModuleSerialize]
public List<DataClassNotShared> dataListNotShared;
[NonSerialized, PartModuleSerialize]
public DataClassNotShared data1;
[NonSerialized, PartModuleSerialize]
public DataClassNotShared data2;
public override void OnLoad(ConfigNode node)
{
// get config data on prefab only
if (IsPrefab && node.HasNode("DATA"))
{
dataListNotShared = new List<DataClassNotShared>();
dataDictNotShared = new Dictionary<int, DataClassNotShared>();
int i = 0;
foreach (ConfigNode dataNode in node.GetNodes("DATA"))
{
DataClassNotShared dataNotShared = new DataClassNotShared();
dataNotShared.Load(dataNode);
dataListNotShared.Add(dataNotShared);
dataDictNotShared.Add(i, dataNotShared);
if (i == 0)
data1 = dataNotShared;
else if (i == 1)
data2 = dataNotShared;
i++;
}
}
}
}
[Serializable]
public class DataClassNotShared
{
public float value1;
public string value2;
public void Load(ConfigNode node)
{
node.TryGetValue("value1", ref value1);
node.TryGetValue("value2", ref value2);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment