From 07ba6f2dc450ca9ac43214b0e9e93a4c74d8da65 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Tue, 28 Apr 2020 21:56:34 -0400 Subject: [PATCH] Add game file persistence functionality --- GamecraftModdingAPI/Main.cs | 1 + .../DeserializeFromDiskEntitiesEnginePatch.cs | 87 +++++++++++++++++ .../Persistence/IEntitySerializer.cs | 18 ++++ .../SaveAndLoadCompositionRootPatch.cs | 18 ++++ .../Persistence/SaveGameEnginePatch.cs | 81 ++++++++++++++++ .../Persistence/SerializerManager.cs | 72 ++++++++++++++ .../Persistence/SimpleEntitySerializer.cs | 63 ++++++++++++ .../Tests/GamecraftModdingAPIPluginTest.cs | 3 + .../Utility/ApiExclusiveGroups.cs | 2 + .../Utility/VersionTracking.cs | 96 +++++++++++++++++++ 10 files changed, 441 insertions(+) create mode 100644 GamecraftModdingAPI/Persistence/DeserializeFromDiskEntitiesEnginePatch.cs create mode 100644 GamecraftModdingAPI/Persistence/IEntitySerializer.cs create mode 100644 GamecraftModdingAPI/Persistence/SaveAndLoadCompositionRootPatch.cs create mode 100644 GamecraftModdingAPI/Persistence/SaveGameEnginePatch.cs create mode 100644 GamecraftModdingAPI/Persistence/SerializerManager.cs create mode 100644 GamecraftModdingAPI/Persistence/SimpleEntitySerializer.cs create mode 100644 GamecraftModdingAPI/Utility/VersionTracking.cs diff --git a/GamecraftModdingAPI/Main.cs b/GamecraftModdingAPI/Main.cs index ff8b63c..e0717d2 100644 --- a/GamecraftModdingAPI/Main.cs +++ b/GamecraftModdingAPI/Main.cs @@ -48,6 +48,7 @@ namespace GamecraftModdingAPI // init utility Logging.MetaDebugLog($"Initializing Utility"); Utility.GameState.Init(); + Utility.VersionTracking.Init(); // create default event emitters Logging.MetaDebugLog($"Initializing Events"); EventManager.AddEventEmitter(new SimpleEventEmitterEngine(EventType.ApplicationInitialized, "GamecraftModdingAPIApplicationInitializedEventEmitter", false)); diff --git a/GamecraftModdingAPI/Persistence/DeserializeFromDiskEntitiesEnginePatch.cs b/GamecraftModdingAPI/Persistence/DeserializeFromDiskEntitiesEnginePatch.cs new file mode 100644 index 0000000..8f3482f --- /dev/null +++ b/GamecraftModdingAPI/Persistence/DeserializeFromDiskEntitiesEnginePatch.cs @@ -0,0 +1,87 @@ +using System; +using System.Text; +using System.Reflection; + +using RobocraftX.Common; +using Svelto.DataStructures; +using Svelto.ECS; +using Svelto.ECS.Serialization; + +using Harmony; +using GamecraftModdingAPI.Utility; + +namespace GamecraftModdingAPI.Persistence +{ + [HarmonyPatch] + class DeserializeFromDiskEntitiesEnginePatch + { + internal static EntitiesDB entitiesDB = null; + + private static readonly byte[] frameStart = Encoding.UTF8.GetBytes("\0\0\0GamecraftModdingAPI\0\0\0"); + + public static void Prefix(ref ISerializationData ____serializationData, ref FasterList ____bytesStream, ref IEntitySerialization ____entitySerializer, bool ____spawnBlocksOnly) + { + if (____spawnBlocksOnly) return; // only run after second deserialization call (when all vanilla stuff is already deserialized) + uint originalPos = ____serializationData.dataPos; + Logging.MetaDebugLog($"dataPos: {originalPos}"); + BinaryBufferReader bbr = new BinaryBufferReader(____bytesStream.ToArrayFast(out uint count), ____serializationData.dataPos); + byte[] frameBuffer = new byte[frameStart.Length]; + Logging.MetaDebugLog($"serial data count: {____serializationData.data.count} capacity: {____serializationData.data.capacity}"); + int i = 0; + // match frame start + while (frameBuffer != frameStart && bbr.Position < count-frameStart.Length) + { + i = 0; + frameBuffer[0] = bbr.ReadByte(); + while (frameBuffer[i] == frameStart[i] && bbr.Position < count - frameStart.Length + i) + { + i++; + if (i == frameStart.Length) break; + frameBuffer[i] = bbr.ReadByte(); + } + if (i == frameStart.Length) break; + } + // abort if at end of file + if (bbr.Position >= count - frameStart.Length) + { + Logging.MetaLog("Skipping deserialization (no frame found)"); + return; + } + //____serializationData.dataPos = bbr.Position; + Logging.MetaDebugLog($"dataPos (after frame): {bbr.Position}"); + uint customComponentsCount = bbr.ReadUint(); + for (uint c = 0; c < customComponentsCount; c++) + { + // determine component from info + uint nameLength = bbr.ReadUint(); + byte[] nameBytes = new byte[nameLength]; + bbr.ReadBytes(nameBytes, nameLength); + string name = Encoding.UTF8.GetString(nameBytes); + Logging.MetaDebugLog($"Component name: {name} (len: {nameLength})"); + uint componentEnd = bbr.ReadUint(); + ____serializationData.dataPos = bbr.Position; + if (SerializerManager.ExistsSerializer(name)) + { + // deserialize component + IEntitySerializer serial = SerializerManager.GetSerializer(name); + if (!serial.Deserialize(ref ____serializationData, ____entitySerializer)) + { + Logging.MetaDebugLog("Component deserialization failed!"); + } + } + else + { + Logging.MetaDebugLog("Skipping component deserialization: not found!"); + } + bbr = new BinaryBufferReader(____bytesStream.ToArrayFast(out count), componentEnd); + } + ____serializationData.dataPos = originalPos; // change back to original end point (just in case) + Logging.MetaDebugLog("Deserialization complete"); + } + + public static MethodBase TargetMethod() + { + return AccessTools.Method("RobocraftX.SaveAndLoad.DeserializeFromDiskEntitiesEngine:LoadingFinished");//AccessTools.TypeByName("RobocraftX.SaveAndLoad.DeserializeFromDiskEntities") + } + } +} diff --git a/GamecraftModdingAPI/Persistence/IEntitySerializer.cs b/GamecraftModdingAPI/Persistence/IEntitySerializer.cs new file mode 100644 index 0000000..c32ee17 --- /dev/null +++ b/GamecraftModdingAPI/Persistence/IEntitySerializer.cs @@ -0,0 +1,18 @@ +using System; + +using Svelto.ECS; +using Svelto.ECS.Serialization; + +using GamecraftModdingAPI.Utility; + +namespace GamecraftModdingAPI.Persistence +{ + public interface IEntitySerializer : IDeserializationFactory, IQueryingEntitiesEngine + { + IEntityFactory EntityFactory { set; } + + bool Serialize(ref ISerializationData serializationData, EntitiesDB entitiesDB, IEntitySerialization entitySerializer); + + bool Deserialize(ref ISerializationData serializationData, IEntitySerialization entitySerializer); + } +} diff --git a/GamecraftModdingAPI/Persistence/SaveAndLoadCompositionRootPatch.cs b/GamecraftModdingAPI/Persistence/SaveAndLoadCompositionRootPatch.cs new file mode 100644 index 0000000..a510580 --- /dev/null +++ b/GamecraftModdingAPI/Persistence/SaveAndLoadCompositionRootPatch.cs @@ -0,0 +1,18 @@ +using System; + +using RobocraftX.SaveAndLoad; +using Svelto.ECS; + +using Harmony; + +namespace GamecraftModdingAPI.Persistence +{ + [HarmonyPatch(typeof(SaveAndLoadCompositionRoot), "Compose")] + class SaveAndLoadCompositionRootPatch + { + public static void Prefix(EnginesRoot enginesRoot) + { + SerializerManager.RegisterSerializers(enginesRoot); + } + } +} diff --git a/GamecraftModdingAPI/Persistence/SaveGameEnginePatch.cs b/GamecraftModdingAPI/Persistence/SaveGameEnginePatch.cs new file mode 100644 index 0000000..21cd68a --- /dev/null +++ b/GamecraftModdingAPI/Persistence/SaveGameEnginePatch.cs @@ -0,0 +1,81 @@ +using System; +using System.Text; +using System.Reflection; + +using RobocraftX.Common; +using RobocraftX.SaveAndLoad; +using Svelto.DataStructures; +using Svelto.ECS; +using Svelto.ECS.Serialization; + +using GamecraftModdingAPI.Utility; +using Harmony; + +namespace GamecraftModdingAPI.Persistence +{ + [HarmonyPatch] + class SaveGameEnginePatch + { + private static readonly byte[] frameStart = Encoding.UTF8.GetBytes("\0\0\0GamecraftModdingAPI\0\0\0"); + + public static void Postfix(ref ISerializationData serializationData, EntitiesDB entitiesDB, IEntitySerialization entitySerializer) + { + Logging.MetaDebugLog("Running Postfix on SerializeGameToBuffer: serializing custom components..."); + if (SerializerManager.GetSerializersCount() == 0) + { + Logging.MetaDebugLog("Skipping component serialization: no serializers registered!"); + return; + } + serializationData.data.ExpandBy((uint)frameStart.Length); + BinaryBufferWriter bbw = new BinaryBufferWriter(serializationData.data.ToArrayFast(out uint buffLen), serializationData.dataPos); + uint originalPos = serializationData.dataPos; + Logging.MetaDebugLog($"dataPos: {originalPos}"); + // Add frame start so it's easier to find GamecraftModdingAPI-serialized components + for (int i = 0; i < frameStart.Length; i++) + { + bbw.Write(frameStart[i]); + } + Logging.MetaDebugLog($"dataPos (after frame start): {bbw.Position}"); + serializationData.data.ExpandBy(4u); + bbw.Write((uint)SerializerManager.GetSerializersCount()); + string[] serializerKeys = SerializerManager.GetSerializerNames(); + for (uint c = 0; c < serializerKeys.Length; c++) + { + Logging.MetaDebugLog($"dataPos (loop start): {bbw.Position}"); + // write component info + serializationData.data.ExpandBy(4u + (uint)serializerKeys[c].Length); + bbw.Write((uint)serializerKeys[c].Length); + Logging.MetaDebugLog($"dataPos (now): {bbw.Position}"); + byte[] nameBytes = Encoding.UTF8.GetBytes(serializerKeys[c]); + for (int i = 0; i < nameBytes.Length; i++) + { + bbw.Write(nameBytes[i]); + } + Logging.MetaDebugLog($"dataPos (now): {bbw.Position}"); + serializationData.data.ExpandBy(4u); + serializationData.dataPos = bbw.Position + 4u; + Logging.MetaDebugLog($"dataPos (now): {bbw.Position}"); + Logging.MetaDebugLog($"dataPos (appears to be): {serializationData.dataPos}"); + // serialize component + IEntitySerializer serializer = SerializerManager.GetSerializer(serializerKeys[c]); + if (!serializer.Serialize(ref serializationData, entitiesDB, entitySerializer)) + { + Logging.MetaDebugLog("Component serialization failed!"); + } + Logging.MetaDebugLog($"dataPos (now): {bbw.Position}"); + bbw.Write((uint)serializationData.dataPos); + Logging.MetaDebugLog($"dataPos (now): {bbw.Position}"); + bbw = new BinaryBufferWriter(serializationData.data.ToArrayFast(out buffLen), serializationData.dataPos); + Logging.MetaDebugLog($"dataPos (loop end): {bbw.Position}"); + } + serializationData.data.Trim(); + Logging.MetaDebugLog($"dataPos (end): {bbw.Position}"); + Logging.MetaDebugLog("Serialization complete"); + } + + public static MethodBase TargetMethod() + { + return typeof(SaveGameEngine).GetMethod("SerializeGameToBuffer"); + } + } +} diff --git a/GamecraftModdingAPI/Persistence/SerializerManager.cs b/GamecraftModdingAPI/Persistence/SerializerManager.cs new file mode 100644 index 0000000..ae4d9c3 --- /dev/null +++ b/GamecraftModdingAPI/Persistence/SerializerManager.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Svelto.ECS; +using Svelto.ECS.Serialization; + +using GamecraftModdingAPI.Utility; + +namespace GamecraftModdingAPI.Persistence +{ + public static class SerializerManager + { + private static Dictionary _serializers = new Dictionary(); + + private static Dictionary> _registrations = new Dictionary>(); + + private static EnginesRoot _lastEnginesRoot; + + public static void AddSerializer(IEntitySerializer serializer) where T : ISerializableEntityDescriptor, new() + { + string name = typeof(T).FullName; + _serializers[name] = serializer; + _registrations[name] = (IEntitySerialization ies) => { ies.RegisterSerializationFactory(serializer); }; + if (_lastEnginesRoot != null) + { + serializer.EntityFactory = _lastEnginesRoot.GenerateEntityFactory(); + _registrations[name].Invoke(_lastEnginesRoot.GenerateEntitySerializer()); + _lastEnginesRoot.AddEngine(serializer); + } + } + + public static bool ExistsSerializer(string name) + { + return _serializers.ContainsKey(name); + } + + public static bool ExistsSerializer(IEntitySerializer serializer) where T : ISerializableEntityDescriptor, new() + { + return ExistsSerializer(typeof(T).FullName); + } + + public static IEntitySerializer GetSerializer(string name) + { + return _serializers[name]; + } + + public static string[] GetSerializerNames() + { + return _serializers.Keys.ToArray(); + } + + public static int GetSerializersCount() + { + return _serializers.Count; + } + + public static void RegisterSerializers(EnginesRoot enginesRoot) + { + _lastEnginesRoot = enginesRoot; + IEntityFactory factory = enginesRoot.GenerateEntityFactory(); + IEntitySerialization ies = enginesRoot.GenerateEntitySerializer(); + foreach (string key in _serializers.Keys) + { + Logging.MetaDebugLog($"Registering IEntitySerializer for {key}"); + _serializers[key].EntityFactory = factory; + _registrations[key].Invoke(ies); + enginesRoot.AddEngine(_serializers[key]); + } + } + } +} diff --git a/GamecraftModdingAPI/Persistence/SimpleEntitySerializer.cs b/GamecraftModdingAPI/Persistence/SimpleEntitySerializer.cs new file mode 100644 index 0000000..e18943f --- /dev/null +++ b/GamecraftModdingAPI/Persistence/SimpleEntitySerializer.cs @@ -0,0 +1,63 @@ +using System; + +using Svelto.ECS; +using Svelto.ECS.Serialization; + +using RobocraftX.Common; + +namespace GamecraftModdingAPI.Persistence +{ + public class SimpleEntitySerializer : IEntitySerializer where Descriptor : ISerializableEntityDescriptor, new() + { + public delegate EGID[] GetEntitiesToSerialize(EntitiesDB entitiesDB); + + private GetEntitiesToSerialize getEntitiesToSerialize; + + protected int serializationType; + + public IEntityFactory EntityFactory { set; protected get; } + + public EntitiesDB entitiesDB { set; protected get; } + + public EntityComponentInitializer BuildDeserializedEntity(EGID egid, ISerializationData serializationData, ISerializableEntityDescriptor entityDescriptor, int serializationType, IEntitySerialization entitySerialization) + { + EntityComponentInitializer esi = EntityFactory.BuildEntity(egid); + entitySerialization.DeserializeEntityComponents(serializationData, entityDescriptor, ref esi, serializationType); + return esi; + } + + public bool Deserialize(ref ISerializationData serializationData, IEntitySerialization entitySerializer) + { + BinaryBufferReader bbr = new BinaryBufferReader(serializationData.data.ToArrayFast(out uint count), serializationData.dataPos); + uint entityCount = bbr.ReadUint(); + serializationData.dataPos = bbr.Position; + for (uint i = 0; i < entityCount; i++) + { + entitySerializer.DeserializeEntity(serializationData, serializationType); + } + return true; + } + + public void Ready() { } + + public bool Serialize(ref ISerializationData serializationData, EntitiesDB entitiesDB, IEntitySerialization entitySerializer) + { + serializationData.data.ExpandBy(4u); + BinaryBufferWriter bbw = new BinaryBufferWriter(serializationData.data.ToArrayFast(out uint count), serializationData.dataPos); + EGID[] toSerialize = getEntitiesToSerialize(entitiesDB); + bbw.Write((uint)toSerialize.Length); + serializationData.dataPos = bbw.Position; + for (uint i = 0; i < toSerialize.Length; i++) + { + entitySerializer.SerializeEntity(toSerialize[i], serializationData, serializationType); + } + return true; + } + + public SimpleEntitySerializer(GetEntitiesToSerialize getEntitiesToSerialize) + { + this.getEntitiesToSerialize = getEntitiesToSerialize; + serializationType = (int)SerializationType.Storage; + } + } +} diff --git a/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs b/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs index dac4312..11d63c6 100644 --- a/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs +++ b/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs @@ -44,6 +44,7 @@ namespace GamecraftModdingAPI.Tests FileLog.Reset(); HarmonyInstance.DEBUG = true; GamecraftModdingAPI.Main.Init(); + Logging.MetaDebugLog($"Version group id {(uint)ApiExclusiveGroups.versionGroup}"); // in case Steam is not installed/running // this will crash the game slightly later during startup //SteamInitPatch.ForcePassSteamCheck = true; @@ -55,6 +56,8 @@ namespace GamecraftModdingAPI.Tests Logging.MetaDebugLog("Audio Mixers: "+string.Join(",", AudioTools.GetMixers())); //AudioTools.SetVolume(0.0f, "Music"); // The game now sets this from settings again after this is called :( + Utility.VersionTracking.Enable(); + // debug/test handlers EventManager.AddEventHandler(new SimpleEventHandlerEngine(() => { Logging.Log("App Inited event!"); }, () => { }, EventType.ApplicationInitialized, "appinit API debug")); diff --git a/GamecraftModdingAPI/Utility/ApiExclusiveGroups.cs b/GamecraftModdingAPI/Utility/ApiExclusiveGroups.cs index df32418..5538404 100644 --- a/GamecraftModdingAPI/Utility/ApiExclusiveGroups.cs +++ b/GamecraftModdingAPI/Utility/ApiExclusiveGroups.cs @@ -13,5 +13,7 @@ namespace GamecraftModdingAPI.Utility public static readonly ExclusiveGroup eventsExclusiveGroup = new ExclusiveGroup(); public static uint eventID; + + public static readonly ExclusiveGroup versionGroup = new ExclusiveGroup("GamecraftModdingAPIVersion"); } } diff --git a/GamecraftModdingAPI/Utility/VersionTracking.cs b/GamecraftModdingAPI/Utility/VersionTracking.cs new file mode 100644 index 0000000..108220c --- /dev/null +++ b/GamecraftModdingAPI/Utility/VersionTracking.cs @@ -0,0 +1,96 @@ +using System; +using System.Reflection; + +using RobocraftX.Common; +using Svelto.ECS; +using Svelto.ECS.Serialization; + +using GamecraftModdingAPI.Persistence; +using GamecraftModdingAPI.Events; + +namespace GamecraftModdingAPI.Utility +{ + public static class VersionTracking + { + private static readonly VersionTrackingEngine versionEngine = new VersionTrackingEngine(); + + private static bool isEnabled = false; + + public static uint GetVersion() + { + if (!isEnabled) return 0u; + return versionEngine.GetGameVersion(); + } + + public static void Enable() + { + EventManager.AddEventEmitter(versionEngine); + isEnabled = true; + } + + public static void Disable() + { + EventManager.AddEventEmitter(versionEngine); + isEnabled = false; + } + + public static void Init() + { + SerializerManager.AddSerializer(new SimpleEntitySerializer( + (_) => { return new EGID[1] { new EGID(0u, ApiExclusiveGroups.versionGroup) }; } + )); + } + } + + internal class VersionTrackingEngine : IEventEmitterEngine + { + public string Name { get; } = "GamecraftModdingAPIVersionTrackingGameEngine"; + + public EntitiesDB entitiesDB { set; private get; } + + public int type => -1; + + public bool isRemovable => false; + + public IEntityFactory Factory { set; private get; } + + public void Dispose() { } + + public void Ready() + { + EGID egid = new EGID(0u, ApiExclusiveGroups.versionGroup); + if (!entitiesDB.Exists(egid)) + { + Version currentVersion = Assembly.GetExecutingAssembly().GetName().Version; + int v = (currentVersion.Major * 1000) + (currentVersion.Minor); + Factory.BuildEntity(egid).Init(new ModVersionStruct + { + version = (uint)v + }); + } + } + + public uint GetGameVersion() + { + return entitiesDB.QueryUniqueEntity(ApiExclusiveGroups.versionGroup).version; + } + + public void Emit() { } + } + + public struct ModVersionStruct : IEntityComponent + { + public uint version; + } + + public class ModVersionDescriptor: SerializableEntityDescriptor + { + [HashName("GamecraftModdingAPIVersionV0")] + public class _ModVersionDescriptor : IEntityDescriptor + { + public IComponentBuilder[] componentsToBuild { get; } = new IComponentBuilder[]{ + new SerializableComponentBuilder(((int)SerializationType.Network, new DefaultSerializer()), ((int)SerializationType.Storage, new DefaultSerializer())), + }; + } + } +}