using System; using System.Collections.Generic; using DataLoader; 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 HarmonyLib; using RobocraftX.PilotSeat; using TechbloxModdingAPI.Blocks; using TechbloxModdingAPI.Blocks.Engines; using TechbloxModdingAPI.Tests; using TechbloxModdingAPI.Utility; namespace TechbloxModdingAPI { /// <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 TechbloxModdingAPI.Blocks namespace. /// </summary> public class Block : EcsObjectBase, 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="position">The block's position - default block size is 0.2</param> /// <param name="autoWire">Whether the block should be auto-wired (if functional)</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, bool autoWire = false, Player player = null) { if (PlacementEngine.IsInGame && GameState.IsBuildMode()) { var initializer = PlacementEngine.PlaceBlock(block, position, player, autoWire); var egid = initializer.EGID; var bl = New(egid); bl.InitData = initializer; Placed += bl.OnPlacedInit; return bl; } return null; } /// <summary> /// Returns the most recently placed block. /// </summary> /// <returns>The block object or null if doesn't exist</returns> public static Block GetLastPlacedBlock() { uint lastBlockID = (uint) AccessTools.Field(typeof(CommonExclusiveGroups), "_nextBlockEntityID").GetValue(null) - 1; EGID? egid = BlockEngine.FindBlockEGID(lastBlockID); return egid.HasValue ? New(egid.Value) : null; } /*public static Block CreateGhostBlock() { return BlockGroup._engine.BuildGhostChild(); }*/ /// <summary> /// An event that fires each time a block is placed. /// </summary> public static event EventHandler<BlockPlacedRemovedEventArgs> Placed { //TODO: Rename and add instance version in 3.0 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 readonly Dictionary<ExclusiveBuildGroup, (Func<EGID, Block> Constructor, Type Type)> GroupToConstructor = new Dictionary<ExclusiveBuildGroup, (Func<EGID, Block>, Type)> { {CommonExclusiveGroups.DAMPEDSPRING_BLOCK_GROUP, (id => new DampedSpring(id), typeof(DampedSpring))}, {CommonExclusiveGroups.ENGINE_BLOCK_BUILD_GROUP, (id => new Engine(id), typeof(Engine))}, {CommonExclusiveGroups.LOGIC_BLOCK_GROUP, (id => new LogicGate(id), typeof(LogicGate))}, {CommonExclusiveGroups.PISTON_BLOCK_GROUP, (id => new Piston(id), typeof(Piston))}, {CommonExclusiveGroups.SERVO_BLOCK_GROUP, (id => new Servo(id), typeof(Servo))}, {CommonExclusiveGroups.WHEELRIG_BLOCK_BUILD_GROUP, (id => new WheelRig(id), typeof(WheelRig))} }; static Block() { foreach (var group in SeatGroups.SEATS_BLOCK_GROUPS) // Adds driver and passenger seats, occupied and unoccupied GroupToConstructor.Add(group, (id => new Seat(id), typeof(Seat))); } /// <summary> /// Returns a correctly typed instance of this block. The instances are shared for a specific block. /// If an instance is no longer referenced a new instance is returned. /// </summary> /// <param name="egid">The EGID of the block</param> /// <param name="signaling">Whether the block is definitely a signaling block</param> /// <returns></returns> internal static Block New(EGID egid, bool signaling = false) { if (egid == default) return null; if (GroupToConstructor.ContainsKey(egid.groupID)) { var (constructor, type) = GroupToConstructor[egid.groupID]; return GetInstance(egid, constructor, type); } return signaling ? GetInstance(egid, e => new SignalingBlock(e)) : GetInstance(egid, e => new Block(e)); } public Block(EGID id) : base(id) { Type expectedType; if (GroupToConstructor.ContainsKey(id.groupID) && !GetType().IsAssignableFrom(expectedType = GroupToConstructor[id.groupID].Type)) throw new BlockSpecializationException($"Incorrect block type! Expected: {expectedType} Actual: {GetType()}"); } /// <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) : this(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.")) { } /// <summary> /// Places a new block in the world. /// </summary> /// <param name="type">The block's type</param> /// <param name="position">The block's position (a block is 0.2 wide in terms of position)</param> /// <param name="autoWire">Whether the block should be auto-wired (if functional)</param> /// <param name="player">The player who placed the block</param> public Block(BlockIDs type, float3 position, bool autoWire = false, Player player = null) : base(block => { if (!PlacementEngine.IsInGame || !GameState.IsBuildMode()) throw new BlockException("Blocks can only be placed in build mode."); var initializer = PlacementEngine.PlaceBlock(type, position, player, autoWire); block.InitData = initializer; Placed += ((Block)block).OnPlacedInit; return initializer.EGID; }) { } 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(this); set { MovementEngine.MoveBlock(this, 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(this); set { RotationEngine.RotateBlock(this, 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<ScalingEntityStruct>(this).scale; set { int uscale = UniformScale; if (value.x < 4e-5) value.x = uscale; if (value.y < 4e-5) value.y = uscale; if (value.z < 4e-5) value.z = uscale; BlockEngine.GetBlockInfo<ScalingEntityStruct>(this).scale = value; //BlockEngine.GetBlockInfo<GridScaleStruct>(this).gridScale = value - (int3) value + 1; 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<UniformBlockScaleEntityStruct>(this).scaleFactor; set { if (value < 1) value = 1; BlockEngine.GetBlockInfo<UniformBlockScaleEntityStruct>(this).scaleFactor = value; Scale = new float3(value, value, value); } } /** * Whether the block is flipped. */ public bool Flipped { get => BlockEngine.GetBlockInfo<ScalingEntityStruct>(this).scale.x < 0; set { ref var st = ref BlockEngine.GetBlockInfo<ScalingEntityStruct>(this); st.scale.x = math.abs(st.scale.x) * (value ? -1 : 1); BlockEngine.UpdatePrefab(this, (byte) Material, value); } } /// <summary> /// The block's type (ID). Returns BlockIDs.Invalid if the block doesn't exist anymore. /// </summary> public BlockIDs Type { get { var opt = BlockEngine.GetBlockInfoOptional<DBEntityStruct>(this); return opt ? (BlockIDs) opt.Get().DBID : BlockIDs.Invalid; } } /// <summary> /// The block's color. Returns BlockColors.Default if the block no longer exists. /// </summary> public BlockColor Color { get { var opt = BlockEngine.GetBlockInfoOptional<ColourParameterEntityStruct>(this); return new BlockColor(opt ? opt.Get().indexInPalette : byte.MaxValue); } set { if (value.Color == BlockColors.Default) value = new BlockColor(FullGameFields._dataDb.TryGetValue((int) Type, out CubeListData cld) ? cld.DefaultColour : throw new BlockTypeException("Unknown block type! Could not set default color.")); ref var color = ref BlockEngine.GetBlockInfo<ColourParameterEntityStruct>(this); color.indexInPalette = value.Index; color.hasNetworkChange = true; color.paletteColour = BlockEngine.ConvertBlockColor(color.indexInPalette); //Setting to 255 results in black } } /// <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<ColourParameterEntityStruct>(this).paletteColour; set { ref var color = ref BlockEngine.GetBlockInfo<ColourParameterEntityStruct>(this); color.paletteColour = value; color.hasNetworkChange = true; } } /** * The block's material. */ public BlockMaterial Material { get { var opt = BlockEngine.GetBlockInfoOptional<CubeMaterialStruct>(this); return opt ? (BlockMaterial) opt.Get().materialId : BlockMaterial.Default; } set { byte val = (byte) value; if (value == BlockMaterial.Default) val = FullGameFields._dataDb.TryGetValue((int) Type, out CubeListData cld) ? cld.DefaultMaterialID : throw new BlockTypeException("Unknown block type! Could not set default material."); if (!FullGameFields._dataDb.ContainsKey<MaterialPropertiesData>(val)) throw new BlockException($"Block material {value} does not exist!"); ref var comp = ref BlockEngine.GetBlockInfo<CubeMaterialStruct>(this); if (comp.materialId == val) return; comp.materialId = val; BlockEngine.UpdatePrefab(this, val, Flipped); //The default causes the screen to go black } } /// <summary> /// The text displayed on the block if applicable, or null. /// Setting it is temporary to the session, it won't be saved. /// </summary> [TestValue(null)] public string Label { get => BlockEngine.GetBlockInfoViewComponent<TextLabelEntityViewStruct>(this).textLabelComponent?.text; set { var comp = BlockEngine.GetBlockInfoViewComponent<TextLabelEntityViewStruct>(this).textLabelComponent; if (comp != null) comp.text = value; } } private BlockGroup blockGroup; /// <summary> /// Returns the block group this block is a part of. Block groups can also be placed using blueprints. /// Returns null if not part of a group, although all blocks should have their own by default.<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; var bgec = BlockEngine.GetBlockInfo<BlockGroupEntityComponent>(this); return blockGroup = bgec.currentBlockGroup == -1 ? null : new BlockGroup(bgec.currentBlockGroup, this); } set { if (Exists) { 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); if (!InitData.Valid) return; BlockEngine.GetBlockInfo<BlockGroupEntityComponent>(this).currentBlockGroup = (int?) value?.Id.entityID ?? -1; value?.AddInternal(this); blockGroup = value; } } /// <summary> /// Whether the block should be static in simulation. If set, it cannot be moved. The effect is temporary, it will not be saved with the block. /// </summary> public bool Static { get => BlockEngine.GetBlockInfo<BlockStaticComponent>(this).isStatic; set => BlockEngine.GetBlockInfo<BlockStaticComponent>(this).isStatic = 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() { var st = BlockEngine.GetBlockInfo<GridConnectionsEntityStruct>(this); return 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 Block Copy() { var block = PlaceNew(Type, Position); block.Rotation = Rotation; block.Color = Color; block.Material = Material; block.UniformScale = UniformScale; block.Scale = Scale; block.copiedFrom = Id; 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 != default) 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 } } }