TechbloxModdingAPI/GamecraftModdingAPI/Block.cs
Norbi Peti 680721256c Add support for copying wires, some fixes and additions
Removing blocks from groups when they are removed from the game
Attempted to update graphics when changing blocks
Disallowing changing the block group after creation, now that we can copy blocks
2020-11-13 23:59:37 +01:00

541 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using Gamecraft.Blocks.BlockGroups;
using Svelto.ECS;
using Svelto.ECS.EntityStructs;
using RobocraftX.Common;
using RobocraftX.Blocks;
using Unity.Mathematics;
using Gamecraft.Blocks.GUI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Utility;
namespace GamecraftModdingAPI
{
/// <summary>
/// A single (perhaps scaled) block. Properties may return default values if the block is removed and then setting them is ignored.
/// For specific block type operations, use the specialised block classes in the GamecraftModdingAPI.Blocks namespace.
/// </summary>
public class Block : IEquatable<Block>, IEquatable<EGID>
{
protected static readonly PlacementEngine PlacementEngine = new PlacementEngine();
protected static readonly MovementEngine MovementEngine = new MovementEngine();
protected static readonly RotationEngine RotationEngine = new RotationEngine();
protected static readonly RemovalEngine RemovalEngine = new RemovalEngine();
protected static readonly SignalEngine SignalEngine = new SignalEngine();
protected static readonly BlockEventsEngine BlockEventsEngine = new BlockEventsEngine();
protected static readonly ScalingEngine ScalingEngine = new ScalingEngine();
protected static readonly BlockCloneEngine BlockCloneEngine = new BlockCloneEngine();
protected internal static readonly BlockEngine BlockEngine = new BlockEngine();
/// <summary>
/// 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.
/// </summary>
/// <param name="block">The block's type</param>
/// <param name="color">The block's color</param>
/// <param name="darkness">The block color's darkness (0-9) - 0 is default color</param>
/// <param name="position">The block's position - default block size is 0.2</param>
/// <param name="rotation">The block's rotation in degrees</param>
/// <param name="uscale">The block's uniform scale - default scale is 1 (with 0.2 width)</param>
/// <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 Block PlaceNew(BlockIDs block, float3 position,
float3 rotation = default, BlockColors color = BlockColors.Default, byte darkness = 0,
int uscale = 1, float3 scale = default, Player player = null)
{
return PlaceNew<Block>(block, position, rotation, color, darkness, uscale, scale, player);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="block">The block's type</param>
/// <param name="color">The block's color</param>
/// <param name="darkness">The block color's darkness (0-9) - 0 is default color</param>
/// <param name="position">The block's position - default block size is 0.2</param>
/// <param name="rotation">The block's rotation in degrees</param>
/// <param name="uscale">The block's uniform scale - default scale is 1 (with 0.2 width)</param>
/// <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 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) where T : Block
{
if (PlacementEngine.IsInGame && GameState.IsBuildMode())
{
var egid = PlacementEngine.PlaceBlock(block, color, darkness,
position, uscale, scale, player, rotation, out var initializer);
var bl = New<T>(egid.entityID, egid.groupID);
bl.InitData.Group = BlockEngine.InitGroup(initializer);
Placed += bl.OnPlacedInit;
return bl;
}
return null;
}
/// <summary>
/// Returns the most recently placed block.
/// </summary>
/// <returns>The block object</returns>
public static Block GetLastPlacedBlock()
{
return New<Block>(BlockIdentifiers.LatestBlockID);
}
/// <summary>
/// An event that fires each time a block is placed.
/// </summary>
public static event EventHandler<BlockPlacedRemovedEventArgs> Placed
{
add => BlockEventsEngine.Placed += value;
remove => BlockEventsEngine.Placed -= value;
}
/// <summary>
/// An event that fires each time a block is removed.
/// </summary>
public static event EventHandler<BlockPlacedRemovedEventArgs> Removed
{
add => BlockEventsEngine.Removed += value;
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.CONSOLE_BLOCK_GROUP}},
{typeof(LogicGate), new [] {CommonExclusiveGroups.LOGIC_BLOCK_GROUP}},
{typeof(Motor), new[] {CommonExclusiveGroups.MOTOR_BLOCK_GROUP}},
{typeof(MusicBlock), new[] {CommonExclusiveGroups.MUSIC_BLOCK_GROUP}},
{typeof(ObjectIdentifier), new[]{CommonExclusiveGroups.OBJID_BLOCK_GROUP}},
{typeof(Piston), new[] {CommonExclusiveGroups.PISTON_BLOCK_GROUP}},
{typeof(Servo), new[] {CommonExclusiveGroups.SERVO_BLOCK_GROUP}},
{
typeof(SpawnPoint),
new[]
{
CommonExclusiveGroups.SPAWNPOINT_BLOCK_GROUP,
CommonExclusiveGroups.BUILDINGSPAWN_BLOCK_GROUP
}
},
{
typeof(SfxBlock),
new[]
{
CommonExclusiveGroups.SIMPLESFX_BLOCK_GROUP,
CommonExclusiveGroups.LOOPEDSFX_BLOCK_GROUP
}
},
{typeof(DampedSpring), new [] {CommonExclusiveGroups.DAMPEDSPRING_BLOCK_GROUP}},
{typeof(TextBlock), new[] {CommonExclusiveGroups.TEXT_BLOCK_GROUP}},
{typeof(Timer), new[] {CommonExclusiveGroups.TIMER_BLOCK_GROUP}}
};
/// <summary>
/// Constructs a new instance of T with the given ID and group using dynamically created delegates.
/// It's equivalent to new T(EGID) with a minimal overhead thanks to caching the created delegates.
/// </summary>
/// <param name="id">The block ID</param>
/// <param name="group">The block group</param>
/// <typeparam name="T">The block's type or Block itself</typeparam>
/// <returns>An instance of the provided type</returns>
/// <exception cref="BlockTypeException">The block group doesn't match or cannot be found</exception>
/// <exception cref="MissingMethodException">The block class doesn't have the needed constructor</exception>
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); - doesn't seem like we need these
//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;
var type = GetType();
if (typeToGroup.TryGetValue(type, out var groups))
{
if (groups.All(gr => gr != id.groupID))
throw new BlockTypeException("The block has the wrong group! The type is " + GetType() +
" while the group is " + id.groupID);
}
else if (type != typeof(Block))
Logging.LogWarning($"Unknown block type! Add {type} to the dictionary.");
}
/// <summary>
/// This overload searches for the correct group the block is in.
/// It will throw an exception if the block doesn't exist.
/// Use the EGID constructor where possible or subclasses of Block as those specify the group.
/// </summary>
public Block(uint id)
{
Id = BlockEngine.FindBlockEGID(id) ?? throw new BlockTypeException("Could not find the appropriate group for the block. The block probably doesn't exist or hasn't been submitted.");
}
public EGID Id { get; }
internal BlockEngine.BlockInitData InitData;
private EGID copiedFrom;
/// <summary>
/// The block's current position or zero if the block no longer exists.
/// A block is 0.2 wide by default in terms of position.
/// </summary>
public float3 Position
{
get => MovementEngine.GetPosition(Id, InitData);
set
{
MovementEngine.MoveBlock(Id, InitData, value);
if (blockGroup != null)
blockGroup.PosAndRotCalculated = false;
BlockEngine.UpdateDisplayedBlock(Id);
}
}
/// <summary>
/// The block's current rotation in degrees or zero if the block doesn't exist.
/// </summary>
public float3 Rotation
{
get => RotationEngine.GetRotation(Id, InitData);
set
{
RotationEngine.RotateBlock(Id, InitData, value);
if (blockGroup != null)
blockGroup.PosAndRotCalculated = false;
BlockEngine.UpdateDisplayedBlock(Id);
}
}
/// <summary>
/// The block's non-uniform scale or zero if the block's invalid. Independent of the uniform scaling.
/// The default scale of 1 means 0.2 in terms of position.
/// </summary>
public float3 Scale
{
get => BlockEngine.GetBlockInfo(this, (ScalingEntityStruct st) => st.scale);
set
{
BlockEngine.SetBlockInfo(this, (ref ScalingEntityStruct st, float3 val) => st.scale = val, value);
if (!Exists) return; //UpdateCollision needs the block to exist
ScalingEngine.UpdateCollision(Id);
BlockEngine.UpdateDisplayedBlock(Id);
}
}
/// <summary>
/// The block's uniform scale or zero if the block's invalid. Also sets the non-uniform scale.
/// The default scale of 1 means 0.2 in terms of position.
/// </summary>
public int UniformScale
{
get => BlockEngine.GetBlockInfo(this, (UniformBlockScaleEntityStruct st) => st.scaleFactor);
set
{
BlockEngine.SetBlockInfo(this, (ref UniformBlockScaleEntityStruct st, int val) => st.scaleFactor = val,
value);
Scale = new float3(value, value, value);
}
}
/// <summary>
/// The block's type (ID). Returns BlockIDs.Invalid if the block doesn't exist anymore.
/// </summary>
public BlockIDs Type
{
get
{
return BlockEngine.GetBlockInfo(this, (DBEntityStruct st) => (BlockIDs) st.DBID, BlockIDs.Invalid);
}
}
/// <summary>
/// The block's color. Returns BlockColors.Default if the block no longer exists.
/// </summary>
public BlockColor Color
{
get
{
byte index = BlockEngine.GetBlockInfo(this, (ColourParameterEntityStruct st) => st.indexInPalette,
byte.MaxValue);
return new BlockColor(index);
}
set
{
BlockEngine.SetBlockInfo(this, (ref ColourParameterEntityStruct color, BlockColor val) =>
{
color.indexInPalette = (byte) (val.Color + val.Darkness * 10);
//color.overridePaletteColour = false;
//color.needsUpdate = true;
color.hasNetworkChange = true;
color.paletteColour = BlockEngine.ConvertBlockColor(color.indexInPalette);
}, value);
}
}
/// <summary>
/// The block's exact color. Gets reset to the palette color (Color property) after reentering the game.
/// </summary>
public float4 CustomColor
{
get => BlockEngine.GetBlockInfo(this, (ColourParameterEntityStruct st) => st.paletteColour);
set
{
BlockEngine.SetBlockInfo(this, (ref ColourParameterEntityStruct color, float4 val) =>
{
color.paletteColour = val;
//color.overridePaletteColour = true;
//color.needsUpdate = true;
color.hasNetworkChange = true;
}, value);
}
}
/// <summary>
/// The text displayed on the block if applicable, or null.
/// Setting it is temporary to the session, it won't be saved.
/// </summary>
public string Label
{
get => BlockEngine.GetBlockInfoViewStruct(this, (TextLabelEntityViewStruct st) => st.textLabelComponent?.text);
set
{
BlockEngine.SetBlockInfoViewStruct(this, (ref TextLabelEntityViewStruct text, string val) =>
{
if (text.textLabelComponent != null) text.textLabelComponent.text = val;
}, value);
}
}
private BlockGroup blockGroup;
/// <summary>
/// Returns the block group this block is a part of. Block groups can be placed using blueprints.
/// Returns null if not part of a group.<br />
/// Setting the group after the block has been initialized will not update everything properly,
/// so you can only set this property on blocks newly placed by your code.<br />
/// To set it for existing blocks, you can use the Copy() method and set the property on the resulting block
/// (and remove this block).
/// </summary>
public BlockGroup BlockGroup
{
get
{
if (blockGroup != null) return blockGroup;
return blockGroup = BlockEngine.GetBlockInfo(this,
(BlockGroupEntityComponent bgec) =>
bgec.currentBlockGroup == -1 ? null : new BlockGroup(bgec.currentBlockGroup, this));
}
set
{
if (Exists)
{
/*var copy = Copy<Block>();
copy.BlockGroup = value; //It won't run this on the new instance as it won't 'exist' yet
Remove();*/
Logging.LogWarning("Attempted to set group of existing block. This is not supported."
+ " Copy the block and set the group of the resulting block.");
return;
}
blockGroup?.RemoveInternal(this);
BlockEngine.SetBlockInfo(this,
(ref BlockGroupEntityComponent bgec, BlockGroup val) => bgec.currentBlockGroup = val?.Id ?? -1,
value);
value?.AddInternal(this);
blockGroup = value;
}
}
/// <summary>
/// Whether the block exists. The other properties will return a default value if the block doesn't exist.
/// If the block was just placed, then this will also return false but the properties will work correctly.
/// </summary>
public bool Exists => BlockEngine.BlockExists(Id);
/// <summary>
/// Returns an array of blocks that are connected to this one. Returns an empty array if the block doesn't exist.
/// </summary>
public Block[] GetConnectedCubes() => BlockEngine.GetConnectedBlocks(Id);
/// <summary>
/// Removes this block.
/// </summary>
/// <returns>True if the block exists and could be removed.</returns>
public bool Remove() => RemovalEngine.RemoveBlock(Id);
/// <summary>
/// Returns the rigid body of the chunk of blocks this one belongs to during simulation.
/// Can be used to apply forces or move the block around while the simulation is running.
/// </summary>
/// <returns>The SimBody of the chunk or null if the block doesn't exist or not in simulation mode.</returns>
public SimBody GetSimBody()
{
return BlockEngine.GetBlockInfo(this,
(GridConnectionsEntityStruct st) => st.machineRigidBodyId != uint.MaxValue
? new SimBody(st.machineRigidBodyId, st.clusterId)
: null);
}
/// <summary>
/// Creates a copy of the block in the game with the same properties, stats and wires.
/// </summary>
/// <returns></returns>
public T Copy<T>() where T : Block
{
var block = PlaceNew<T>(Type, Position, Rotation, Color.Color, Color.Darkness, UniformScale, Scale);
block.copiedFrom = Id;
if (Type == BlockIDs.ConsoleBlock
&& (this is ConsoleBlock srcCB || (srcCB = Specialise<ConsoleBlock>()) != null)
&& (block is ConsoleBlock dstCB || (dstCB = block.Specialise<ConsoleBlock>()) != null))
{
//Console block properties are set by a separate engine in the game
dstCB.Arg1 = srcCB.Arg1;
dstCB.Arg2 = srcCB.Arg2;
dstCB.Arg3 = srcCB.Arg3;
dstCB.Command = srcCB.Command;
}
return block;
}
private void OnPlacedInit(object sender, BlockPlacedRemovedEventArgs e)
{ //Member method instead of lambda to avoid constantly creating delegates
if (e.ID != Id) return;
Placed -= OnPlacedInit; //And we can reference it
InitData = default; //Remove initializer as it's no longer valid - if the block gets removed it shouldn't be used again
if (copiedFrom != EGID.Empty)
BlockCloneEngine.CopyBlockStats(copiedFrom, Id);
}
public override string ToString()
{
return $"{nameof(Id)}: {Id}, {nameof(Position)}: {Position}, {nameof(Type)}: {Type}, {nameof(Color)}: {Color}, {nameof(Exists)}: {Exists}";
}
public bool Equals(Block other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Id.Equals(other.Id);
}
public bool Equals(EGID other)
{
return Id.Equals(other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((Block) obj);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
public static void Init()
{
GameEngineManager.AddGameEngine(PlacementEngine);
GameEngineManager.AddGameEngine(MovementEngine);
GameEngineManager.AddGameEngine(RotationEngine);
GameEngineManager.AddGameEngine(RemovalEngine);
GameEngineManager.AddGameEngine(BlockEngine);
GameEngineManager.AddGameEngine(BlockEventsEngine);
GameEngineManager.AddGameEngine(ScalingEngine);
GameEngineManager.AddGameEngine(SignalEngine);
GameEngineManager.AddGameEngine(BlockCloneEngine);
Wire.signalEngine = SignalEngine; // requires same functionality, no need to duplicate the engine
}
/// <summary>
/// Convert the block to a specialised block class.
/// </summary>
/// <returns>The block.</returns>
/// <typeparam name="T">The specialised block type.</typeparam>
public T Specialise<T>() where T : Block
{
// What have I gotten myself into?
// 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
//Lets improve that using delegates
var block = New<T>(Id.entityID, Id.groupID);
if (this.InitData.Group != null)
{
block.InitData = this.InitData;
Placed += block.OnPlacedInit; //Reset InitData of new object
}
return block;
}
#if DEBUG
public static EntitiesDB entitiesDB
{
get
{
return BlockEngine.GetEntitiesDB();
}
}
#endif
}
}