diff --git a/GamecraftScripting/Commands/ExecuteCommandEngine.cs b/GamecraftScripting/Commands/ExecuteCommandEngine.cs new file mode 100644 index 0000000..d392b6f --- /dev/null +++ b/GamecraftScripting/Commands/ExecuteCommandEngine.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; + +using GamecraftModdingAPI.Commands; +using GamecraftModdingAPI.Utility; +using Svelto.ECS; + +using Microsoft.Scripting.Hosting; + +using GamecraftScripting.Environment; +using GamecraftScripting.Serialization; + +namespace GamecraftScripting.Commands +{ + public class ExecuteCommandEngine : ICustomCommandEngine + { + public const string URI_IMMEDIATE = "immediate://"; + public const string URI_HTTP = "http://"; + public const string URI_HTTPS = "https://"; + public const string URI_INTERNAL = "internal://"; + public const string URI_BUILTIN = "exmods://"; + public const string URI_FILE = "file://"; + public const string URI_ANSWER = "42://"; + + public string Description { get; } = "Execute some Python, intelligently guessing how to execute it"; + + public string Name { get; } = "ExecutePython"; + + public EntitiesDB entitiesDB { set; private get; } + + public void Dispose() + { + CommandRegistrationHelper.Unregister(Name); + } + + public void Ready() + { + // TODO command registration + CommandRegistrationHelper.Register(Name, ExecutePythonCommand, Description); + // create insecure Python namespace + ScriptEngine pyEngine = PythonEnvironment.CreateEngine(); + ScriptScope pyScope = PythonEnvironment.CreateScope(pyEngine); + PythonEnvironment.CurrentEngine = pyEngine; + PythonEnvironment.CurrentScope = pyScope; + } + + private void ExecutePythonCommand(string name) + { + string uriName = urifyName(name.Trim()); + if (uriName == null) + { + Logging.CommandLogError("Unable to guess script type. Please add a scheme to the start of the command argument to avoid this. \nValid schemes include: \n" + URI_IMMEDIATE.Replace('/', '\\') + " (for Python code) \n" + URI_FILE.Replace('/', '\\') + " (for Python file) "); + return; + } + string scriptContents = getScript(uriName); + if (scriptContents == null) + { + Logging.CommandLogError("Invalid script or URI scheme. Please replace the scheme at the start of the command argument. Valid schemes include: \n" + URI_IMMEDIATE.Replace('/', '\\') + "\\\\ (for Python code) \n" + URI_FILE.Replace('/', '\\') + "\\\\ (for Python file) "); + return; + } + Execute(uriName, scriptContents); + } + + private string urifyName(string name) + { + if (name.Contains("://")) return name; // already is a uri, hopefully a valid uri + if (name.Contains(":\\\\")) return name.Replace(":\\\\", "://"); // :\\ is easier to input in CLI + if (!name.EndsWith(".py", StringComparison.InvariantCultureIgnoreCase)) + return URI_IMMEDIATE + name; // probably not a script file, hopefully it's some code + name = name.TrimEnd('\\', '/'); // I still hate that Windows filenames use the escape character as a separator + // search scripts included in game save + ScriptStruct[] integratedScripts = entitiesDB.QueryEntities(ScriptBuilder.ScriptGroup).ToFastAccess(out uint integratedScriptsCount); + for (int i = 0; i < integratedScriptsCount; i++) + { + if (integratedScripts[i].name == name) + { + // matching script found in game save + return URI_INTERNAL + name; + } + } + // search scripts on disk + try + { + FileInfo file = new FileInfo(name); + if (file.Exists) + { + return URI_FILE + name; + } + } + catch (Exception e) when ( + e is System.Security.SecurityException + || e is ArgumentException + || e is UnauthorizedAccessException + || e is PathTooLongException + || e is NotSupportedException) + { } + return null; + } + + private string getScheme(string uriName) + { + string[] splitName = uriName.Split(new string[] { @"://" }, StringSplitOptions.None); + if (splitName.Length == 2) + { + return splitName[0] + "://"; + } + return null; + } + + private string getPath(string uriName) + { + string[] splitName = uriName.Split(new string[] { @"://" }, StringSplitOptions.None); + if (splitName.Length == 2) + { + return splitName[1]; + } + return null; + } + + private string getScript(string uriName) + { + string uri_scheme = getScheme(uriName); + if (uri_scheme == null) return null; + string uri_path = getPath(uriName); + if (uri_path == null) return null; + switch (uri_scheme) + { + case URI_IMMEDIATE: + return uri_path; + case URI_HTTP: // download from web + case URI_HTTPS: +#if DEBUG + Logging.CommandLogWarning("Executing code from the Internet is dangerous! \nWeb scripts are only available in DEVELOPMENT versions of GamecraftScripting."); + try + { + WebRequest req = WebRequest.Create(uriName); + req.Method = "GET"; + HttpWebResponse resp = (HttpWebResponse)req.GetResponse(); + byte[] body = new byte[int.Parse(resp.GetResponseHeader("Content-Length"))]; + resp.GetResponseStream().Read(body, 0, body.Length); + return Encoding.ASCII.GetString(body); + } + catch (Exception) + { + return null; + } +#else + Logging.CommandLogError("Executing code from the Internet is dangerous! \nWeb scripts are only available in DEVELOPMENT versions of GamecraftScripting."); + return null; +#endif + case URI_INTERNAL: // found in game file (already deserialized to memory) + ScriptStruct[] internalScripts = entitiesDB.QueryEntities(ScriptBuilder.ScriptGroup).ToFastAccess(out uint scriptCount); + for (int i = 0; i < scriptCount; i++) + { + if (internalScripts[i].name == uri_path) + { + return internalScripts[i].script; + } + } + return null; + case URI_BUILTIN: + // TODO: for future implementation + return null; + case URI_FILE: + try + { + return File.ReadAllText(uri_path); + } + catch (Exception) + { + return null; + } + case URI_ANSWER: + return "print 42"; + default: + return null; + } + } + + private void Execute(string uriName, string script, bool logError = true) + { + ScriptSource src = PythonEnvironment.CurrentEngine.CreateScriptSourceFromString(script); + try + { + src.Execute(PythonEnvironment.CurrentScope); + } + catch (Exception e) + { + if (logError) + { + Logging.CommandLogError($"Python error in '{uriName}': \n{e.Message}"); + } + else + { + throw e; + } + + } + } + } +} diff --git a/GamecraftScripting/Commands/PythonRunnerCommandEngine.cs b/GamecraftScripting/Commands/PythonRunnerCommandEngine.cs index 2c17370..207c1ff 100644 --- a/GamecraftScripting/Commands/PythonRunnerCommandEngine.cs +++ b/GamecraftScripting/Commands/PythonRunnerCommandEngine.cs @@ -9,6 +9,7 @@ using Svelto.ECS; using Microsoft.Scripting.Hosting; using GamecraftScripting.Environment; +using GamecraftScripting.Serialization; namespace GamecraftScripting.Commands { @@ -24,12 +25,16 @@ namespace GamecraftScripting.Commands { CommandRegistrationHelper.Unregister("RunPythonScript"); CommandRegistrationHelper.Unregister("RunPython"); + CommandRegistrationHelper.Unregister("RunGamePython"); + CommandRegistrationHelper.Unregister("ListGameScripts"); } public void Ready() { CommandRegistrationHelper.Register("RunPythonScript", RunPythonScript, "Run a python script stored on your computer"); CommandRegistrationHelper.Register("RunPython", RunPythonString, "Run a python argument"); + CommandRegistrationHelper.Register("RunGamePython", RunIntegratedScript, "Run a python script stored in the game file"); + CommandRegistrationHelper.Register("ListGameScripts", ListIntegratedScripts, "List python scripts stored in the game file"); // create insecure Python namespace ScriptEngine pyEngine = PythonEnvironment.CreateEngine(); ScriptScope pyScope = PythonEnvironment.CreateScope(pyEngine); @@ -38,6 +43,37 @@ namespace GamecraftScripting.Commands //Logging.MetaLog(string.Join(", ", pyEngine.GetSearchPaths().ToArray())); } + private void ListIntegratedScripts() + { + string result = ""; + ScriptStruct[] scripts = entitiesDB.QueryEntities(ScriptBuilder.ScriptGroup).ToFastAccess(out uint count); + for (uint i = 0u; i < count; i++) + { + result += scripts[i].name + " \n"; + } + Logging.CommandLog($"Found {count} integrated script(s) \n{result}"); + } + + private void RunIntegratedScript(string scriptName) + { + if (scriptName == null) return; + if (scriptName.StartsWith("exmods://", StringComparison.InvariantCultureIgnoreCase)) + { + // TODO: Lookup for built-in scripts + return; + } + ScriptStruct[] scripts = entitiesDB.QueryEntities(ScriptBuilder.ScriptGroup).ToFastAccess(out uint count); + for (uint i = 0u; i < count; i++) + { + if (scripts[i].name == scriptName) + { + ExecuteScript(scripts[i].script); + return; + } + } + Logging.CommandLogError("Script not found"); + } + private void RunPythonScript(string scriptPath) { string script = File.ReadAllText(scriptPath); diff --git a/GamecraftScripting/Commands/SerializationCommandEngine.cs b/GamecraftScripting/Commands/SerializationCommandEngine.cs new file mode 100644 index 0000000..dffc5fa --- /dev/null +++ b/GamecraftScripting/Commands/SerializationCommandEngine.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using Svelto.ECS; +using GamecraftModdingAPI.Commands; +using GamecraftModdingAPI.Utility; + +namespace GamecraftScripting.Commands +{ + public class SerializationCommandEngine : ICustomCommandEngine + { + public string Description => "Save a script into a Gamecraft game file"; + + public string Name => "IntegrateScript"; + + public EntitiesDB entitiesDB { set; private get; } + + internal static IEntityFactory entityFactory = null; + + public void Dispose() + { + CommandRegistrationHelper.Unregister(Name); + } + + public void Ready() + { + CommandRegistrationHelper.Register(Name, persistScript, Description); + } + + private void persistScript(string filepath) + { + if (entityFactory == null) return; + EGID scriptId = Serialization.ScriptBuilder.BuildScriptEntity(filepath, File.ReadAllText(filepath), entityFactory); + Logging.MetaLog($"Created persistent script with id {scriptId.entityID}"); + } + } +} diff --git a/GamecraftScripting/GamecraftScripting.csproj b/GamecraftScripting/GamecraftScripting.csproj index ab55180..87a0c2f 100644 --- a/GamecraftScripting/GamecraftScripting.csproj +++ b/GamecraftScripting/GamecraftScripting.csproj @@ -90,9 +90,6 @@ ..\ref\Gamecraft_Data\Managed\Havok.Physics.Hybrid.dll - - ..\ref\Gamecraft_Data\Managed\HdgRemoteDebugRuntime.dll - ..\ref\Gamecraft_Data\Managed\IllusionInjector.dll @@ -123,9 +120,6 @@ ..\ref\Gamecraft_Data\Managed\Rewired_Windows.dll - - ..\ref\Gamecraft_Data\Managed\Robocraft.MainGame.AutoEnterSimulation.dll - ..\ref\Gamecraft_Data\Managed\RobocraftX.AccountPreferences.dll @@ -150,15 +144,9 @@ ..\ref\Gamecraft_Data\Managed\RobocraftX.Crosshair.dll - - ..\ref\Gamecraft_Data\Managed\RobocraftX.EntityStreamUtility.dll - ..\ref\Gamecraft_Data\Managed\RobocraftX.FrontEnd.dll - - ..\ref\Gamecraft_Data\Managed\RobocraftX.GameSignalHandling.dll - ..\ref\Gamecraft_Data\Managed\RobocraftX.GUI.DebugDisplay.dll @@ -171,18 +159,12 @@ ..\ref\Gamecraft_Data\Managed\RobocraftX.GUI.ScaleGhost.dll - - ..\ref\Gamecraft_Data\Managed\RobocraftX.GUI.SignalLabel.dll - ..\ref\Gamecraft_Data\Managed\RobocraftX.GUIs.WorkshopPrefabs.dll ..\ref\Gamecraft_Data\Managed\RobocraftX.Input.dll - - ..\ref\Gamecraft_Data\Managed\RobocraftX.Inventory.dll - ..\ref\Gamecraft_Data\Managed\RobocraftX.MachineEditor.dll @@ -216,9 +198,6 @@ ..\ref\Gamecraft_Data\Managed\RobocraftX.Player.dll - - ..\ref\Gamecraft_Data\Managed\RobocraftX.Priority.dll - ..\ref\Gamecraft_Data\Managed\RobocraftX.Rendering.dll @@ -288,12 +267,6 @@ ..\ref\Gamecraft_Data\Managed\Unity.Entities.Hybrid.dll - - ..\ref\Gamecraft_Data\Managed\Unity.Entities.Properties.dll - - - ..\ref\Gamecraft_Data\Managed\Unity.Entities.StaticTypeRegistry.dll - ..\ref\Gamecraft_Data\Managed\Unity.Jobs.dll @@ -324,9 +297,6 @@ ..\ref\Gamecraft_Data\Managed\Unity.RenderPipelines.Core.ShaderLibrary.dll - - ..\ref\Gamecraft_Data\Managed\Unity.RenderPipelines.Lightweight.Runtime.dll - ..\ref\Gamecraft_Data\Managed\Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary.dll @@ -396,9 +366,6 @@ ..\ref\Gamecraft_Data\Managed\UnityEngine.DSPGraphModule.dll - - ..\ref\Gamecraft_Data\Managed\UnityEngine.FileSystemHttpModule.dll - ..\ref\Gamecraft_Data\Managed\UnityEngine.GameCenterModule.dll diff --git a/GamecraftScripting/Plugin.cs b/GamecraftScripting/Plugin.cs index 8b1c7bf..44158eb 100644 --- a/GamecraftScripting/Plugin.cs +++ b/GamecraftScripting/Plugin.cs @@ -9,6 +9,7 @@ using IllusionPlugin; using UnityEngine; using GamecraftModdingAPI; using GamecraftModdingAPI.Commands; +using Harmony; namespace GamecraftScripting { @@ -19,24 +20,33 @@ namespace GamecraftScripting public string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString(); - + + private HarmonyInstance harmony = null; public void OnApplicationQuit() { + var currentAssembly = Assembly.GetExecutingAssembly(); + harmony.UnpatchAll(currentAssembly.GetName().Name); + harmony = null; Main.Shutdown(); } public void OnApplicationStart() { Main.Init(); + var currentAssembly = Assembly.GetExecutingAssembly(); + harmony = HarmonyInstance.Create(currentAssembly.GetName().Name); + harmony.PatchAll(currentAssembly); // register development commands #if DEBUG - //CommandManager.AddCommand(new SimpleCustomCommandEngine(() => { Application.Quit(); }, "Exit", "Exit the game immediately")); + #endif // debugging commands CommandManager.AddCommand(new Commands.DebugCommandEngine()); // functional commands CommandManager.AddCommand(new Commands.PythonRunnerCommandEngine()); + CommandManager.AddCommand(new Commands.ExecuteCommandEngine()); + CommandManager.AddCommand(new Commands.SerializationCommandEngine()); } public void OnFixedUpdate() { } diff --git a/GamecraftScripting/Serialization/DeserializeFromDiskEntitiesEnginePatch.cs b/GamecraftScripting/Serialization/DeserializeFromDiskEntitiesEnginePatch.cs new file mode 100644 index 0000000..a8fcce4 --- /dev/null +++ b/GamecraftScripting/Serialization/DeserializeFromDiskEntitiesEnginePatch.cs @@ -0,0 +1,74 @@ +using System; +using System.Text; +using System.Reflection; +using Harmony; +using RobocraftX.Common; +using Svelto.DataStructures; +using Svelto.ECS; +using Svelto.ECS.Serialization; + +namespace GamecraftScripting.Serialization +{ + [HarmonyPatch] + class DeserializeFromDiskEntitiesEnginePatch + { + internal static IEntityFactory entityFactory = null; + + private static readonly byte[] frameStart = (new UTF8Encoding()).GetBytes("GamecraftScripting"); + + public static void Prefix(ref ISerializationData ____serializationData, ref FasterList ____bytesStream, ref IEntitySerialization ____entitySerializer) + { + ____entitySerializer.RegisterSerializationFactory(new ScriptDeserializationFactory(entityFactory)); + uint scriptDataStart = ____serializationData.dataPos; + GamecraftModdingAPI.Utility.Logging.MetaLog($"dataPos: {scriptDataStart}"); + BinaryBufferReader bbr = new BinaryBufferReader(____bytesStream.ToArrayFast(out uint count), ____serializationData.dataPos); + byte[] frameBuffer = new byte[frameStart.Length]; + GamecraftModdingAPI.Utility.Logging.MetaLog($"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(); + GamecraftModdingAPI.Utility.Logging.MetaLog($"Buffer byte 0: {(new UTF8Encoding()).GetString(new byte[]{frameBuffer[0]})}"); + while (frameBuffer[i] == frameStart[i]) + { + i++; + if (i == frameStart.Length) break; + frameBuffer[i] = bbr.ReadByte(); + GamecraftModdingAPI.Utility.Logging.MetaLog($"Buffer byte {i}: {(new UTF8Encoding()).GetString(new byte[] { frameBuffer[i] })}"); + } + if (i == frameStart.Length) break; + } + // abort if at end of file + if (bbr.Position >= count - frameStart.Length) + { + GamecraftModdingAPI.Utility.Logging.MetaLog("Skipping script deserialization (no frame found)"); + return; + } + ____serializationData.dataPos = bbr.Position; + GamecraftModdingAPI.Utility.Logging.MetaLog($"dataPos (after frame): {____serializationData.dataPos}"); + uint scriptCount = bbr.ReadUint(); + GamecraftModdingAPI.Utility.Logging.MetaLog($"scriptCount: {scriptCount}"); + uint potentialStart = bbr.ReadUint(); + if (potentialStart == ____serializationData.dataPos - frameStart.Length) return; + ____serializationData.dataPos += 4u; + for (uint j = 0; j < scriptCount; j++) + { + EGID newScriptId = new EGID(j, ScriptBuilder.ScriptGroup); + // why doesn't \/this\/ do anything? + ____entitySerializer.DeserializeNewEntity(newScriptId, ____serializationData, SerializationType.Storage); + } + bbr = new BinaryBufferReader(____bytesStream.ToArrayFast(out count), ____serializationData.dataPos); + uint actualStart = bbr.ReadUint(); + GamecraftModdingAPI.Utility.Logging.MetaLog($"actualStart: {actualStart}"); + GamecraftModdingAPI.Utility.Logging.MetaLog($"Deserialized {scriptCount} scripts starting at {scriptDataStart} ({actualStart})"); + ____serializationData.dataPos = scriptDataStart; // change back to original end point (just in case) + } + + public static MethodBase TargetMethod() + { + return AccessTools.Method("RobocraftX.SaveAndLoad.DeserializeFromDiskEntitiesEngine:LoadingFinished");//AccessTools.TypeByName("RobocraftX.SaveAndLoad.DeserializeFromDiskEntities") + } + } +} diff --git a/GamecraftScripting/Serialization/SaveAndLoadCompositionRootPatch.cs b/GamecraftScripting/Serialization/SaveAndLoadCompositionRootPatch.cs new file mode 100644 index 0000000..b264fdc --- /dev/null +++ b/GamecraftScripting/Serialization/SaveAndLoadCompositionRootPatch.cs @@ -0,0 +1,17 @@ +using System; +using Harmony; +using RobocraftX.SaveAndLoad; +using Svelto.ECS; +namespace GamecraftScripting.Serialization +{ + [HarmonyPatch(typeof(SaveAndLoadCompositionRoot), "Compose")] + class SaveAndLoadCompositionRootPatch + { + public static void Prefix(EnginesRoot enginesRoot) + { + IEntityFactory factory = enginesRoot.GenerateEntityFactory(); + DeserializeFromDiskEntitiesEnginePatch.entityFactory = factory; + Commands.SerializationCommandEngine.entityFactory = factory; + } + } +} diff --git a/GamecraftScripting/Serialization/SaveGameEnginePatch.cs b/GamecraftScripting/Serialization/SaveGameEnginePatch.cs new file mode 100644 index 0000000..464bec2 --- /dev/null +++ b/GamecraftScripting/Serialization/SaveGameEnginePatch.cs @@ -0,0 +1,57 @@ +using System; +using System.Text; +using System.Reflection; +using Harmony; +using RobocraftX.Common; +using RobocraftX.SaveAndLoad; +using Svelto.DataStructures; +using Svelto.ECS; +using Svelto.ECS.Serialization; + +using GamecraftModdingAPI.Utility; + +namespace GamecraftScripting.Serialization +{ + [HarmonyPatch] + class SaveGameEnginePatch + { + private static readonly byte[] frameStart = (new UTF8Encoding()).GetBytes("GamecraftScripting"); + + public static void Postfix(ref ISerializationData serializationData, EntitiesDB entitiesDB, IEntitySerialization entitySerializer) + { + Logging.MetaLog("Running Postfix on game save serializer"); + serializationData.data.ExpandBy((uint)frameStart.Length); + BinaryBufferWriter bbw = new BinaryBufferWriter(serializationData.data.ToArrayFast(out uint buffLen), serializationData.dataPos); + ScriptStruct[] scripts = entitiesDB.QueryEntities(ScriptBuilder.ScriptGroup).ToFastAccess(out uint count); + uint scriptDataStart = serializationData.dataPos; + GamecraftModdingAPI.Utility.Logging.MetaLog($"dataPos: {scriptDataStart}"); + for (int i = 0; i < frameStart.Length; i++) + { + bbw.Write(frameStart[i]); + } + serializationData.dataPos += (uint)frameStart.Length; + GamecraftModdingAPI.Utility.Logging.MetaLog($"dataPos (after frame start): {serializationData.dataPos}"); + serializationData.data.ExpandBy(4u); + Logging.MetaLog($"scriptCount: {count}"); + bbw.Write(count); + serializationData.dataPos += 4u; + //foreach (byte b in BitConverter.GetBytes(count)) serializationData.data.Add(b); + for (uint i = 0; i < count; i++) + { + EGID scriptId = new EGID(i, ScriptBuilder.ScriptGroup); + entitySerializer.SerializeEntity(scriptId, serializationData, SerializationType.Storage); + } + bbw = new BinaryBufferWriter(serializationData.data.ToArrayFast(out buffLen), serializationData.dataPos); + serializationData.data.ExpandBy(4u); + bbw.Write(scriptDataStart); + serializationData.dataPos += 4u; + //foreach (byte b in BitConverter.GetBytes(scriptDataStart)) serializationData.data.Add(b); + serializationData.data.Trim(); + } + + public static MethodBase TargetMethod() + { + return typeof(SaveGameEngine).GetMethod("SerializeGameToBuffer"); + } + } +} diff --git a/GamecraftScripting/Serialization/ScriptBuilder.cs b/GamecraftScripting/Serialization/ScriptBuilder.cs new file mode 100644 index 0000000..29d2a90 --- /dev/null +++ b/GamecraftScripting/Serialization/ScriptBuilder.cs @@ -0,0 +1,24 @@ +using System; +using Svelto.ECS; +using Svelto.ECS.Experimental; + +namespace GamecraftScripting.Serialization +{ + public static class ScriptBuilder + { + public static readonly ExclusiveGroup ScriptGroup = new ExclusiveGroup("SCRIPT_GROUP"); + + private static uint nextScriptId = 0u; + + public static EGID BuildScriptEntity(string name, string script, IEntityFactory entityFactory) + { + EGID scriptId = new EGID(nextScriptId++, ScriptGroup); + EntityStructInitializer builder = entityFactory.BuildEntity(scriptId); + builder.Init(new ScriptStruct { + name = new ECSString(name), + script = new ECSString(script), + }); + return scriptId; + } + } +} diff --git a/GamecraftScripting/Serialization/ScriptDeserializationFactory.cs b/GamecraftScripting/Serialization/ScriptDeserializationFactory.cs new file mode 100644 index 0000000..4055dc6 --- /dev/null +++ b/GamecraftScripting/Serialization/ScriptDeserializationFactory.cs @@ -0,0 +1,25 @@ +using System; +using Svelto.ECS; +using Svelto.ECS.Serialization; +using GamecraftModdingAPI.Utility; +namespace GamecraftScripting.Serialization +{ + public class ScriptDeserializationFactory : IDeserializationFactory + { + private IEntityFactory entityFactory; + + public ScriptDeserializationFactory(IEntityFactory entityFactory) + { + this.entityFactory = entityFactory; + } + + public EntityStructInitializer BuildDeserializedEntity(EGID egid, ISerializationData serializationData, ISerializableEntityDescriptor entityDescriptor, SerializationType serializationType, IEntitySerialization entitySerialization) + { + Logging.MetaLog(entityFactory == null); + EntityStructInitializer esi = entityFactory.BuildEntity(egid); + entitySerialization.DeserializeEntityStructs(serializationData, entityDescriptor, ref esi, serializationType); + Logging.MetaLog($"Deserialized script named {esi.Get().name.ToString()}"); + return esi; + } + } +} diff --git a/GamecraftScripting/Serialization/ScriptEntityDescriptor.cs b/GamecraftScripting/Serialization/ScriptEntityDescriptor.cs new file mode 100644 index 0000000..2d4a835 --- /dev/null +++ b/GamecraftScripting/Serialization/ScriptEntityDescriptor.cs @@ -0,0 +1,28 @@ +using System; +using Svelto.ECS; +using Svelto.ECS.Serialization; + +namespace GamecraftScripting.Serialization +{ + public class ScriptEntityDescriptor : SerializableEntityDescriptor + { + [HashName("GamecraftScriptingScriptEntityDescriptorV0")] + public class _ScriptEntityDescriptor : IEntityDescriptor + { + public IEntityBuilder[] entitiesToBuild => _entityBuilders; + + private static readonly IEntityBuilder[] _entityBuilders = new IEntityBuilder[1] + { + new SerializableEntityBuilder((SerializationType.Network, new ScriptSerializer()), + (SerializationType.Storage, new ScriptSerializer())) + }; + } + + public ScriptEntityDescriptor() : base() + { + GamecraftModdingAPI.Utility.Logging.MetaLog("ScriptEntityDescriptor Initialized"); + GamecraftModdingAPI.Utility.Logging.MetaLog($"Entities to serialize: {entitiesToSerialize.Length}"); + GamecraftModdingAPI.Utility.Logging.MetaLog($"Entities to serialize: {entitiesToBuild.Length}"); + } + } +} diff --git a/GamecraftScripting/Serialization/ScriptSerializer.cs b/GamecraftScripting/Serialization/ScriptSerializer.cs new file mode 100644 index 0000000..a888ce9 --- /dev/null +++ b/GamecraftScripting/Serialization/ScriptSerializer.cs @@ -0,0 +1,61 @@ +using System; +using System.Text; + +using RobocraftX.Common; +using Svelto.ECS; +using Svelto.ECS.Serialization; + +namespace GamecraftScripting.Serialization +{ + public class ScriptSerializer : ISerializer + { + private static readonly UTF8Encoding utf8Encoding = new UTF8Encoding(); + + private static readonly uint padding = 42u; + + public uint size => 0u; + + public bool Deserialize(ref ScriptStruct value, ISerializationData serializationData) + { + GamecraftModdingAPI.Utility.Logging.MetaLog($"dataPos: {serializationData.dataPos}"); + BinaryBufferReader bbr = new BinaryBufferReader(serializationData.data.ToArrayFast(out uint count), serializationData.dataPos); + short nameLength = bbr.ReadShort(); + GamecraftModdingAPI.Utility.Logging.MetaLog($"nameLength: {nameLength}"); + byte[] serialName = new byte[nameLength]; + bbr.ReadBytes(serialName, (uint)nameLength); + value.name.Set(utf8Encoding.GetString(serialName)); + int scriptLength = bbr.ReadInt(); + GamecraftModdingAPI.Utility.Logging.MetaLog($"scriptLength: {scriptLength}"); + byte[] serialScript = new byte[scriptLength]; + bbr.ReadBytes(serialScript, (uint)scriptLength); + value.script.Set(utf8Encoding.GetString(serialScript)); + uint serialPadding = bbr.ReadUint(); + serializationData.dataPos += (uint)(2 + nameLength + 4 + scriptLength + 4); + GamecraftModdingAPI.Utility.Logging.MetaLog($"Deserializing Script successful: {serialPadding == padding}"); + return true; + } + + public bool Serialize(in ScriptStruct value, ISerializationData serializationData) + { + GamecraftModdingAPI.Utility.Logging.MetaLog($"dataPos: {serializationData.dataPos}"); + byte[] serialName = utf8Encoding.GetBytes(value.name); + byte[] serialScript = utf8Encoding.GetBytes(value.script); + uint actualSize = (uint)(2 + serialName.Length + 4 + serialScript.Length + 4); + serializationData.data.ExpandBy(actualSize); + BinaryBufferWriter bbw = new BinaryBufferWriter(serializationData.data.ToArrayFast(out uint count), serializationData.dataPos); + bbw.Write((short)serialName.Length); + for (int i = 0; i < serialName.Length; i++) + { + bbw.Write(serialName[i]); + } + bbw.Write((int)serialScript.Length); + for (int i = 0; i < serialScript.Length; i++) + { + bbw.Write(serialScript[i]); + } + bbw.Write(padding); + serializationData.dataPos += actualSize; + return true; + } + } +} diff --git a/GamecraftScripting/Serialization/ScriptStruct.cs b/GamecraftScripting/Serialization/ScriptStruct.cs new file mode 100644 index 0000000..e5f4530 --- /dev/null +++ b/GamecraftScripting/Serialization/ScriptStruct.cs @@ -0,0 +1,13 @@ +using System; +using Svelto.ECS; +using Svelto.ECS.Experimental; + +namespace GamecraftScripting.Serialization +{ + public struct ScriptStruct : IEntityStruct + { + public ECSString name; + + public ECSString script; + } +} diff --git a/GamecraftScripting/Serialization/SerializationDescriptorMapPatch.cs b/GamecraftScripting/Serialization/SerializationDescriptorMapPatch.cs new file mode 100644 index 0000000..4d3cdfa --- /dev/null +++ b/GamecraftScripting/Serialization/SerializationDescriptorMapPatch.cs @@ -0,0 +1,60 @@ +using System; +using System.Reflection; +using Harmony; +using Svelto.ECS; +using Svelto.ECS.Serialization; +using GamecraftModdingAPI.Utility; + +namespace GamecraftScripting.Serialization +{ +#if DEBUG + [HarmonyPatch] + public class SerializationDescriptorMapPatch + { + public static void Prefix(ISerializableEntityDescriptor descriptor) + { + if (descriptor.entitiesToSerialize.Length == 0) + { + if (descriptor.entitiesToBuild.Length != 0) + { + Logging.MetaLog($"Descriptor: {descriptor.entitiesToBuild[0].GetType().FullName} (hash:{descriptor.hash})"); + } + else + { + Logging.MetaLog($"Emtpy descriptor (hash: {descriptor.hash})"); + } + + } + else + { + Logging.MetaLog($"Descriptor: {descriptor.entitiesToSerialize[0].GetType().FullName} (hash:{descriptor.hash}) (serializables: {descriptor.entitiesToSerialize.Length})"); + } + } + + public static MethodBase TargetMethod() + { + //Logging.MetaLog(AccessTools.Method(AccessTools.Inner(AccessTools.TypeByName("Svelto.ECS.EnginesRoot"), "SerializationDescriptorMap"), "RegisterEntityDescriptor") == null); + return AccessTools.Method(AccessTools.Inner(AccessTools.TypeByName("Svelto.ECS.EnginesRoot"), "SerializationDescriptorMap"), "RegisterEntityDescriptor"); + } + } + + [HarmonyPatch] + public class SerializationDescriptorMapGetPatch + { + public static void Prefix(uint descriptorID) + { + if (descriptorID == 3520129338u) + { + Logging.MetaLog("Got hash for ScriptStruct"); + } + Logging.MetaLog($"GetDescriptorFromHash({descriptorID})"); + } + + public static MethodBase TargetMethod() + { + //Logging.MetaLog(AccessTools.Method(AccessTools.Inner(AccessTools.TypeByName("Svelto.ECS.EnginesRoot"), "SerializationDescriptorMap"), "RegisterEntityDescriptor") == null); + return AccessTools.Method(AccessTools.Inner(AccessTools.TypeByName("Svelto.ECS.EnginesRoot"), "SerializationDescriptorMap"), "GetDescriptorFromHash"); + } + } +#endif +}