Automatically invoke the correct block constructor
And store delegates of dynamic methods invoking constructors Tested with the automated tests
This commit is contained in:
parent
aa0aefd41b
commit
5bbb54c0c5
11 changed files with 114 additions and 195 deletions
|
@ -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;
|
||||
if (!BlockEngine.GetBlockInfoExists<ConsoleBlockEntityStruct>(this.Id))
|
||||
{
|
||||
throw new BlockTypeException($"Block is not a {this.GetType().Name} block");
|
||||
}
|
||||
}
|
||||
|
||||
public ConsoleBlock(EGID id): base(id)
|
||||
public ConsoleBlock(uint id): base(new EGID(id, CommonExclusiveGroups.BUILD_CONSOLE_BLOCK_GROUP))
|
||||
{
|
||||
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))
|
||||
{
|
||||
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);
|
||||
|
|
Loading…
Reference in a new issue