Created
March 18, 2024 21:47
-
-
Save gotmachine/33e64cc31be76824c15f5a4f945efb4f to your computer and use it in GitHub Desktop.
Custom serialization for partmodules
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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