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 HarmonyLib; using RobocraftX.PilotSeat; using RobocraftX.Rendering; using Techblox.BlockLabelsServer; using TechbloxModdingAPI.Blocks; using TechbloxModdingAPI.Blocks.Engines; using TechbloxModdingAPI.Tests; using TechbloxModdingAPI.Utility; namespace TechbloxModdingAPI { /// /// 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. /// public class Block : EcsObjectBase, IEquatable, IEquatable { 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(); /// /// 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. /// /// The block's type /// The block's position - default block size is 0.2 /// Whether the block should be auto-wired (if functional) /// The player who placed the block /// The placed block or null if failed 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; } /// /// Returns the most recently placed block. /// /// The block object or null if doesn't exist public static Block GetLastPlacedBlock() { uint lastBlockID = CommonExclusiveGroups.blockIDGeneratorClient.Peek() - 1; EGID? egid = BlockEngine.FindBlockEGID(lastBlockID); return egid.HasValue ? New(egid.Value) : null; } /*public static Block CreateGhostBlock() { return BlockGroup._engine.BuildGhostChild(); }*/ /// /// An event that fires each time a block is placed. /// public static event EventHandler Placed { //TODO: Rename and add instance version in 3.0 add => BlockEventsEngine.Placed += value; remove => BlockEventsEngine.Placed -= value; } /// /// An event that fires each time a block is removed. /// public static event EventHandler Removed { add => BlockEventsEngine.Removed += value; remove => BlockEventsEngine.Removed -= value; } private static readonly Dictionary Constructor, Type Type)> GroupToConstructor = new Dictionary, 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))); } /// /// 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. /// /// The EGID of the block /// Whether the block is definitely a signaling block /// 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()}"); } /// /// 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. /// 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.")) { } /// /// Places a new block in the world. /// /// The block's type /// The block's position (a block is 0.2 wide in terms of position) /// Whether the block should be auto-wired (if functional) /// The player who placed the block 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; /// /// 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. /// public float3 Position { get => MovementEngine.GetPosition(this); set { MovementEngine.MoveBlock(this, value); if (blockGroup != null) blockGroup.PosAndRotCalculated = false; BlockEngine.UpdateDisplayedBlock(Id); } } /// /// The block's current rotation in degrees or zero if the block doesn't exist. /// public float3 Rotation { get => RotationEngine.GetRotation(this); set { RotationEngine.RotateBlock(this, value); if (blockGroup != null) blockGroup.PosAndRotCalculated = false; BlockEngine.UpdateDisplayedBlock(Id); } } /// /// 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. /// public float3 Scale { get => BlockEngine.GetBlockInfo(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(this).scale = value; //BlockEngine.GetBlockInfo(this).gridScale = value - (int3) value + 1; if (!Exists) return; //UpdateCollision needs the block to exist ScalingEngine.UpdateCollision(Id); BlockEngine.UpdateDisplayedBlock(Id); } } /// /// 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. /// public int UniformScale { get => BlockEngine.GetBlockInfo(this).scaleFactor; set { if (value < 1) value = 1; BlockEngine.GetBlockInfo(this).scaleFactor = value; Scale = new float3(value, value, value); } } /** * Whether the block is flipped. */ public bool Flipped { get => BlockEngine.GetBlockInfo(this).scale.x < 0; set { ref var st = ref BlockEngine.GetBlockInfo(this); st.scale.x = math.abs(st.scale.x) * (value ? -1 : 1); BlockEngine.UpdatePrefab(this, (byte) Material, value); } } /// /// The block's type (ID). Returns BlockIDs.Invalid if the block doesn't exist anymore. /// public BlockIDs Type { get { var opt = BlockEngine.GetBlockInfoOptional(this); return opt ? (BlockIDs) opt.Get().DBID : BlockIDs.Invalid; } } /// /// The block's color. Returns BlockColors.Default if the block no longer exists. /// public BlockColor Color { get { var opt = BlockEngine.GetBlockInfoOptional(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(this); color.indexInPalette = value.Index; color.hasNetworkChange = true; color.paletteColour = BlockEngine.ConvertBlockColor(color.indexInPalette); //Setting to 255 results in black BlockEngine.UpdateBlockColor(Id); } } /// /// The block's exact color. Gets reset to the palette color (Color property) after reentering the game. /// public float4 CustomColor { get => BlockEngine.GetBlockInfo(this).paletteColour; set { ref var color = ref BlockEngine.GetBlockInfo(this); color.paletteColour = value; color.hasNetworkChange = true; } } /** * The block's material. */ public BlockMaterial Material { get { var opt = BlockEngine.GetBlockInfoOptional(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(val)) throw new BlockException($"Block material {value} does not exist!"); ref var comp = ref BlockEngine.GetBlockInfo(this); if (comp.materialId == val) return; comp.materialId = val; BlockEngine.UpdatePrefab(this, val, Flipped); //The default causes the screen to go black } } /// /// The text displayed on the block if applicable, or null. /// Setting it is temporary to the session, it won't be saved. /// [TestValue(null)] public string Label { get { var opt = BlockEngine.GetBlockInfoOptional(this); return opt ? FullGameFields._managers.blockLabelResourceManager.GetText(opt.Get().instanceID) : null; } set { var opt = BlockEngine.GetBlockInfoOptional(this); if (opt) FullGameFields._managers.blockLabelResourceManager.SetText(opt.Get().instanceID, value); } } private BlockGroup blockGroup; /// /// 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.
/// 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.
/// To set it for existing blocks, you can use the Copy() method and set the property on the resulting block /// (and remove this block). ///
public BlockGroup BlockGroup { get { if (blockGroup != null) return blockGroup; if (!GameState.IsBuildMode()) return null; // Breaks in simulation var bgec = BlockEngine.GetBlockInfo(this); return blockGroup = bgec.currentBlockGroup == -1 ? null : GetInstance(new EGID((uint)bgec.currentBlockGroup, BlockGroupExclusiveGroups.BlockGroupEntityGroup), egid => new BlockGroup((int)egid.entityID, 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(this).currentBlockGroup = (int?) value?.Id.entityID ?? -1; value?.AddInternal(this); blockGroup = value; } } /// /// 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. /// public bool Static { get => BlockEngine.GetBlockInfo(this).isStatic; set => BlockEngine.GetBlockInfo(this).isStatic = value; } /// /// The mass of the block. /// public float Mass { get => BlockEngine.GetBlockInfo(this).mass; } /// /// Block complexity used for build rules. Determines the 'cost' of the block. /// public BlockComplexity Complexity { get => new(BlockEngine.GetBlockInfo(this)); set => BlockEngine.GetBlockInfo(this) = value; } /// /// 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. /// public bool Exists => BlockEngine.BlockExists(Id); /// /// Returns an array of blocks that are connected to this one. Returns an empty array if the block doesn't exist. /// public Block[] GetConnectedCubes() => BlockEngine.GetConnectedBlocks(Id); /// /// Removes this block. /// /// True if the block exists and could be removed. public bool Remove() => RemovalEngine.RemoveBlock(Id); /// /// 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. /// /// The SimBody of the chunk or null if the block doesn't exist or not in simulation mode. public SimBody GetSimBody() { var st = BlockEngine.GetBlockInfo(this); /*return st.machineRigidBodyId != uint.MaxValue ? new SimBody(st.machineRigidBodyId, st.clusterId) - TODO: : null;*/ return null; } /// /// Creates a copy of the block in the game with the same properties, stats and wires. /// /// 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 } } }