using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using DataLoader; using HarmonyLib; using TechbloxModdingAPI; using TechbloxModdingAPI.Blocks; using TechbloxModdingAPI.Commands; using TechbloxModdingAPI.Utility; using IllusionPlugin; using RobocraftX.Schedulers; using Svelto.Tasks; using Svelto.Tasks.Enumerators; using Svelto.Tasks.Lean; using TechbloxModdingAPI.App; using Unity.Mathematics; using Main = TechbloxModdingAPI.Main; namespace BuildingTools { public class BuildingTools : IEnhancedPlugin { private readonly CommandUtils _commandUtils; private readonly BlockSelections _blockSelections; //private readonly MirrorModeEngine _mirrorModeEngine = new MirrorModeEngine(); public BuildingTools() { _blockSelections = new BlockSelections(); _commandUtils = new CommandUtils(_blockSelections); } public override void OnApplicationStart() { Main.Init(); Game.AddPersistentDebugInfo("PlayerInfo", GetPlayerInfo); Game.AddPersistentDebugInfo("BlockModInfo", GetBlockInfo); _commandUtils.RegisterBlockCommand("scaleBlocks", "Scales the selected blocks, relative to current size (current scale * new scale)." + " The block you're looking at stays where it is, everything else is moved next to it.", (scaleX, scaleY, scaleZ, blocks, refBlock) => //TODO: Either remove refBlock or add commands for changing it { if (!GameState.IsBuildMode()) return; //Scaling & positioning is weird in simulation if (_blockSelections.CheckNoBlocks(blocks)) return; float3? reference = Player.LocalPlayer.GetBlockLookedAt()?.Position; if (!reference.HasValue) { Logging.CommandLogError("Look at a block (not too far away) to be used as reference."); return; } float3 scale = new float3(scaleX, scaleY, scaleZ); foreach (var block in blocks) { block.Scale *= scale; block.Position = (float3) (reference + (block.Position - reference) * scale); } Logging.CommandLog("Blocks scaled and moved."); }); _commandUtils.RegisterBlockCommand("scaleIndividually", "Scales the blocks you're looking at, but doesn't move them." + " The scale is relative, 1 means no change.", (scaleX, scaleY, scaleZ, blocks, refBlock) => { if (!GameState.IsBuildMode()) return; //Scaling & positioning is weird in simulation float3 scale = new float3(scaleX, scaleY, scaleZ); foreach (var block in blocks) block.Scale *= scale; Logging.CommandLog("Blocks scaled individually."); }); _commandUtils.RegisterBlockCommand("moveBlocks", "Moves (teleports) the selected blocks around both in time stopped and running. The latter will be reset as expected.", (x, y, z, blocks, refBlock) => { if (GameState.IsBuildMode()) foreach (var block in blocks) block.Position += new float3(x, y, z); else if (GameState.IsSimulationMode()) foreach (var body in GetSimBodies(blocks)) body.Position += new float3(x, y, z); Logging.CommandLog("Blocks moved."); }); _commandUtils.RegisterBlockCommand("colorBlocks", "Colors the selected blocks permanently both in time stopped and running. It won't be reset when stopping time.", (color, darkness, blocks, refBlock) => { if (!Enum.TryParse(color, true, out BlockColors clr)) { Logging.CommandLogWarning("Color " + color + " not found"); } foreach (var block in blocks) block.Color = new BlockColor(clr, darkness); Logging.CommandLog("Blocks colored."); }); _commandUtils.RegisterBlockCommand("materialBlocks", "Sets the material of the selected blocks permanently both in time stopped and running. It won't be reset when stopping time.", (material, darkness, blocks, refBlock) => { if (!Enum.TryParse(material, true, out BlockMaterial mat)) { Logging.CommandLogWarning("Material " + material + " not found"); } IEnumerator SetMaterial() { foreach (var block in blocks) { block.Material = mat; yield return new WaitForSecondsEnumerator(0.2f).Continue(); } } SetMaterial().RunOn(ClientLean.UIScheduler); Logging.CommandLog("Block materials set."); }); CommandBuilder.Builder("selectBlocksLookedAt", "Selects blocks (1 or more) to change. Only works in time stopped mode." + " Parameter: whether one (true) or all connected (false) blocks should be selected.") .Action(single => { if (!GameState.IsBuildMode()) { Logging.CommandLogError("This command can only be used in time stopped mode."); return; } var refBlock = Player.LocalPlayer.GetBlockLookedAt(); if (refBlock == null) { Logging.CommandLogError("Block not found. Make sure to be close enough to the block."); return; } _blockSelections.refBlock = refBlock; _blockSelections.blocks = single ? new[] {refBlock} : refBlock.GetConnectedCubes() ?? new Block[0]; var blocks = _blockSelections.blocks; Logging.CommandLog(blocks.Length + " blocks selected."); }).Build(); CommandBuilder.Builder("selectBlocksWithID", "Selects blocks with a specific object ID.") .Action(id => { _blockSelections.blocks = (_blockSelections.refBlock = ObjectID.GetByID(id).FirstOrDefault()) ?.GetConnectedCubes() ?? Array.Empty(); Logging.CommandLog(_blockSelections.blocks.Length + " blocks selected."); }).Build(); CommandBuilder.Builder("selectSelectedBlocks", "Selects blocks that are box selected by the player.") .Action(() => { _blockSelections.blocks = Player.LocalPlayer.GetSelectedBlocks(); _blockSelections.refBlock = _blockSelections.blocks.Length > 0 ? _blockSelections.blocks[0] : null; Logging.CommandLog(_blockSelections.blocks.Length + " blocks selected."); }).Build(); CommandBuilder.Builder("selectBlocksInGroup", "Selects the blocks in the block group you are looking at (the blocks currently highlighted when in blueprint mode).") .Action(() => { var block = Player.LocalPlayer.GetBlockLookedAt(); if (block is null) { Logging.CommandLogError("You need to look at a block first (and be close to it)."); return; } var group = block.BlockGroup; _blockSelections.blocks = group is null ? new[] {block} : group.ToArray(); _blockSelections.refBlock = block; Logging.CommandLog(_blockSelections.blocks.Length + " blocks selected."); }).Build(); /*ConsoleCommands.RegisterWithChannel("selectSendSignal", ch => { }, ChannelType.Object, "Sends a signal for selecting a given object ID for a command block.");*/ _commandUtils.RegisterBlockCommand("pushBlocks", "Adds velocity to the selected blocks. Only works in time running mode.", (x, y, z, blocks, refBlock) => { if (!GameState.IsSimulationMode()) { Logging.CommandLogError("This command can only be used in time running mode."); return; } foreach (var block in GetSimBodies(blocks)) block.Velocity += new float3(x, y, z); Logging.CommandLog("Blocks pushed."); }); _commandUtils.RegisterBlockCommand("pushRotateBlocks", "Adds angular velocity to the selected blocks. Only works in simulation.", (x, y, z, blocks, refBlock) => { if (!GameState.IsSimulationMode()) { Logging.CommandLogError("This command can only be used in time running mode."); return; } foreach (var block in GetSimBodies(blocks)) block.AngularVelocity += new float3(x, y, z); Logging.CommandLog("Blocks pushed to rotate."); }); CommandBuilder.Builder("pushPlayer", "Adds velocity to the player.") .Action((x, y, z) => { Player.LocalPlayer.Velocity += new float3(x, y, z); Logging.CommandLog("Player pushed."); }).Build(); CommandBuilder.Builder("pushRotatePlayer", "Adds angular velocity to the player.") .Action((x, y, z) => { Player.LocalPlayer.AngularVelocity += new float3(x, y, z); Logging.CommandLog("Player pushed to rotate."); }).Build(); CommandBuilder.Builder("addBlocksToGroup", "Adds the selected blocks to the same group (they will be highlighted together)." + " This command recreates the blocks that are moved into the group, but block data is almost certainly preserved.") .Action(() => { if (_blockSelections.blocks.Length == 0) { Logging.CommandLogWarning("No blocks selected. Use a select command first."); return; } var group = _blockSelections.refBlock.BlockGroup; uint refID = _blockSelections.refBlock.Id.entityID; if (group is null) { var copy = _blockSelections.refBlock.Copy(); group = BlockGroup.Create(copy); _blockSelections.refBlock.Remove(); _blockSelections.refBlock = copy; } _blockSelections.blocks = _blockSelections.blocks.Where(block => block.Id.entityID != refID) .Select(block => { if (block.BlockGroup == group) return block; var copy = block.Copy(); group.Add(copy); block.Remove(); return copy; }).ToArray(); }).Build(); var setLimits = new SetLimitsCommandEngine(); CommandBuilder.Builder("setBuildLimits", "Set build limits").Action((Action)setLimits.SetLimits).Build(); GameEngineManager.AddGameEngine(setLimits); CommandBuilder.Builder("freeScaling", "This command removes scaling restrictions on the selected block. Reselect block to apply.") .Action(() => { var blockID = Player.LocalPlayer.SelectedBlock; if (blockID == BlockIDs.Invalid) { Logging.CommandLogWarning("You don't have any blocks in your hand."); return; } FullGameFields._dataDb.GetValue((int) blockID).scalingPermission = ScalingPermission.NonUniform; Logging.CommandLog("Free scaling enabled for " + blockID + " until the game is restarted. Reselect block to apply."); }).Build(); CommandBuilder.Builder("setTweakLimit", "Sets the limit on the tweakable stat on selected block. Usage: setTweakLimit [stat] [value]. Sets all stats to 1000 by default.") .Action((string stat, int value) => { var bl = Player.LocalPlayer.SelectedBlock; if (bl == BlockIDs.Invalid) { Logging.CommandLogError("Select the block in the inventory first."); return; } if (!FullGameFields._dataDb.TryGetValue((int)bl, out var data)) { Logging.CommandLogError($"No tweakable stats found on {bl} (selected)"); return; } TweakPropertyInfo[] stats; if (stat is null || stat.Length == 0) { stats = data.Stats; } else { if (!data.statsByName.TryGetValue(stat, out var statInfo)) { Logging.CommandLogError($"Tweakable stat {stat} not found. Stats: {data.statsByName.Keys.Aggregate((a, b) => a + ", " + b)}"); return; } stats = new[] { statInfo }; } foreach (var statInfo in stats) { statInfo.max = value == 0 ? 1000 : value; statInfo.min = -1000; } Logging.CommandLog($"{(stat is null || stat.Length == 0 ? "All stats" : $"Stat {stat}")} max changed to {(value == 0 ? 1000 : value)} on {bl}"); }).Build(); //_mirrorModeEngine.Init(); UI.Init(); new Harmony("BuildTools").PatchAll(Assembly.GetExecutingAssembly()); } private string GetBlockInfo() { if (GameState.IsBuildMode()) return GetBlockInfoInBuildMode(); if (GameState.IsSimulationMode()) return GetBodyInfoInSimMode(); return "Switching modes..."; } private static string GetBlockInfoInBuildMode() { var block = Player.LocalPlayer.GetBlockLookedAt(); if (block == null) return GetWireInfoInBuildMode(); float3 pos = block.Position; float3 rot = block.Rotation; float3 scale = block.Scale; return $"Block: {block.Type} at {pos.x:F} {pos.y:F} {pos.z:F}\n" + $"- Rotation: {rot.x:F}° {rot.y:F}° {rot.z:F}°\n" + $"- Color: {block.Color.Color} darkness: {block.Color.Darkness}\n" + $"- Material: {block.Material}\n" + $"- Scale: {scale.x:F} {scale.y:F} {scale.z:F}\n" + $"- Label: {block.Label}\n" + $"- ID: {block.Id}\n" + (block.BlockGroup != null ? $"- Group: {block.BlockGroup.Id}\n" : "") + $"- Mass: {block.Mass}"; } private static string GetWireInfoInBuildMode() { var wire = Player.LocalPlayer.GetWireLookedAt(); if (wire == null) return ""; var startPos = wire.Start.Position; var endPos = wire.End.Position; return $"Wire with {wire.Id}\n" + $"- From block {wire.Start.Type} at {startPos.x:F} {startPos.y:F} {startPos.z:F}\n" + $"- at port {wire.StartPortName}\n" + $"- To block {wire.End.Type} at {endPos.x:F} {endPos.y:F} {endPos.z:F}\n" + $"- at port {wire.EndPortName}"; } private static string GetBodyInfoInSimMode() { var body = Player.LocalPlayer.GetSimBodyLookedAt(); if (body == null) return GetBlockInfoInBuildMode(); float3 pos = body.Position; float3 rot = body.Rotation; float3 vel = body.Velocity; float3 ave = body.AngularVelocity; float3 com = body.CenterOfMass; Cluster cluster = body.Cluster; return $"Body at {pos.x:F} {pos.y:F} {pos.z:F}\n" + $"- Rotation: {rot.x:F}° {rot.y:F}° {rot.z:F}°\n" + $"- Velocity: {vel.x:F} {vel.y:F} {vel.z:F}\n" + $"- Angular velocity: {ave.x:F} {ave.y:F} {ave.z:F}\n" + $"- {(body.Static ? "Static body" : $"Center of mass: {com.x:F} {com.y:F} {com.z:F}")}\n" + $"- Volume: {body.Volume:F}\n" + $"- Chunk health: {body.CurrentHealth:F} / {body.InitialHealth:F} - Multiplier: {body.HealthMultiplier:F}\n" + (cluster == null ? "" : $"- Cluster health: {cluster.CurrentHealth:F} / {cluster.InitialHealth:F} - Multiplier: {cluster.HealthMultiplier:F}\n" + $"- Cluster mass: {cluster.Mass}" ); } private string GetPlayerInfo() { var player = Player.LocalPlayer; if (player == null) return ""; float3 pos = player.Position; float3 rot = player.Rotation; float3 vel = player.Velocity; float3 ave = player.AngularVelocity; return $"Player position: {pos.x:F} {pos.y:F} {pos.z:F}\n" + $"- Rotation: {rot.x:F}° {rot.y:F}° {rot.z:F}°\n" + $"- Velocity: {vel.x:F} {vel.y:F} {vel.z:F}\n" + $"- Angular velocity: {ave.x:F} {ave.y:F} {ave.z:F}\n" + $"- Health: {player.CurrentHealth:F} / {player.InitialHealth:F}"; } private IEnumerable GetSimBodies(Block[] blocks) => blocks.Select(block => block.GetSimBody()).Where(block => !(block is null)).Distinct(); public override void OnApplicationQuit() => Main.Shutdown(); public override string Name => "BuildingTools"; public override string Version => "v1.1.0"; } }