And store delegates of dynamic methods invoking constructors Tested with the automated teststags/v2.2.0
@@ -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. | |||
/// <para></para> | |||
/// <para>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.</para> | |||
/// </summary> | |||
/// <param name="block">The block's type</param> | |||
/// <param name="color">The block's color</param> | |||
@@ -81,23 +78,15 @@ namespace GamecraftModdingAPI | |||
/// <param name="scale">The block's non-uniform scale - 0 means <paramref name="uscale"/> is used</param> | |||
/// <param name="player">The player who placed the block</param> | |||
/// <returns>The placed block or null if failed</returns> | |||
public static async Task<Block> PlaceNewAsync(BlockIDs block, float3 position, | |||
public static T PlaceNew<T>(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<T>(egid.entityID, egid.groupID); | |||
} | |||
return null; | |||
@@ -109,7 +98,7 @@ namespace GamecraftModdingAPI | |||
/// <returns>The block object</returns> | |||
public static Block GetLastPlacedBlock() | |||
{ | |||
return new Block(BlockIdentifiers.LatestBlockID); | |||
return New<Block>(BlockIdentifiers.LatestBlockID); | |||
} | |||
/// <summary> | |||
@@ -130,6 +119,75 @@ namespace GamecraftModdingAPI | |||
remove => BlockEventsEngine.Removed -= value; | |||
} | |||
private static Dictionary<Type, Func<EGID, Block>> initializers = new Dictionary<Type, Func<EGID, Block>>(); | |||
private static Dictionary<Type, ExclusiveGroupStruct[]> typeToGroup = | |||
new Dictionary<Type, ExclusiveGroupStruct[]> | |||
{ | |||
{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<T>(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<EGID, T>) dynamic.CreateDelegate(typeof(Func<EGID, T>)); | |||
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<T>(Id.entityID, Id.groupID); | |||
} | |||
#if DEBUG | |||
@@ -30,9 +30,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<TextBlock>(); }, "Block.Specialize<TextBlock>() raised an exception: ", "Block.Specialize<TextBlock>() completed without issue."); | |||
Assert.Errorless(() => { textBlock = Block.PlaceNew<TextBlock>(BlockIDs.TextBlock, Unity.Mathematics.float3.zero + 1); }, "Block.PlaceNew<TextBlock>() raised an exception: ", "Block.PlaceNew<TextBlock>() completed without issue."); | |||
if (!Assert.NotNull(textBlock, "Block.Specialize<TextBlock>() 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; | |||
@@ -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<ConsoleBlockEntityStruct>(this.Id)) | |||
{ | |||
throw new BlockTypeException($"Block is not a {this.GetType().Name} block"); | |||
} | |||
if (!BlockEngine.GetBlockInfoExists<ConsoleBlockEntityStruct>(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<ConsoleBlockEntityStruct>(this.Id)) | |||
if (!BlockEngine.GetBlockInfoExists<ConsoleBlockEntityStruct>(this.Id)) | |||
{ | |||
throw new BlockTypeException($"Block is not a {this.GetType().Name} block"); | |||
throw new BlockTypeException($"Block is not a {this.GetType().Name} block"); | |||
} | |||
} | |||
@@ -11,29 +11,6 @@ namespace GamecraftModdingAPI.Blocks | |||
{ | |||
public class Motor : Block | |||
{ | |||
/// <summary> | |||
/// Places a new motor. | |||
/// Any valid motor type is accepted. | |||
/// This re-implements Block.PlaceNew(...) | |||
/// </summary> | |||
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<MotorReadOnlyStruct>(this.Id)) | |||
@@ -11,30 +11,7 @@ namespace GamecraftModdingAPI.Blocks | |||
{ | |||
public class Piston : Block | |||
{ | |||
/// <summary> | |||
/// Places a new piston. | |||
/// Any valid piston type is accepted. | |||
/// This re-implements Block.PlaceNew(...) | |||
/// </summary> | |||
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<PistonReadOnlyStruct>(this.Id)) | |||
{ | |||
@@ -11,29 +11,6 @@ namespace GamecraftModdingAPI.Blocks | |||
{ | |||
public class Servo : Block | |||
{ | |||
/// <summary> | |||
/// Places a new servo. | |||
/// Any valid servo type is accepted. | |||
/// This re-implements Block.PlaceNew(...) | |||
/// </summary> | |||
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<ServoReadOnlyStruct>(this.Id)) | |||
@@ -14,25 +14,6 @@ namespace GamecraftModdingAPI.Blocks | |||
/// </summary> | |||
public class SignalingBlock : Block | |||
{ | |||
/// <summary> | |||
/// Places a new signaling block. | |||
/// Any valid functional block type with IO ports will work. | |||
/// This re-implements Block.PlaceNew(...) | |||
/// </summary> | |||
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<BlockPortsStruct>(this.Id)) | |||
@@ -13,30 +13,7 @@ namespace GamecraftModdingAPI.Blocks | |||
{ | |||
public class SpawnPoint : Block | |||
{ | |||
/// <summary> | |||
/// Places a new spawn point. | |||
/// Any valid spawn block type is accepted. | |||
/// This re-implements Block.PlaceNew(...) | |||
/// </summary> | |||
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<SpawnPointStatsEntityStruct>(this.Id)) | |||
{ | |||
@@ -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<TextBlockDataStruct>(this.Id)) | |||
@@ -13,23 +13,6 @@ namespace GamecraftModdingAPI.Blocks | |||
{ | |||
public class Timer : Block | |||
{ | |||
/// <summary> | |||
/// Places a new timer block. | |||
/// </summary> | |||
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<TimerBlockDataStruct>(this.Id)) | |||
@@ -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<ConsoleBlock>(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); | |||