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 RobocraftX.PilotSeat; using RobocraftX.Rendering; using Techblox.BlockLabelsServer; using TechbloxModdingAPI.Blocks; using TechbloxModdingAPI.Blocks.Engines; using TechbloxModdingAPI.Client.App; using TechbloxModdingAPI.Common; using TechbloxModdingAPI.Common.Engines; using TechbloxModdingAPI.Common.Traits; using TechbloxModdingAPI.Tests; using TechbloxModdingAPI.Utility; using Unity.Transforms; using UnityEngine; 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, IHasPhysics, IEquatable, IEquatable { protected static readonly PlacementEngine PlacementEngine = new(); protected static readonly RemovalEngine RemovalEngine = new(); protected static readonly SignalEngine SignalEngine = new(); protected static readonly BlockEventsEngine BlockEventsEngine = new(); protected static readonly ScalingEngine ScalingEngine = new(); protected static readonly BlockCloneEngine BlockCloneEngine = new(); protected internal static readonly BlockEngine BlockEngine = new(); /// /// 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 && GameClient.IsBuildMode) { var initializer = PlacementEngine.PlaceBlock(block, position, player, autoWire); var bl = New(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() { {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) { return New(egid, default, signaling); } private static Block New(EcsInitData initData, bool signaling = false) { return New(initData.EGID, initData, signaling); } private static Block New(EGID egid, EcsInitData initData, bool signaling) { if (egid == default) return null; Func constructor; Type type = null; if (GroupToConstructor.TryGetValue(egid.groupID, out var value)) (constructor, type) = value; else constructor = signaling ? e => new SignalingBlock(e) : e => new Block(e); return initData != default ? GetInstanceNew(initData, constructor, type) : GetInstanceExisting(egid, constructor, type); } 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.")) { } 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 => GetComponent().position; set { // main (persistent) position GetComponent().position = value; // placement grid position GetComponent().position = value; // rendered position GetComponent().position = value; this.UpdatePhysicsUECSComponent(new Translation { Value = value }); GetComponent().areConnectionsAssigned = false; if (blockGroup != null) blockGroup.PosAndRotCalculated = false; BlockEngine.UpdateDisplayData(Id); } } /// /// The block's current rotation in degrees or zero if the block doesn't exist. /// public float3 Rotation { get => ((Quaternion)GetComponent().rotation).eulerAngles; set { // main (persistent) rotation Quaternion newRotation = GetComponent().rotation; newRotation.eulerAngles = value; GetComponent().rotation = newRotation; // placement grid rotation GetComponent().rotation = newRotation; // rendered rotation GetComponent().rotation = newRotation; this.UpdatePhysicsUECSComponent(new Rotation { Value = newRotation }); // They are assigned during machine processing anyway GetComponent().areConnectionsAssigned = false; if (blockGroup != null) blockGroup.PosAndRotCalculated = false; BlockEngine.UpdateDisplayData(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 => GetComponent().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; GetComponent().scale = value; GetComponent().areConnectionsAssigned = false; //BlockEngine.GetBlockInfo(this).gridScale = value - (int3) value + 1; if (!Exists) return; //UpdateCollision needs the block to exist ScalingEngine.UpdateCollision(Id); BlockEngine.UpdateDisplayData(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 => GetComponent().scaleFactor; set { //It appears that only the non-uniform scale has any visible effect so we'll set that as well if (value < 1) value = 1; GetComponent().scaleFactor = value; Scale = new float3(value, value, value); } } /** * Whether the block is flipped. */ public bool Flipped { get => GetComponent().scale.x < 0; set { ref var st = ref GetComponent(); st.scale.x = math.abs(st.scale.x) * (value ? -1 : 1); BlockEngine.UpdateDisplayedPrefab(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 = GetComponentOptional(); 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 = GetComponentOptional(); return new BlockColor(opt ? opt.Get().indexInPalette : byte.MaxValue); } set { // TODO: Expose CubeListData in the API 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 GetComponent(); 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 => GetComponent().paletteColour; set { ref var color = ref GetComponent(); color.paletteColour = value; color.hasNetworkChange = true; } } /** * The block's material. */ public BlockMaterial Material { get { var opt = GetComponentOptional(); 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 GetComponent(); if (comp.materialId == val) return; comp.materialId = val; BlockEngine.UpdateDisplayedPrefab(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 = GetComponentOptional(); return opt ? FullGameFields._managers.blockLabelResourceManager.GetText(opt.Get().instanceID) : null; } set { var opt = GetComponentOptional(); 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 (!GameClient.IsBuildMode) return null; // Breaks in simulation var bgec = GetComponent(); return blockGroup = bgec.currentBlockGroup == -1 ? null : GetInstanceExisting(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; GetComponent().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 => GetComponent().isStatic; set => GetComponent().isStatic = value; } /// /// The mass of the block. /// public float Mass { get => GetComponent().mass; } /// /// Block complexity used for build rules. Determines the 'cost' of the block. /// public BlockComplexity Complexity { get => new(GetComponent()); set => GetComponent() = value; } /// /// 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 = GetComponent(); /*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 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() { EngineManager.AddEngine(PlacementEngine, ApiEngineType.Build); EngineManager.AddEngine(RemovalEngine, ApiEngineType.Build); EngineManager.AddEngine(BlockEngine, ApiEngineType.Build, ApiEngineType.PlayServer, ApiEngineType.PlayClient); EngineManager.AddEngine(BlockEventsEngine, ApiEngineType.Build); EngineManager.AddEngine(ScalingEngine, ApiEngineType.Build); EngineManager.AddEngine(SignalEngine, ApiEngineType.Build); EngineManager.AddEngine(BlockCloneEngine, ApiEngineType.Build); Wire.signalEngine = SignalEngine; // requires same functionality, no need to duplicate the engine } } }