From 89d32956d950e1eb08ac9c3844ec96c748108eaf Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Mon, 13 Jul 2020 21:55:48 +0200 Subject: [PATCH] Automatically invoke the correct block constructor And store delegates of dynamic methods invoking constructors Tested with the automated tests --- GamecraftModdingAPI/Block.cs | 107 +++++++++++++----- GamecraftModdingAPI/Blocks/BlockTests.cs | 3 +- GamecraftModdingAPI/Blocks/ConsoleBlock.cs | 30 ++--- GamecraftModdingAPI/Blocks/Motor.cs | 23 ---- GamecraftModdingAPI/Blocks/Piston.cs | 25 +--- GamecraftModdingAPI/Blocks/Servo.cs | 23 ---- GamecraftModdingAPI/Blocks/SignalingBlock.cs | 19 ---- GamecraftModdingAPI/Blocks/SpawnPoint.cs | 25 +--- GamecraftModdingAPI/Blocks/TextBlock.cs | 15 --- GamecraftModdingAPI/Blocks/Timer.cs | 17 --- .../Tests/GamecraftModdingAPIPluginTest.cs | 22 ++++ 11 files changed, 114 insertions(+), 195 deletions(-) diff --git a/GamecraftModdingAPI/Block.cs b/GamecraftModdingAPI/Block.cs index 16a0c81..673ef49 100644 --- a/GamecraftModdingAPI/Block.cs +++ b/GamecraftModdingAPI/Block.cs @@ -1,6 +1,7 @@ using System; -using System.Reflection; -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; using Svelto.ECS; using Svelto.ECS.EntityStructs; @@ -67,10 +68,6 @@ namespace GamecraftModdingAPI /// Place a new block at the given position. If scaled, position means the center of the block. The default block size is 0.2 in terms of position. /// Place blocks next to each other to connect them. /// The placed block will be a complete block with a placement grid and collision which will be saved along with the game. - /// - /// This method waits for the block to be constructed in the game which may take a significant amount of time. - /// Only use this to place a single block. - /// For placing multiple blocks, use PlaceNew() then AsyncUtils.WaitForSubmission() when done with placing blocks. /// /// The block's type /// The block's color @@ -81,23 +78,15 @@ namespace GamecraftModdingAPI /// The block's non-uniform scale - 0 means is used /// The player who placed the block /// The placed block or null if failed - public static async Task PlaceNewAsync(BlockIDs block, float3 position, + public static T PlaceNew(BlockIDs block, float3 position, float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, - int uscale = 1, float3 scale = default, Player player = null) + int uscale = 1, float3 scale = default, Player player = null) where T : Block { if (PlacementEngine.IsInGame && GameState.IsBuildMode()) { - try - { - var ret = new Block(PlacementEngine.PlaceBlock(block, color, darkness, - position, uscale, scale, player, rotation)); - await AsyncUtils.WaitForSubmission(); - return ret; - } - catch (Exception e) - { - Logging.MetaDebugLog(e); - } + var egid = PlacementEngine.PlaceBlock(block, color, darkness, + position, uscale, scale, player, rotation); + return New(egid.entityID, egid.groupID); } return null; @@ -109,7 +98,7 @@ namespace GamecraftModdingAPI /// The block object public static Block GetLastPlacedBlock() { - return new Block(BlockIdentifiers.LatestBlockID); + return New(BlockIdentifiers.LatestBlockID); } /// @@ -130,6 +119,75 @@ namespace GamecraftModdingAPI remove => BlockEventsEngine.Removed -= value; } + private static Dictionary> initializers = new Dictionary>(); + + private static Dictionary typeToGroup = + new Dictionary + { + {typeof(ConsoleBlock), new[] {CommonExclusiveGroups.BUILD_CONSOLE_BLOCK_GROUP}}, + {typeof(Motor), new[] {CommonExclusiveGroups.BUILD_MOTOR_BLOCK_GROUP}}, + {typeof(Piston), new[] {CommonExclusiveGroups.BUILD_PISTON_BLOCK_GROUP}}, + {typeof(Servo), new[] {CommonExclusiveGroups.BUILD_SERVO_BLOCK_GROUP}}, + { + typeof(SpawnPoint), + new[] + { + CommonExclusiveGroups.BUILD_SPAWNPOINT_BLOCK_GROUP, + CommonExclusiveGroups.BUILD_BUILDINGSPAWN_BLOCK_GROUP + } + }, + {typeof(TextBlock), new[] {CommonExclusiveGroups.BUILD_TEXT_BLOCK_GROUP}}, + {typeof(Timer), new[] {CommonExclusiveGroups.BUILD_TIMER_BLOCK_GROUP}} + }; + + private static T New(uint id, ExclusiveGroupStruct? group = null) where T : Block + { + var type = typeof(T); + EGID egid; + if (!group.HasValue) + { + if (typeToGroup.TryGetValue(type, out var gr) && gr.Length == 1) + egid = new EGID(id, gr[0]); + else + egid = BlockEngine.FindBlockEGID(id) ?? throw new BlockTypeException("Could not find block group!"); + } + else + { + egid = new EGID(id, group.Value); + if (typeToGroup.TryGetValue(type, out var gr) + && gr.All(egs => egs != group.Value)) //If this subclass has a specific group, then use that - so Block should still work + throw new BlockTypeException($"Incompatible block type! Type {type.Name} belongs to group {gr.Select(g => g.ToString()).Aggregate((a, b) => a + ", " + b)} instead of {group.Value}"); + } + + if (initializers.TryGetValue(type, out var func)) + { + var bl = (T) func(egid); + return bl; + } + + //https://stackoverflow.com/a/10593806/2703239 + var ctor = type.GetConstructor(new[] {typeof(EGID)}); + if (ctor == null) + throw new MissingMethodException("There is no constructor with an EGID parameter for this object"); + DynamicMethod dynamic = new DynamicMethod(string.Empty, + type, + new[] {typeof(EGID)}, + type); + ILGenerator il = dynamic.GetILGenerator(); + + il.DeclareLocal(type); + il.Emit(OpCodes.Ldarg_0); //Load EGID and pass to constructor + il.Emit(OpCodes.Newobj, ctor); //Call constructor + il.Emit(OpCodes.Stloc_0); + il.Emit(OpCodes.Ldloc_0); + il.Emit(OpCodes.Ret); + + func = (Func) dynamic.CreateDelegate(typeof(Func)); + initializers.Add(type, func); + var block = (T) func(egid); + return block; + } + public Block(EGID id) { Id = id; @@ -344,12 +402,9 @@ namespace GamecraftModdingAPI // C# can't cast to a child of Block unless the object was originally that child type // And C# doesn't let me make implicit cast operators for child types // So thanks to Microsoft, we've got this horrible implementation using reflection - ConstructorInfo ctor = typeof(T).GetConstructor(types: new System.Type[] { typeof(EGID) }); - if (ctor == null) - { - throw new BlockSpecializationException("Specialized block constructor does not accept an EGID"); - } - return (T)ctor.Invoke(new object[] { Id }); + + //Lets improve that using delegates + return New(Id.entityID, Id.groupID); } #if DEBUG diff --git a/GamecraftModdingAPI/Blocks/BlockTests.cs b/GamecraftModdingAPI/Blocks/BlockTests.cs index ed42024..3b66f28 100644 --- a/GamecraftModdingAPI/Blocks/BlockTests.cs +++ b/GamecraftModdingAPI/Blocks/BlockTests.cs @@ -32,9 +32,8 @@ namespace GamecraftModdingAPI.Blocks [APITestCase(TestType.EditMode)] public static void TestTextBlock() { - Block newBlock = Block.PlaceNew(BlockIDs.TextBlock, Unity.Mathematics.float3.zero + 1); TextBlock textBlock = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler - Assert.Errorless(() => { textBlock = newBlock.Specialise(); }, "Block.Specialize() raised an exception: ", "Block.Specialize() completed without issue."); + Assert.Errorless(() => { textBlock = Block.PlaceNew(BlockIDs.TextBlock, Unity.Mathematics.float3.zero + 1); }, "Block.PlaceNew() raised an exception: ", "Block.PlaceNew() completed without issue."); if (!Assert.NotNull(textBlock, "Block.Specialize() returned null, possibly because it failed silently.", "Specialized TextBlock is not null.")) return; if (!Assert.NotNull(textBlock.Text, "TextBlock.Text is null, possibly because it failed silently.", "TextBlock.Text is not null.")) return; if (!Assert.NotNull(textBlock.TextBlockId, "TextBlock.TextBlockId is null, possibly because it failed silently.", "TextBlock.TextBlockId is not null.")) return; diff --git a/GamecraftModdingAPI/Blocks/ConsoleBlock.cs b/GamecraftModdingAPI/Blocks/ConsoleBlock.cs index 6d4217d..e132029 100644 --- a/GamecraftModdingAPI/Blocks/ConsoleBlock.cs +++ b/GamecraftModdingAPI/Blocks/ConsoleBlock.cs @@ -12,33 +12,19 @@ namespace GamecraftModdingAPI.Blocks { public class ConsoleBlock : Block { - public static ConsoleBlock PlaceNew(float3 position, - float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, - int uscale = 1, float3 scale = default, Player player = null) + public ConsoleBlock(EGID id): base(id) { - if (PlacementEngine.IsInGame && GameState.IsBuildMode()) - { - EGID id = PlacementEngine.PlaceBlock(BlockIDs.ConsoleBlock, color, darkness, - position, uscale, scale, player, rotation); - return new ConsoleBlock(id); - } - - return null; - } - - public ConsoleBlock(EGID id): base(id) - { - if (!BlockEngine.GetBlockInfoExists(this.Id)) - { - throw new BlockTypeException($"Block is not a {this.GetType().Name} block"); - } + if (!BlockEngine.GetBlockInfoExists(this.Id)) + { + throw new BlockTypeException($"Block is not a {this.GetType().Name} block"); + } } - public ConsoleBlock(uint id): base(new EGID(id, CommonExclusiveGroups.BUILD_CONSOLE_BLOCK_GROUP)) + public ConsoleBlock(uint id): base(new EGID(id, CommonExclusiveGroups.BUILD_CONSOLE_BLOCK_GROUP)) { - if (!BlockEngine.GetBlockInfoExists(this.Id)) + if (!BlockEngine.GetBlockInfoExists(this.Id)) { - throw new BlockTypeException($"Block is not a {this.GetType().Name} block"); + throw new BlockTypeException($"Block is not a {this.GetType().Name} block"); } } diff --git a/GamecraftModdingAPI/Blocks/Motor.cs b/GamecraftModdingAPI/Blocks/Motor.cs index 55f3649..fde2920 100644 --- a/GamecraftModdingAPI/Blocks/Motor.cs +++ b/GamecraftModdingAPI/Blocks/Motor.cs @@ -11,29 +11,6 @@ namespace GamecraftModdingAPI.Blocks { public class Motor : Block { - /// - /// Places a new motor. - /// Any valid motor type is accepted. - /// This re-implements Block.PlaceNew(...) - /// - public static new Motor PlaceNew(BlockIDs block, float3 position, - float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, - int uscale = 1, float3 scale = default, Player player = null) - { - if (!(block == BlockIDs.MotorS || block == BlockIDs.MotorM)) - { - throw new BlockTypeException($"Block is not a {typeof(Motor).Name} block"); - } - if (PlacementEngine.IsInGame && GameState.IsBuildMode()) - { - EGID id = PlacementEngine.PlaceBlock(block, color, darkness, - position, uscale, scale, player, rotation); - return new Motor(id); - } - - return null; - } - public Motor(EGID id) : base(id) { if (!BlockEngine.GetBlockInfoExists(this.Id)) diff --git a/GamecraftModdingAPI/Blocks/Piston.cs b/GamecraftModdingAPI/Blocks/Piston.cs index b96a806..2586b60 100644 --- a/GamecraftModdingAPI/Blocks/Piston.cs +++ b/GamecraftModdingAPI/Blocks/Piston.cs @@ -11,30 +11,7 @@ namespace GamecraftModdingAPI.Blocks { public class Piston : Block { - /// - /// Places a new piston. - /// Any valid piston type is accepted. - /// This re-implements Block.PlaceNew(...) - /// - public static new Piston PlaceNew(BlockIDs block, float3 position, - float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, - int uscale = 1, float3 scale = default, Player player = null) - { - if (!(block == BlockIDs.ServoPiston || block == BlockIDs.StepperPiston || block == BlockIDs.PneumaticPiston)) - { - throw new BlockTypeException($"Block is not a {typeof(Piston).Name} block"); - } - if (PlacementEngine.IsInGame && GameState.IsBuildMode()) - { - EGID id = PlacementEngine.PlaceBlock(block, color, darkness, - position, uscale, scale, player, rotation); - return new Piston(id); - } - - return null; - } - - public Piston(EGID id) : base(id) + public Piston(EGID id) : base(id) { if (!BlockEngine.GetBlockInfoExists(this.Id)) { diff --git a/GamecraftModdingAPI/Blocks/Servo.cs b/GamecraftModdingAPI/Blocks/Servo.cs index ef7225b..c4b57fb 100644 --- a/GamecraftModdingAPI/Blocks/Servo.cs +++ b/GamecraftModdingAPI/Blocks/Servo.cs @@ -11,29 +11,6 @@ namespace GamecraftModdingAPI.Blocks { public class Servo : Block { - /// - /// Places a new servo. - /// Any valid servo type is accepted. - /// This re-implements Block.PlaceNew(...) - /// - public static new Servo PlaceNew(BlockIDs block, float3 position, - float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, - int uscale = 1, float3 scale = default, Player player = null) - { - if (!(block == BlockIDs.ServoAxle || block == BlockIDs.ServoHinge || block == BlockIDs.ServoPiston)) - { - throw new BlockTypeException($"Block is not a {nameof(Servo)} block"); - } - if (PlacementEngine.IsInGame && GameState.IsBuildMode()) - { - EGID id = PlacementEngine.PlaceBlock(block, color, darkness, - position, uscale, scale, player, rotation); - return new Servo(id); - } - - return null; - } - public Servo(EGID id) : base(id) { if (!BlockEngine.GetBlockInfoExists(this.Id)) diff --git a/GamecraftModdingAPI/Blocks/SignalingBlock.cs b/GamecraftModdingAPI/Blocks/SignalingBlock.cs index f8006f6..8a62d64 100644 --- a/GamecraftModdingAPI/Blocks/SignalingBlock.cs +++ b/GamecraftModdingAPI/Blocks/SignalingBlock.cs @@ -14,25 +14,6 @@ namespace GamecraftModdingAPI.Blocks /// public class SignalingBlock : Block { - /// - /// Places a new signaling block. - /// Any valid functional block type with IO ports will work. - /// This re-implements Block.PlaceNew(...) - /// - public static new SignalingBlock PlaceNew(BlockIDs block, float3 position, - float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, - int uscale = 1, float3 scale = default, Player player = null) - { - if (PlacementEngine.IsInGame && GameState.IsBuildMode()) - { - EGID id = PlacementEngine.PlaceBlock(block, color, darkness, - position, uscale, scale, player, rotation); - return new SignalingBlock(id); - } - - return null; - } - public SignalingBlock(EGID id) : base(id) { if (!BlockEngine.GetBlockInfoExists(this.Id)) diff --git a/GamecraftModdingAPI/Blocks/SpawnPoint.cs b/GamecraftModdingAPI/Blocks/SpawnPoint.cs index 65ef750..4419e38 100644 --- a/GamecraftModdingAPI/Blocks/SpawnPoint.cs +++ b/GamecraftModdingAPI/Blocks/SpawnPoint.cs @@ -13,30 +13,7 @@ namespace GamecraftModdingAPI.Blocks { public class SpawnPoint : Block { - /// - /// Places a new spawn point. - /// Any valid spawn block type is accepted. - /// This re-implements Block.PlaceNew(...) - /// - public static new SpawnPoint PlaceNew(BlockIDs block, float3 position, - float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, - int uscale = 1, float3 scale = default, Player player = null) - { - if (!(block == BlockIDs.LargeSpawn || block == BlockIDs.SmallSpawn || block == BlockIDs.MediumSpawn || block == BlockIDs.PlayerSpawn)) - { - throw new BlockTypeException($"Block is not a {nameof(SpawnPoint)} block"); - } - if (PlacementEngine.IsInGame && GameState.IsBuildMode()) - { - EGID id = PlacementEngine.PlaceBlock(block, color, darkness, - position, uscale, scale, player, rotation); - return new SpawnPoint(id); - } - - return null; - } - - public SpawnPoint(EGID id) : base(id) + public SpawnPoint(EGID id) : base(id) { if (!BlockEngine.GetBlockInfoExists(this.Id)) { diff --git a/GamecraftModdingAPI/Blocks/TextBlock.cs b/GamecraftModdingAPI/Blocks/TextBlock.cs index c1a9344..94cf212 100644 --- a/GamecraftModdingAPI/Blocks/TextBlock.cs +++ b/GamecraftModdingAPI/Blocks/TextBlock.cs @@ -12,21 +12,6 @@ namespace GamecraftModdingAPI.Blocks { public class TextBlock : Block { - - public static TextBlock PlaceNew(float3 position, - float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, - int uscale = 1, float3 scale = default, Player player = null) - { - if (PlacementEngine.IsInGame && GameState.IsBuildMode()) - { - EGID id = PlacementEngine.PlaceBlock(BlockIDs.TextBlock, color, darkness, - position, uscale, scale, player, rotation); - return new TextBlock(id); - } - - return null; - } - public TextBlock(EGID id) : base(id) { if (!BlockEngine.GetBlockInfoExists(this.Id)) diff --git a/GamecraftModdingAPI/Blocks/Timer.cs b/GamecraftModdingAPI/Blocks/Timer.cs index 0e7f744..2acab5b 100644 --- a/GamecraftModdingAPI/Blocks/Timer.cs +++ b/GamecraftModdingAPI/Blocks/Timer.cs @@ -13,23 +13,6 @@ namespace GamecraftModdingAPI.Blocks { public class Timer : Block { - /// - /// Places a new timer block. - /// - public static Timer PlaceNew(float3 position, - float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0, - int uscale = 1, float3 scale = default, Player player = null) - { - if (PlacementEngine.IsInGame && GameState.IsBuildMode()) - { - EGID id = PlacementEngine.PlaceBlock(BlockIDs.Timer, color, darkness, - position, uscale, scale, player, rotation); - return new Timer(id); - } - - return null; - } - public Timer(EGID id) : base(id) { if (!BlockEngine.GetBlockInfoExists(this.Id)) diff --git a/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs b/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs index fa65ebb..d5cf714 100644 --- a/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs +++ b/GamecraftModdingAPI/Tests/GamecraftModdingAPIPluginTest.cs @@ -232,6 +232,28 @@ namespace GamecraftModdingAPI.Tests } }).Build(); + CommandBuilder.Builder() + .Name("PlaceConsole") + .Description("Place a bunch of console block with a given text") + .Action((float x, float y, float z) => + { + Stopwatch sw = new Stopwatch(); + sw.Start(); + for (int i = 0; i < 100; i++) + { + for (int j = 0; j < 100; j++) + { + var block = Block.PlaceNew(BlockIDs.ConsoleBlock, + new float3(x + i, y, z + j)); + block.Command = "test_command"; + } + } + + sw.Stop(); + Logging.CommandLog($"Blocks placed in {sw.ElapsedMilliseconds} ms"); + }) + .Build(); + GameClient.SetDebugInfo("InstalledMods", InstalledMods); Block.Placed += (sender, args) => Logging.MetaDebugLog("Placed block " + args.Type + " with ID " + args.ID);