diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod.zip b/ShipyardsRevival/Data/Scripts/ShipyardMod.zip deleted file mode 100644 index fbad0bd..0000000 Binary files a/ShipyardsRevival/Data/Scripts/ShipyardMod.zip and /dev/null differ diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/BlockTarget.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/BlockTarget.cs new file mode 100644 index 0000000..7f9bc1d --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/BlockTarget.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Sandbox.Definitions; +using Sandbox.Game.Entities; +using Sandbox.ModAPI; +using ShipyardMod.Utility; +using VRage.Game.ModAPI; +using VRageMath; + +namespace ShipyardMod.ItemClasses +{ + public class BlockTarget + { + public BlockTarget(IMySlimBlock block, ShipyardItem item) + { + Logging.Instance.WriteDebug($"[BlockTarget] Creating new target for block at position {block.Position}"); + + Block = block; + + // Log physics and projector state + if (CubeGrid.Physics == null) + Logging.Instance.WriteDebug("[BlockTarget] Grid has no physics"); + if (Projector != null) + Logging.Instance.WriteDebug("[BlockTarget] Block is from projector"); + + // Calculate and log distances + if (CubeGrid.Physics == null && Projector != null) + { + CenterDist = Vector3D.DistanceSquared(block.GetPosition(), Projector.GetPosition()); + Logging.Instance.WriteDebug($"[BlockTarget] Distance to projector: {Math.Sqrt(CenterDist):F2}m"); + } + else + { + CenterDist = Vector3D.DistanceSquared(block.GetPosition(), block.CubeGrid.Center()); + Logging.Instance.WriteDebug($"[BlockTarget] Distance to grid center: {Math.Sqrt(CenterDist):F2}m"); + } + + // Calculate and log tool distances + ToolDist = new Dictionary(); + foreach (IMyCubeBlock tool in item.Tools) + { + double dist = Vector3D.DistanceSquared(block.GetPosition(), tool.GetPosition()); + ToolDist.Add(tool.EntityId, dist); + Logging.Instance.WriteDebug($"[BlockTarget] Distance to tool {tool.EntityId}: {Math.Sqrt(dist):F2}m"); + } + + // Calculate build time + var blockDef = (MyCubeBlockDefinition)block.BlockDefinition; + BuildTime = blockDef.MaxIntegrity / blockDef.IntegrityPointsPerSec; + Logging.Instance.WriteDebug($"[BlockTarget] Estimated build time: {BuildTime:F2}s"); + + // Log initial block state + var missingComponents = new Dictionary(); + block.GetMissingComponents(missingComponents); + if (missingComponents.Any()) + { + Logging.Instance.WriteDebug("[BlockTarget] Initial missing components:"); + foreach (var component in missingComponents) + Logging.Instance.WriteDebug($" {component.Key}: {component.Value}"); + } + + Logging.Instance.WriteDebug($"[BlockTarget] Initial integrity: {block.BuildIntegrity:F2}/{block.MaxIntegrity:F2}"); + } + + public IMyCubeGrid CubeGrid + { + get { return Block.CubeGrid; } + } + + public IMyProjector Projector + { + get { return ((MyCubeGrid)Block.CubeGrid).Projector; } + } + + public Vector3I GridPosition + { + get { return Block.Position; } + } + + public bool CanBuild + { + get + { + if (CubeGrid.Physics != null) + { + Logging.Instance.WriteDebug($"[BlockTarget] Block at {GridPosition} can build: has physics"); + return true; + } + + var result = Projector?.CanBuild(Block, false) == BuildCheckResult.OK; + if (!result) + { + var buildResult = Projector?.CanBuild(Block, false); + Logging.Instance.WriteDebug($"[BlockTarget] Block at {GridPosition} cannot build: {buildResult}"); + } + return result; + } + } + + public IMySlimBlock Block { get; private set; } + public float BuildTime { get; } + public double CenterDist { get; } + public Dictionary ToolDist { get; } + + public void UpdateAfterBuild() + { + Logging.Instance.WriteDebug($"[BlockTarget] Updating block at {GridPosition} after build"); + + Vector3D pos = Block.GetPosition(); + IMyCubeGrid grid = Projector.CubeGrid; + Vector3I gridPos = grid.WorldToGridInteger(pos); + IMySlimBlock newBlock = grid.GetCubeBlock(gridPos); + + if (newBlock != null) + { + Block = newBlock; + Logging.Instance.WriteDebug($"[BlockTarget] Updated to new block. Integrity: {Block.BuildIntegrity:F2}/{Block.MaxIntegrity:F2}"); + + // Log any remaining missing components + var missingComponents = new Dictionary(); + Block.GetMissingComponents(missingComponents); + if (missingComponents.Any()) + { + Logging.Instance.WriteDebug("[BlockTarget] Post-build missing components:"); + foreach (var component in missingComponents) + Logging.Instance.WriteDebug($" {component.Key}: {component.Value}"); + } + } + else + { + Logging.Instance.WriteDebug("[BlockTarget] Failed to find new block after build"); + } + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/BoxItem.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/BoxItem.cs new file mode 100644 index 0000000..5c13c41 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/BoxItem.cs @@ -0,0 +1,12 @@ +using VRageMath; + +namespace ShipyardMod.ItemClasses +{ + public class BoxItem + { + public long GridId; + public Vector3D LastPos; + public LineItem[] Lines; + public uint PackedColor; + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/Communication.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/Communication.cs new file mode 100644 index 0000000..e065bad --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/Communication.cs @@ -0,0 +1,821 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Sandbox.Definitions; +using Sandbox.ModAPI; +using ShipyardMod.ItemClasses; +using ShipyardMod.ProcessHandlers; +using ShipyardMod.Settings; +using SpaceEngineers.Game.ModAPI; +using VRage; +using VRage.Game.ModAPI; +using VRage.ModAPI; +using VRageMath; + +namespace ShipyardMod.Utility +{ + public class Communication + { + private const ushort MESSAGE_ID = 53286; + public static Dictionary> LineDict = new Dictionary>(); + public static HashSet FadeList = new HashSet(); + public static List ScanList = new List(); + public static string FullName = typeof(Communication).FullName; + + private static void Recieve(byte[] data) + { + try + { + var recieveBlock = Profiler.Start(FullName, nameof(Recieve)); + var messageId = (MessageTypeEnum)data[0]; + var newData = new byte[data.Length - 1]; + Array.Copy(data, 1, newData, 0, newData.Length); + //Logging.Instance.WriteDebug("Recieve: " + messageId); + + switch (messageId) + { + case MessageTypeEnum.ToolLine: + HandleToolLine(newData); + break; + + case MessageTypeEnum.NewYard: + HandleNewShipyard(newData); + break; + + case MessageTypeEnum.YardState: + HandleShipyardState(newData); + break; + + case MessageTypeEnum.ClientChat: + HandleClientChat(newData); + break; + + case MessageTypeEnum.ServerChat: + HandleServerChat(newData); + break; + + case MessageTypeEnum.ServerDialog: + HandleServerDialog(newData); + break; + + case MessageTypeEnum.ClearLine: + HandleClearLine(newData); + break; + + case MessageTypeEnum.ShipyardSettings: + HandleShipyardSettings(newData); + break; + + case MessageTypeEnum.YardCommand: + HandleYardCommand(newData); + break; + + case MessageTypeEnum.ButtonAction: + HandleButtonAction(newData); + break; + + case MessageTypeEnum.RequestYard: + HandleYardRequest(newData); + break; + + case MessageTypeEnum.ToolPower: + HandleToolPower(newData); + break; + + case MessageTypeEnum.ShipyardCount: + HandleYardCount(newData); + break; + + case MessageTypeEnum.CustomInfo: + HandleCustomInfo(newData); + break; + } + recieveBlock.End(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine("Recieve(): " + ex); + } + } + + #region Comms Structs + + //(ノ◕ヮ◕)ノ*:・゚✧ ABSTRACTION! (ノ◕ヮ◕)ノ*:・゚✧ + public enum MessageTypeEnum : byte + { + ToolLine, + NewYard, + YardState, + ClientChat, + ServerChat, + ServerDialog, + ClearLine, + ShipyardSettings, + YardCommand, + ButtonAction, + RequestYard, + ToolPower, + ShipyardCount, + CustomInfo + } + + public struct ToolLineStruct + { + public long ToolId; + public long GridId; + public SerializableVector3I BlockPos; + public uint PackedColor; + public bool Pulse; + public byte EmitterIndex; + } + + public struct YardStruct + { + public long GridId; + public long[] ToolIds; + public ShipyardType YardType; + public long ButtonId; + } + + public struct DialogStruct + { + public string Title; + public string Subtitle; + public string Message; + public string ButtonText; + } + + #endregion + + #region Send Methods + + public static void SendLine(ToolLineStruct toolLine, Vector3D target) + { + string messageString = MyAPIGateway.Utilities.SerializeToXML(toolLine); + byte[] data = Encoding.UTF8.GetBytes(messageString); + SendMessageToNearby(target, 2000, MessageTypeEnum.ToolLine, data); + } + + public static void ClearLine(long toolId, int index) + { + var data = new byte[sizeof(long) + 1]; + data[0] = (byte)index; + BitConverter.GetBytes(toolId).CopyTo(data, 1); + SendMessageToClients(MessageTypeEnum.ClearLine, data); + } + + public static void SendYardState(ShipyardItem item) + { + var data = new byte[sizeof(long) + 1]; + BitConverter.GetBytes(item.EntityId).CopyTo(data, 0); + data[sizeof(long)] = (byte)item.YardType; + + SendMessageToClients(MessageTypeEnum.YardState, data); + } + + public static void SendNewYard(ShipyardItem item, ulong steamId = 0) + { + Logging.Instance.WriteLine("Sent Yard"); + var newYard = new YardStruct + { + GridId = item.EntityId, + ToolIds = item.Tools.Select(x => x.EntityId).ToArray(), + YardType = item.YardType + }; + + if (item.Menu?.Buttons != null) + { + Logging.Instance.WriteLine("Button ID: " + item.Menu.Buttons.EntityId); + newYard.ButtonId = item.Menu.Buttons.EntityId; + } + + string message = MyAPIGateway.Utilities.SerializeToXML(newYard); + byte[] data = Encoding.UTF8.GetBytes(message); + + if (steamId == 0) + SendMessageToClients(MessageTypeEnum.NewYard, data); + else + SendMessageTo(MessageTypeEnum.NewYard, data, steamId); + } + + public static void SendMessageToNearby(Vector3D target, double distance, MessageTypeEnum messageId, byte[] data) + { + var players = new List(); + MyAPIGateway.Players.GetPlayers(players); + double distSq = distance * distance; + foreach (IMyPlayer player in players) + { + //compare squared values for M A X I M U M S P E E D + if (Vector3D.DistanceSquared(player.GetPosition(), target) <= distSq) + SendMessageTo(messageId, data, player.SteamUserId); + } + } + + //SendMessageToOthers is apparently bugged + public static void SendMessageToClients(MessageTypeEnum messageId, byte[] data, bool skipLocal = false) + { + var players = new List(); + MyAPIGateway.Players.GetPlayers(players); + foreach (IMyPlayer player in players) + { + if (skipLocal && player == MyAPIGateway.Session.Player) + continue; + + SendMessageTo(messageId, data, player.SteamUserId); + } + } + + public static void SendMessageToServer(MessageTypeEnum messageId, byte[] data) + { + var newData = new byte[data.Length + 1]; + newData[0] = (byte)messageId; + data.CopyTo(newData, 1); + + Utilities.Invoke(() => MyAPIGateway.Multiplayer.SendMessageToServer(MESSAGE_ID, newData)); + } + + public static void SendMessageTo(MessageTypeEnum messageId, byte[] data, ulong steamId) + { + var newData = new byte[data.Length + 1]; + newData[0] = (byte)messageId; + data.CopyTo(newData, 1); + + Utilities.Invoke(() => MyAPIGateway.Multiplayer.SendMessageTo(MESSAGE_ID, newData, steamId)); + } + + public static void SendShipyardSettings(long entityId, YardSettingsStruct settings) + { + string message = MyAPIGateway.Utilities.SerializeToXML(settings); + byte[] messageBytes = Encoding.UTF8.GetBytes(message); + + var data = new byte[sizeof(long) + messageBytes.Length]; + + BitConverter.GetBytes(entityId).CopyTo(data, 0); + messageBytes.CopyTo(data, sizeof(long)); + + SendMessageToServer(MessageTypeEnum.ShipyardSettings, data); + } + + public static void SendPrivateInfo(ulong steamId, string message) + { + byte[] data = Encoding.ASCII.GetBytes(message); + + SendMessageTo(MessageTypeEnum.ServerChat, data, steamId); + } + + public static void SendDialog(ulong steamId, string title, string subtitle, string message, string button = "close") + { + var msg = new DialogStruct + { + Title = title, + Subtitle = subtitle, + Message = message, + ButtonText = button + }; + string serialized = MyAPIGateway.Utilities.SerializeToXML(msg); + byte[] data = Encoding.ASCII.GetBytes(serialized); + + SendMessageTo(MessageTypeEnum.ServerDialog, data, steamId); + } + + public static void SendYardCommand(long yardId, ShipyardType type) + { + var data = new byte[sizeof(long) + 1]; + BitConverter.GetBytes(yardId).CopyTo(data, 0); + data[sizeof(long)] = (byte)type; + + SendMessageToServer(MessageTypeEnum.YardCommand, data); + } + + private static DateTime _lastButtonSend = DateTime.Now; + + public static void SendButtonAction(long yardId, int index) + { + if (DateTime.Now - _lastButtonSend < TimeSpan.FromMilliseconds(100)) + return; + + _lastButtonSend = DateTime.Now; + var data = new byte[sizeof(long) + 1]; + BitConverter.GetBytes(yardId).CopyTo(data, 0); + data[sizeof(long)] = (byte)index; + SendMessageToServer(MessageTypeEnum.ButtonAction, data); + } + + public static void RequestShipyards() + { + byte[] data = BitConverter.GetBytes(MyAPIGateway.Session.Player.SteamUserId); + SendMessageToServer(MessageTypeEnum.RequestYard, data); + } + + public static void SendToolPower(long blockId, float power) + { + var data = new byte[sizeof(long) + sizeof(float)]; + BitConverter.GetBytes(blockId).CopyTo(data, 0); + BitConverter.GetBytes(power).CopyTo(data, sizeof(long)); + + SendMessageToClients(MessageTypeEnum.ToolPower, data); + } + + public static void SendYardCount() + { + byte[] data = BitConverter.GetBytes(ProcessShipyardDetection.ShipyardsList.Count); + + SendMessageToClients(MessageTypeEnum.ShipyardCount, data); + } + + public static void SendCustomInfo(long entityId, string info) + { + byte[] message = Encoding.UTF8.GetBytes(info); + var data = new byte[message.Length + sizeof(long)]; + BitConverter.GetBytes(entityId).CopyTo(data, 0); + Array.Copy(message, 0, data, sizeof(long), message.Length); + + SendMessageToClients(MessageTypeEnum.CustomInfo, data); + } + + #endregion + + #region Receive Methods + + private static void HandleToolPower(byte[] data) + { + long blockId = BitConverter.ToInt64(data, 0); + float power = BitConverter.ToSingle(data, sizeof(long)); + + IMyEntity entity; + if (!MyAPIGateway.Entities.TryGetEntityById(blockId, out entity)) + return; + + ((IMyCollector)entity).GameLogic.GetAs().SetPowerUse(power); + } + + private static void HandleToolLine(byte[] data) + { + string message = Encoding.UTF8.GetString(data); + var item = MyAPIGateway.Utilities.SerializeFromXML(message); + + IMyEntity toolEntity; + IMyEntity gridEntity; + + MyAPIGateway.Entities.TryGetEntityById(item.ToolId, out toolEntity); + MyAPIGateway.Entities.TryGetEntityById(item.GridId, out gridEntity); + + var tool = toolEntity as IMyCubeBlock; + var grid = gridEntity as IMyCubeGrid; + if (grid == null || tool == null) + return; + + var newLine = new LineItem + { + Start = MathUtility.CalculateEmitterOffset(tool, item.EmitterIndex), + End = grid.GridIntegerToWorld(item.BlockPos), + Color = new Color(item.PackedColor).ToVector4(), + Pulse = item.Pulse, + PulseVal = 0, + Index = item.EmitterIndex, + EmitterBlock = tool, + TargetGrid = grid, + TargetBlock = item.BlockPos, + }; + + if (newLine.Color == Color.OrangeRed.ToVector4()) + { + newLine.Color.W = 0.5f; + newLine.LinePackets = new PacketManager(newLine.End, newLine.Start, Color.Yellow.ToVector4(), invert: true); + } + else if (newLine.Color == Color.DarkCyan.ToVector4()) + newLine.LinePackets = new PacketManager(newLine.Start, newLine.End, Color.CadetBlue.ToVector4()); + else + newLine.LinePackets = null; + + if (LineDict.ContainsKey(item.ToolId)) + { + LineItem oldLine = null; + foreach (LineItem line in LineDict[item.ToolId]) + if (line.Index == item.EmitterIndex) + oldLine = line; + + if (oldLine == null) + { + LineDict[item.ToolId].Add(newLine); + return; + } + + if (oldLine.Color == Color.Purple.ToVector4() && item.ToolId != 0) + { + //if the old line is pulsing, and the new one is set to pulse, leave it alone so we don't reset the pulse cycle + if (newLine.Color == Color.Purple.ToVector4() && (oldLine.End == newLine.End)) + return; + + FadeList.Add(oldLine); + LineDict[item.ToolId].Remove(oldLine); + LineDict[item.ToolId].Add(newLine); + return; + } + + if (oldLine.End == newLine.End) + return; + + LineDict[item.ToolId].Remove(oldLine); + + if (newLine.Start != Vector3D.Zero) + LineDict[item.ToolId].Add(newLine); + + if (LineDict[item.ToolId].Count == 0) + LineDict.Remove(item.ToolId); + + FadeList.Add(oldLine); + } + else + LineDict[item.ToolId] = new List(3) {newLine}; + } + + private static void HandleNewShipyard(byte[] data) + { + Logging.Instance.WriteLine("ReceivedYard"); + string message = Encoding.UTF8.GetString(data); + var yardStruct = MyAPIGateway.Utilities.SerializeFromXML(message); + + //the server has already verified this shipyard. Don't question it, just make the shipyard item + if (yardStruct.ToolIds.Length != 8) + return; + + IMyEntity outEntity; + + if (!MyAPIGateway.Entities.TryGetEntityById(yardStruct.GridId, out outEntity)) + return; + + var yardGrid = outEntity as IMyCubeGrid; + if (yardGrid == null) + return; + + var points = new List(8); + var tools = new List(8); + foreach (long toolId in yardStruct.ToolIds) + { + IMyEntity entity; + if (!MyAPIGateway.Entities.TryGetEntityById(toolId, out entity)) + return; + + var block = entity as IMyCubeBlock; + if (block == null) + return; + + tools.Add(block); + points.Add(block.GetPosition()); + } + + MyOrientedBoundingBoxD yardBox = MathUtility.CreateOrientedBoundingBox(yardGrid, points, 2.5); + + var yardItem = new ShipyardItem(yardBox, tools.ToArray(), yardStruct.YardType, yardGrid); + + //TODO: this was part of the touch screen API, it bricks shipyards under certain circumstances with other (poorly made?) mods that affect something about button panels + //if (MyAPIGateway.Entities.TryGetEntityById(yardStruct.ButtonId, out outEntity)) + //{ + // try + // { + // Logging.Instance.WriteLine("Bind Buttons"); + // var buttons = (IMyButtonPanel)outEntity; + // buttons.ButtonPressed += yardItem.HandleButtonPressed; + // + // // Only set emissive parts if a config option allows it + // if (ShipyardSettings.Instance.UseExperimentalButtonGraphics) + // { + // var blockDef = (MyButtonPanelDefinition)MyDefinitionManager.Static.GetCubeBlockDefinition(buttons.BlockDefinition); + // for (int i = 1; i <= 4; i++) + // { + // var c = blockDef.ButtonColors[i % blockDef.ButtonColors.Length]; + // buttons.SetEmissiveParts($"Emissive{i}", new Color(c.X, c.Y, c.Z), c.W); + // } + // } + // + // buttons.SetCustomButtonName(0, "Exit"); + // buttons.SetCustomButtonName(1, "Up"); + // buttons.SetCustomButtonName(2, "Down"); + // buttons.SetCustomButtonName(3, "Select"); + // } + // catch (Exception ex) + // { + // Logging.Instance.WriteLine($"Error binding buttons: {ex}"); + // // Optionally, fall back to a non-experimental method + // } + //} + + foreach (IMyCubeBlock tool in yardItem.Tools) + { + var corner = ((IMyCollector)tool).GameLogic.GetAs(); + corner.Shipyard = yardItem; + //tool.SetEmissiveParts("Emissive1", Color.Yellow, 0f); + } + + yardItem.UpdateMaxPowerUse(); + yardItem.Settings = ShipyardSettings.Instance.GetYardSettings(yardItem.EntityId); + ProcessLocalYards.LocalYards.Add(yardItem); + } + + private static void HandleShipyardState(byte[] data) + { + long gridId = BitConverter.ToInt64(data, 0); + var type = (ShipyardType)data.Last(); + + Logging.Instance.WriteDebug("Got yard state: " + type); + Logging.Instance.WriteDebug($"Details: {gridId} {ProcessLocalYards.LocalYards.Count} [{string.Join(" ", data)}]"); + + foreach (ShipyardItem yard in ProcessLocalYards.LocalYards) + { + if (yard.EntityId != gridId) + continue; + + yard.YardType = type; + Logging.Instance.WriteLine(type.ToString()); + + foreach (IMyCubeBlock yardTool in yard.Tools) + yardTool?.GameLogic?.GetAs()?.UpdateVisuals(); + + switch (type) + { + case ShipyardType.Disabled: + case ShipyardType.Invalid: + yard.Disable(false); + + foreach (IMyCubeBlock tool in yard.Tools) + { + if (LineDict.ContainsKey(tool.EntityId)) + { + foreach (LineItem line in LineDict[tool.EntityId]) + FadeList.Add(line); + LineDict.Remove(tool.EntityId); + } + //tool.SetEmissiveParts("Emissive0", Color.White, 0.5f); + } + break; + case ShipyardType.Scanning: + ScanList.Add(new ScanAnimation(yard)); + //foreach(var tool in yard.Tools) + // tool.SetEmissiveParts("Emissive0", Color.Green, 0.5f); + break; + //case ShipyardType.Weld: + //foreach (var tool in yard.Tools) + // tool.SetEmissiveParts("Emissive0", Color.DarkCyan, 0.5f); + //break; + //case ShipyardType.Grind: + //foreach (var tool in yard.Tools) + // tool.SetEmissiveParts("Emissive0", Color.OrangeRed, 0.5f); + //break; + //default: + // throw new ArgumentOutOfRangeException(); + } + + yard.UpdateMaxPowerUse(); + + } + } + + private static void HandleClientChat(byte[] data) + { + ulong remoteSteamId = BitConverter.ToUInt64(data, 0); + string command = Encoding.UTF8.GetString(data, sizeof(ulong), data.Length - sizeof(ulong)); + if (!command.StartsWith("/shipyard")) + return; + + Logging.Instance.WriteLine("Received chat: " + command); + if (MyAPIGateway.Session.IsUserAdmin(remoteSteamId)) + { + if (command.Equals("/shipyard debug on")) + { + Logging.Instance.WriteLine("Debug turned on"); + ShipyardCore.Debug = true; + } + else if (command.Equals("/shipyard debug off")) + { + Logging.Instance.WriteLine("Debug turned off"); + ShipyardCore.Debug = false; + } + } + /* + foreach (ChatHandlerBase handler in ChatHandlers) + { + if (handler.CanHandle(remoteSteamId, command)) + { + string[] splits = command.Replace(handler.CommandText(), "").Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries); + handler.Handle(remoteSteamId, splits); + return; + } + } + */ + } + + private static void HandleServerChat(byte[] data) + { + if (MyAPIGateway.Session.Player == null) + return; + + string message = Encoding.ASCII.GetString(data); + + MyAPIGateway.Utilities.ShowMessage("Shipyard Overlord", message); + } + + private static void HandleServerDialog(byte[] data) + { + string serialized = Encoding.ASCII.GetString(data); + var msg = MyAPIGateway.Utilities.SerializeFromXML(serialized); + + MyAPIGateway.Utilities.ShowMissionScreen(msg.Title, null, msg.Subtitle, msg.Message.Replace("|", "\r\n"), null, msg.ButtonText); + } + + private static void HandleClearLine(byte[] bytes) + { + byte index = bytes[0]; + long toolId = BitConverter.ToInt64(bytes, 1); + + if (!LineDict.ContainsKey(toolId)) + return; + + List entry = LineDict[toolId]; + var toRemove = new HashSet(); + foreach (LineItem line in entry) + { + if (line.Index != index) + continue; + + line.FadeVal = 1; + FadeList.Add(line); + toRemove.Add(line); + } + + if (entry.Count == 0) + LineDict.Remove(toolId); + + foreach (LineItem line in toRemove) + LineDict[toolId].Remove(line); + } + + private static void HandleShipyardSettings(byte[] data) + { + long entityId = BitConverter.ToInt64(data, 0); + string message = Encoding.UTF8.GetString(data, sizeof(long), data.Length - sizeof(long)); + var settings = MyAPIGateway.Utilities.SerializeFromXML(message); + + bool found = false; + + Logging.Instance.WriteDebug($"Received shipyard settings:\r\n" + + $"\t{settings.EntityId}\r\n" + + $"\t{settings.BeamCount}\r\n" + + $"\t{settings.GuideEnabled}\r\n" + + $"\t{settings.GrindMultiplier}\r\n" + + $"\t{settings.WeldMultiplier}"); + + foreach (ShipyardItem yard in ProcessShipyardDetection.ShipyardsList) + { + if (yard.EntityId != entityId) + continue; + + yard.Settings = settings; + yard.UpdateMaxPowerUse(); // BeamCount and Multiplier affect our maxpower calculation + ShipyardSettings.Instance.SetYardSettings(yard.EntityId, settings); + found = true; + break; + } + + foreach (ShipyardItem yard in ProcessLocalYards.LocalYards) + { + if (yard.EntityId != entityId) + continue; + + yard.Settings = settings; + yard.UpdateMaxPowerUse(); // BeamCount and Multiplier affect our maxpower calculation + ShipyardSettings.Instance.SetYardSettings(yard.EntityId, settings); + + foreach (IMyCubeBlock tool in yard.Tools) + { + tool.GameLogic.GetAs().UpdateVisuals(); + } + + found = true; + break; + } + + if (found && MyAPIGateway.Multiplayer.IsServer) + { + ShipyardSettings.Instance.Save(); + //pass true to skip the local player + //on player-hosted MP we can get caught in an infinite loop if we don't + SendMessageToClients(MessageTypeEnum.ShipyardSettings, data, true); + } + } + + private static HashSet _processingYards = new HashSet(); + + private static void HandleYardCommand(byte[] data) + { + long yardId = BitConverter.ToInt64(data, 0); + var type = (ShipyardType)data.Last(); + + if (!_processingYards.Add(yardId)) + { + Logging.Instance.WriteDebug($"[HandleYardCommand] Yard {yardId} already being processed, skipping duplicate command"); + return; + } + + try + { + foreach (ShipyardItem yard in ProcessShipyardDetection.ShipyardsList) + { + if (yard.EntityId != yardId) + continue; + + if (!yard.CanProcessCommand(type)) + { + Logging.Instance.WriteDebug($"[HandleYardCommand] Yard {yardId} cannot process command at this time"); + return; + } + + if (type == ShipyardType.Disabled || type == ShipyardType.Invalid) + yard.Disable(); + else + yard.Init(type); + + break; + } + } + finally + { + _processingYards.Remove(yardId); + } + } + + private static void HandleButtonAction(byte[] data) + { + long yardId = BitConverter.ToInt64(data, 0); + byte index = data.Last(); + + foreach (ShipyardItem yard in ProcessShipyardDetection.ShipyardsList) + { + if (yard.EntityId == yardId && yard.Menu != null) + { + yard.Menu.ButtonPanelHandler(index); + return; + } + } + } + + private static void HandleYardRequest(byte[] data) + { + ulong steamId = BitConverter.ToUInt64(data, 0); + + Logging.Instance.WriteLine("Recieved shipyard request from " + steamId); + + if (!ProcessShipyardDetection.ShipyardsList.Any()) + return; + + foreach (ShipyardItem yard in ProcessShipyardDetection.ShipyardsList) + SendNewYard(yard, steamId); + } + + private static void HandleYardCount(byte[] data) + { + if (MyAPIGateway.Multiplayer.IsServer || MyAPIGateway.Session.Player?.Controller?.ControlledEntity?.Entity == null) + return; + + int count = BitConverter.ToInt32(data, 0); + + if (ProcessLocalYards.LocalYards.Count != count) + { + RequestShipyards(); + } + } + + private static void HandleCustomInfo(byte[] data) + { + long entityId = BitConverter.ToInt64(data, 0); + string info = Encoding.UTF8.GetString(data, sizeof(long), data.Length - sizeof(long)); + IMyEntity outEntity; + if (!MyAPIGateway.Entities.TryGetEntityById(entityId, out outEntity)) + return; + + IMyCollector col = outEntity as IMyCollector; + + if (col == null) + return; + + col.GameLogic.GetAs()?.SetInfo(info); + } + + #endregion + + #region Handler Setup + + public static void RegisterHandlers() + { + MyAPIGateway.Multiplayer.RegisterMessageHandler(MESSAGE_ID, Recieve); + } + + public static void UnregisterHandlers() + { + MyAPIGateway.Multiplayer.UnregisterMessageHandler(MESSAGE_ID, Recieve); + } + + #endregion + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/Extensions.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/Extensions.cs new file mode 100644 index 0000000..8c58740 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/Extensions.cs @@ -0,0 +1,193 @@ +using System.Collections.Generic; +using System.Linq; +using Sandbox.Game; +using Sandbox.Game.Entities; +using Sandbox.Game.Entities.Cube; +using Sandbox.ModAPI; +using VRage.Collections; +using VRage.Game; +using VRage.Game.Entity; +using VRage.Game.ModAPI; +using VRage.ModAPI; +using VRageMath; +using static ShipyardMod.Utility.Profiler; + +namespace ShipyardMod.Utility +{ + public static class Extensions + { + public static void Stop(this IMyEntity entity) + { + if (entity?.Physics == null || entity.Closed) + return; + + Utilities.Invoke(() => + { + if (entity.Physics == null || entity.Closed) + return; + entity.Physics.SetSpeeds(Vector3.Zero, Vector3.Zero); + /* + entity.Physics.Clear(); + if (!Vector3.IsZero(entity.Physics.LinearAcceleration) || !Vector3.IsZero(entity.Physics.AngularAcceleration)) + { + entity.Physics.AddForce(MyPhysicsForceType.APPLY_WORLD_IMPULSE_AND_WORLD_ANGULAR_IMPULSE, + Vector3.Negate(entity.Physics.LinearAcceleration) * entity.Physics.Mass, + entity.Center(), + Vector3.Negate(entity.Physics.AngularAcceleration) * entity.Physics.Mass); + } + */ + }); + } + + public static Vector3D Center(this IMyEntity ent) + { + return ent.WorldAABB.Center; + } + + public static Vector3D GetPosition(this IMySlimBlock block) + { + return block.CubeGrid.GridIntegerToWorld(block.Position); + } + + public static long SlimId(this IMySlimBlock block) + { + return block.CubeGrid.EntityId.GetHashCode() + block.Position.GetHashCode(); + } + + public static bool Closed(this IMySlimBlock block) + { + return block.CubeGrid?.GetCubeBlock(block.Position) == null; + } + + public static IMySlimBlock ProjectionResult(this IMySlimBlock block) + { + IMyProjector projector = block.CubeGrid.Projector(); + if (projector == null) + return null; + + Vector3D pos = block.GetPosition(); + IMyCubeGrid grid = projector.CubeGrid; + Vector3I gridPos = grid.WorldToGridInteger(pos); + return grid.GetCubeBlock(gridPos); + } + + public static IMyProjector Projector(this IMyCubeGrid grid) + { + return ((MyCubeGrid)grid).Projector; + } + + public static float BuildPercent(this IMySlimBlock block) + { + return block.Integrity / block.MaxIntegrity; + } + + public static bool PullAny(this MyInventory inventory, HashSet sourceInventories, string component, int count) + { + return PullAny(inventory, sourceInventories, new Dictionary {{component, count}}); + } + + public static bool PullAny(this MyInventory inventory, HashSet sourceInventories, Dictionary toPull) + { + bool result = false; + foreach (KeyValuePair entry in toPull) + { + int remainingAmount = entry.Value; + //Logging.Instance.WriteDebug(entry.Key + entry.Value); + foreach (IMyTerminalBlock block in sourceInventories) + { + if (block == null || block.Closed) + continue; + + MyInventory sourceInventory; + //get the output inventory for production blocks + if (((MyEntity)block).InventoryCount > 1) + sourceInventory = ((MyEntity)block).GetInventory(1); + else + sourceInventory = ((MyEntity)block).GetInventory(); + + List sourceItems = sourceInventory.GetItems(); + if (sourceItems.Count == 0) + continue; + + var toMove = new List>(); + foreach (MyPhysicalInventoryItem item in sourceItems) + { + if (item.Content.SubtypeName == entry.Key) + { + if (item.Amount <= 0) //KEEEN + continue; + + if (item.Amount >= remainingAmount) + { + toMove.Add(new KeyValuePair(item, remainingAmount)); + remainingAmount = 0; + result = true; + } + else + { + remainingAmount -= (int)item.Amount; + toMove.Add(new KeyValuePair(item, (int)item.Amount)); + result = true; + } + } + } + + foreach (KeyValuePair itemEntry in toMove) + { + if (inventory.ComputeAmountThatFits(itemEntry.Key.Content.GetId()) < itemEntry.Value) + return false; + + sourceInventory.Remove(itemEntry.Key, itemEntry.Value); + inventory.Add(itemEntry.Key, itemEntry.Value); + } + + if (remainingAmount == 0) + break; + } + } + + return result; + } + + public static bool IsInVoxels(this IMyCubeGrid grid) + { + /* + if (MyAPIGateway.Session.SessionSettings.StationVoxelSupport) + return grid.IsStatic; + + + List blocks = new List(); + List entities = new List(); + var box = grid.PositionComp.WorldAABB; + Utilities.InvokeBlocking(() => + { + entities = MyAPIGateway.Entities.GetTopMostEntitiesInBox(ref box); + grid.GetBlocks(blocks); + }); + + var voxels = entities.Where(e => e is IMyVoxelBase).ToList(); + + if (!voxels.Any()) + return false; + + foreach (var block in blocks) + { + BoundingBoxD blockBox; + block.GetWorldBoundingBox(out blockBox); + var cubeSize = block.CubeGrid.GridSize; + BoundingBoxD localAAABB = new BoundingBoxD(cubeSize * ((Vector3D)block.Position - 0.5), cubeSize * ((Vector3D)block.Max + 0.5)); + var gridWorldMatrix = block.CubeGrid.WorldMatrix; + foreach (var map in voxels) + { + if (((IMyVoxelBase)map).IsAnyAabbCornerInside(gridWorldMatrix, localAAABB)) + { + return true; + } + } + } + */ + return true; + + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/LCDMenu.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/LCDMenu.cs new file mode 100644 index 0000000..4030317 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/LCDMenu.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Sandbox.Definitions; +using Sandbox.ModAPI; +using SpaceEngineers.Game.ModAPI; +using VRage.Game.GUI.TextPanel; + +//Jimmacle provided the original LCD classes. These are heavily modified :P + +namespace ShipyardMod.ItemClasses +{ + public class LCDMenu + { + public IMyButtonPanel Buttons; + private MenuItem currentItem; + public IMyTextPanel Panel; + private int selectedItemIndex; + + public LCDMenu() + { + Root = new MenuItem("Root", null); + Root.root = this; + SetCurrentItem(Root); + } + + public MenuItem Root { get; set; } + + public void BindButtonPanel(IMyButtonPanel btnpnl) + { + //if (Buttons != null) + //{ + // Buttons.ButtonPressed -= ButtonPanelHandler; + //} + Buttons = btnpnl; + //Buttons.ButtonPressed += ButtonPanelHandler; + var blockDef = (MyButtonPanelDefinition)MyDefinitionManager.Static.GetCubeBlockDefinition(Buttons.BlockDefinition); + Buttons.SetEmissiveParts("Emissive1", blockDef.ButtonColors[1 % blockDef.ButtonColors.Length], 1); + Buttons.SetEmissiveParts("Emissive2", blockDef.ButtonColors[2 % blockDef.ButtonColors.Length], 1); + Buttons.SetEmissiveParts("Emissive3", blockDef.ButtonColors[3 % blockDef.ButtonColors.Length], 1); + Buttons.SetEmissiveParts("Emissive4", blockDef.ButtonColors[4 % blockDef.ButtonColors.Length], 1); + } + + public void BindLCD(IMyTextPanel txtpnl) + { + if (Panel is IMyTextSurfaceProvider && Panel != null) + { + Panel.WriteText("MENU UNBOUND"); + } + Panel = txtpnl; + Panel.ContentType = ContentType.TEXT_AND_IMAGE; + UpdateLCD(); + } + + public void SetCurrentItem(MenuItem item) + { + currentItem = item; + selectedItemIndex = 0; + } + + public void ButtonPanelHandler(int button) + { + switch (button) + { + case 0: + MenuActions.UpLevel(this, currentItem.Items[selectedItemIndex]); + break; + case 1: + if (selectedItemIndex > 0) + { + selectedItemIndex--; + } + else + selectedItemIndex = currentItem.Items.Count - 1; + break; + case 2: + if (selectedItemIndex < currentItem.Items.Count - 1) + { + selectedItemIndex++; + } + else + selectedItemIndex = 0; + break; + case 3: + currentItem.Items[selectedItemIndex].Invoke(); + break; + } + UpdateLCD(); + } + + public void UpdateLCD() + { + if (Panel != null) + { + var sb = new StringBuilder(); + + sb.Append(currentItem.UpdateDesc() ?? currentItem.Description); + for (int i = 0; i < currentItem.Items.Count; i++) + { + if (i == selectedItemIndex) + { + sb.Append("[ " + currentItem.Items[i].Name + " ]" + '\n'); + } + else + { + sb.Append(" " + currentItem.Items[i].Name + '\n'); + } + } + string result = sb.ToString(); + //saves bandwidth and render time + if (Panel.GetText() == result) + return; + Panel.WriteText(result); + //Panel.ShowPrivateTextOnScreen(); + } + } + + public void WriteHugeString(string message) + { + Panel.WriteText(""); + int charcount = 4200; //KEEN + + for (int i = 0; i < message.Length * charcount; i++) + { + string substring = message.Substring(i * charcount, Math.Min(charcount, message.Length - i * charcount)); + Panel.WriteText(substring, true); + } + } + } + + public class MenuItem + { + public delegate string DescriptionAction(); + + public delegate void MenuAction(); + + public LCDMenu root; + + public MenuItem(string name, string desc, MenuAction action = null, DescriptionAction descAct = null) + { + Name = name; + Action = action; + Description = desc; + DescAction = descAct; + Items = new List(); + } + + public MenuAction Action { get; set; } + + public DescriptionAction DescAction { get; set; } + + public MenuItem Parent { get; private set; } + public List Items { get; set; } + + public string Name { get; } + public string Description { get; set; } + + public void Add(MenuItem child) + { + child.root = root; + child.Parent = this; + Items.Add(child); + } + + public void Invoke() + { + if (Action != null) + Action.Invoke(); + } + + public string UpdateDesc() + { + if (DescAction != null) + return DescAction.Invoke(); + return null; + } + } + + public static class MenuActions + { + public static void DownLevel(LCDMenu root, MenuItem item) + { + root.SetCurrentItem(item); + } + + public static void UpLevel(LCDMenu root, MenuItem item) + { + if (item.Parent.Parent != null) + { + root.SetCurrentItem(item.Parent.Parent); + } + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/LineItem.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/LineItem.cs new file mode 100644 index 0000000..d07acc0 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/LineItem.cs @@ -0,0 +1,31 @@ +using VRage.Game.ModAPI; +using VRageMath; + +namespace ShipyardMod.ItemClasses +{ + public class LineItem + { + public Vector4 Color; + public bool Descend; + public Vector3D End; + public float FadeVal; + public byte Index; + public PacketManager LinePackets; + public bool Pulse; + public double PulseVal; + public Vector3D Start; + public IMyCubeBlock EmitterBlock; + public IMyCubeGrid TargetGrid; + public Vector3I TargetBlock; + + public LineItem(Vector3D start, Vector3D end) + { + Start = start; + End = end; + } + + public LineItem() + { + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/Logging.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/Logging.cs new file mode 100644 index 0000000..67a7a97 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/Logging.cs @@ -0,0 +1,140 @@ +using System; +using System.IO; +using System.Text; +using Sandbox.ModAPI; + +namespace ShipyardMod.Utility +{ + internal class Logging + { + private static Logging _instance; + + private static int lineCount; + private static bool init; + private static readonly string[] log = new string[10]; + private readonly StringBuilder _cache = new StringBuilder(); + private readonly TextWriter _writer; + private int _indent; + + public Logging(string logFile) + { + try + { + _writer = MyAPIGateway.Utilities.WriteFileInLocalStorage(logFile, typeof(Logging)); + _instance = this; + } + catch + { + } + } + + public static Logging Instance + { + get + { + if (MyAPIGateway.Utilities == null) + return null; + + if (_instance == null) + { + _instance = new Logging("ShipyardMod.log"); + } + + return _instance; + } + } + + public void IncreaseIndent() + { + _indent++; + } + + public void DecreaseIndent() + { + if (_indent > 0) + _indent--; + } + + public void WriteLine(string text) + { + try + { + if (_cache.Length > 0) + _writer.WriteLine(_cache); + + _cache.Clear(); + _cache.Append(DateTime.Now.ToString("[HH:mm:ss:ffff] ")); + for (int i = 0; i < _indent; i++) + _cache.Append("\t"); + + _writer.WriteLine(_cache.Append(text)); + _writer.Flush(); + _cache.Clear(); + } + catch + { + //logger failed, all hope is lost + } + } + + public void WriteDebug(string text) + { + if (ShipyardCore.Debug) + WriteLine(text); + } + + public void Debug_obj(string text) + { + if (!ShipyardCore.Debug) + return; + + WriteLine("\tDEBUG_OBJ: " + text); + + //server can't show objectives. probably. + if (MyAPIGateway.Session.Player == null) + return; + + //I'm the only one that needs to see this + if (MyAPIGateway.Session.Player.SteamUserId != 76561197996829390) + return; + + text = $"{DateTime.Now.ToString("[HH:mm:ss:ffff]")}: {text}"; + + if (!init) + { + init = true; + MyAPIGateway.Utilities.GetObjectiveLine().Title = "Shipyard debug"; + MyAPIGateway.Utilities.GetObjectiveLine().Objectives.Clear(); + MyAPIGateway.Utilities.GetObjectiveLine().Objectives.Add("Start"); + MyAPIGateway.Utilities.GetObjectiveLine().Show(); + } + if (lineCount > 9) + lineCount = 0; + log[lineCount] = text; + string[] oldLog = log; + for (int i = 0; i < 9; i++) + { + log[i] = oldLog[i + 1]; + } + log[9] = text; + + MyAPIGateway.Utilities.GetObjectiveLine().Objectives[0] = string.Join("\r\n", log); + lineCount++; + } + + public void Write(string text) + { + _cache.Append(text); + } + + + internal void Close() + { + if (_cache.Length > 0) + _writer.WriteLine(_cache); + + _writer.Flush(); + _writer.Close(); + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/MathUtility.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/MathUtility.cs new file mode 100644 index 0000000..42bf0fa --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/MathUtility.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ShipyardMod.ItemClasses; +using VRage.Game; +using VRage.Game.Components; +using VRage.Game.ModAPI; +using VRage.ModAPI; +using VRageMath; + +namespace ShipyardMod.Utility +{ + public static class MathUtility + { + private static readonly Vector3D[] Offsets = + { + new Vector3D(0.8103569, 0.2971599, 0.7976569), + new Vector3D(0.81244801, -0.813483256, -0.312517739), + new Vector3D(-0.311901606, -0.81946053, 0.802108187) + }; + + private static readonly Vector3D[] SmallOffsets = + { + new Vector3D(-0.84227, 0.84227, 0.34794), + new Vector3D(0.34649, 0.84227, -0.84083), + new Vector3D(-0.84227, -0.34649, -0.84083), + }; + + /// + /// Determines if a list of 8 Vector3I defines a rectangular prism aligned to the grid + /// + /// + /// + public static bool ArePointsOrthogonal(List points) + { + //get a list of unique Z values + int[] zVals = points.Select(p => p.Z).Distinct().ToArray(); + + //we should only have two + if (zVals.Length != 2) + return false; + + //get a list of all points in the two Z planes + List zPlane0 = points.FindAll(p => p.Z == zVals[0]); + List zPlane1 = points.FindAll(p => p.Z == zVals[1]); + + //we should have four of each + if (zPlane1.Count != 4 || zPlane0.Count != 4) + return false; + + //make sure each vertex in the maxZ plane has the same X and Y as only one point in the minZ plane + foreach (Vector3I zMaxPoint in zPlane0) + { + if (zPlane1.Count(zMinPoint => zMinPoint.X == zMaxPoint.X && zMinPoint.Y == zMaxPoint.Y) != 1) + return false; + } + + return true; + } + + /// + /// Create an OBB that encloses a grid + /// + /// + /// + public static MyOrientedBoundingBoxD CreateOrientedBoundingBox(IMyCubeGrid grid) + { + //quaternion to rotate the box + Quaternion gridQuaternion = Quaternion.CreateFromForwardUp( + grid.WorldMatrix.Forward, + grid.WorldMatrix.Up); + + //get the width of blocks for this grid size + float blocksize = grid.GridSize; + + //get the halfextents of the grid, then multiply by block size to get world halfextents + //add one so the line sits on the outside edge of the block instead of the center + var halfExtents = new Vector3D( + (Math.Abs(grid.Max.X - grid.Min.X) + 1) * blocksize / 2, + (Math.Abs(grid.Max.Y - grid.Min.Y) + 1) * blocksize / 2, + (Math.Abs(grid.Max.Z - grid.Min.Z) + 1) * blocksize / 2); + + return new MyOrientedBoundingBoxD(grid.PositionComp.WorldAABB.Center, halfExtents, gridQuaternion); + } + + /// + /// Create an OBB from a list of 8 verticies and align it to a grid + /// + /// + /// + /// Allows you to expand or shrink the box by a given number of meters + /// + public static MyOrientedBoundingBoxD CreateOrientedBoundingBox(IMyCubeGrid grid, List verticies, double offset = 0) + { + //create the quaternion to rotate the box around + Quaternion yardQuaternion = Quaternion.CreateFromForwardUp( + grid.WorldMatrix.Forward, + grid.WorldMatrix.Up); + + //find the center of the volume + var yardCenter = new Vector3D(); + + foreach (Vector3D vertex in verticies) + yardCenter = Vector3D.Add(yardCenter, vertex); + + yardCenter = Vector3D.Divide(yardCenter, verticies.Count); + + //find the dimensions of the box. + + //convert verticies to grid coordinates to find adjoining neighbors + var gridVerticies = new List(verticies.Count); + + foreach (Vector3D vertext in verticies) + gridVerticies.Add(grid.WorldToGridInteger(vertext)); + + double xLength = 0d; + double yLength = 0d; + double zLength = 0d; + + //finds the length of each axis + for (int i = 1; i < gridVerticies.Count; ++i) + { + if (gridVerticies[0].Y == gridVerticies[i].Y && gridVerticies[0].Z == gridVerticies[i].Z) + { + xLength = Math.Abs(gridVerticies[0].X - gridVerticies[i].X) * grid.GridSize; + continue; + } + + if (gridVerticies[0].X == gridVerticies[i].X && gridVerticies[0].Z == gridVerticies[i].Z) + { + yLength = Math.Abs(gridVerticies[0].Y - gridVerticies[i].Y) * grid.GridSize; + continue; + } + + if (gridVerticies[0].X == gridVerticies[i].X && gridVerticies[0].Y == gridVerticies[i].Y) + { + zLength = Math.Abs(gridVerticies[0].Z - gridVerticies[i].Z) * grid.GridSize; + } + } + + var halfExtents = new Vector3D(offset + xLength / 2, offset + yLength / 2, offset + zLength / 2); + + //FINALLY we can make the bounding box + return new MyOrientedBoundingBoxD(yardCenter, halfExtents, yardQuaternion); + } + + public static Vector3D CalculateEmitterOffset(IMyCubeBlock tool, byte index) + { + if (tool.BlockDefinition.SubtypeId.EndsWith("_Large")) + return Vector3D.Transform(Offsets[index], tool.WorldMatrix); + else + return Vector3D.Transform(SmallOffsets[index], tool.WorldMatrix); + } + + /// + /// Calculates an array of LineItems that describes an oriented bounding box + /// + /// + /// + public static LineItem[] CalculateObbLines(MyOrientedBoundingBoxD obb) + { + // ZMax ZMin + // 0----1 4----5 + // | | | | + // | | | | + // 3----2 7----6 + + var corners = new Vector3D[8]; + obb.GetCorners(corners, 0); + var lines = new LineItem[12]; + + //ZMax face + lines[0] = new LineItem(corners[0], corners[1]); + lines[1] = new LineItem(corners[1], corners[2]); + lines[2] = new LineItem(corners[2], corners[3]); + lines[3] = new LineItem(corners[3], corners[0]); + + //ZMin face + lines[4] = new LineItem(corners[4], corners[5]); + lines[5] = new LineItem(corners[5], corners[6]); + lines[6] = new LineItem(corners[6], corners[7]); + lines[7] = new LineItem(corners[7], corners[4]); + + //vertical lines + lines[8] = new LineItem(corners[0], corners[4]); + lines[9] = new LineItem(corners[1], corners[5]); + lines[10] = new LineItem(corners[2], corners[6]); + lines[11] = new LineItem(corners[3], corners[7]); + + return lines; + } + + /// + /// Calculates an array of LineItems that describes a bounding box + /// + /// + /// + public static LineItem[] CalculateBoxLines(BoundingBoxD box) + { + // ZMax ZMin + // 0----1 4----5 + // | | | | + // | | | | + // 3----2 7----6 + + Vector3D[] corners = box.GetCorners(); + var lines = new LineItem[12]; + + //ZMax face + lines[0] = new LineItem(corners[0], corners[1]); + lines[1] = new LineItem(corners[1], corners[2]); + lines[2] = new LineItem(corners[2], corners[3]); + lines[3] = new LineItem(corners[3], corners[0]); + + //ZMin face + lines[4] = new LineItem(corners[4], corners[5]); + lines[5] = new LineItem(corners[5], corners[6]); + lines[6] = new LineItem(corners[6], corners[7]); + lines[7] = new LineItem(corners[7], corners[4]); + + //vertical lines + lines[8] = new LineItem(corners[0], corners[4]); + lines[9] = new LineItem(corners[1], corners[5]); + lines[10] = new LineItem(corners[2], corners[6]); + lines[11] = new LineItem(corners[3], corners[7]); + + return lines; + } + + /// + /// Iterates through Vector3I positions between two points in a straight line + /// + public struct Vector3ILineIterator + { + private readonly Vector3I _start; + private readonly Vector3I _end; + private readonly Vector3I _direction; + + public Vector3I Current; + + public Vector3ILineIterator(Vector3I start, Vector3I end) + { + if (start == end) + throw new ArgumentException("Start and end cannot be equal"); + + _start = start; + _end = end; + Current = start; + _direction = Vector3I.Clamp(end - start, -Vector3I.One, Vector3I.One); + + if (_direction.RectangularLength() > 1) + throw new ArgumentException("Start and end are not in a straight line"); + } + + public bool IsValid() + { + return (_end - _start).RectangularLength() >= (Current - _start).RectangularLength(); + } + + public void MoveNext() + { + Current += _direction; + } + } + + private static readonly double[] SqrtCache = new double[50000]; + + /// + /// Caching square root method + /// + /// + /// + public static double Sqrt(int val) + { + if (val < 0) + return double.NaN; + + if (val == 0) + return 0; + + if (val >= SqrtCache.Length) + return Math.Sqrt(val); + + if (SqrtCache[val] != default(double)) + return SqrtCache[val]; + + SqrtCache[val] = Math.Sqrt(val); + return SqrtCache[val]; + } + + public static double Sqrt(double val) + { + return Sqrt((int)val); + } + + /// + /// Raises a value to the one-half power using our cached sqrt method + /// + /// + /// + public static double Pow15(double val) + { + return val * Sqrt(val); + } + + public static double MatchShipVelocity(IMyEntity modify, IMyEntity dest, bool recoil) + { + // local velocity of dest + var velTarget = dest.Physics.GetVelocityAtPoint(modify.Physics.CenterOfMassWorld); + var distanceFromTargetCom = modify.Physics.CenterOfMassWorld - dest.Physics.CenterOfMassWorld; + + var accelLinear = dest.Physics.LinearAcceleration; + var omegaVector = dest.Physics.AngularVelocity + dest.Physics.AngularAcceleration * MyEngineConstants.PHYSICS_STEP_SIZE_IN_SECONDS; + var omegaSquared = omegaVector.LengthSquared(); + // omega^2 * r == a + var accelRotational = omegaSquared * -distanceFromTargetCom; + var accelTarget = accelLinear + accelRotational; + + var velTargetNext = velTarget + accelTarget * MyEngineConstants.PHYSICS_STEP_SIZE_IN_SECONDS; + var velModifyNext = modify.Physics.LinearVelocity;// + modify.Physics.LinearAcceleration * MyEngineConstants.PHYSICS_STEP_SIZE_IN_SECONDS; + + var linearImpulse = modify.Physics.Mass * (velTargetNext - velModifyNext); + + // Angular matching. + // (dAA*dt + dAV) == (mAA*dt + mAV + tensorInverse*mAI) + var avelModifyNext = modify.Physics.AngularVelocity + modify.Physics.AngularAcceleration * MyEngineConstants.PHYSICS_STEP_SIZE_IN_SECONDS; + var angularDV = omegaVector - avelModifyNext; + var angularImpulse = Vector3.Zero; + // var angularImpulse = Vector3.TransformNormal(angularDV, modify.Physics.RigidBody.InertiaTensor); not accessible :/ + + // based on the large grid, small ion thruster. + const double wattsPerNewton = (3.36e6 / 288000); + // based on the large grid gyro + const double wattsPerNewtonMeter = (0.00003 / 3.36e7); + // (W/N) * (N*s) + (W/(N*m))*(N*m*s) == W + var powerCorrectionInJoules = (wattsPerNewton * linearImpulse.Length()) + (wattsPerNewtonMeter * angularImpulse.Length()); + modify.Physics.AddForce(MyPhysicsForceType.APPLY_WORLD_IMPULSE_AND_WORLD_ANGULAR_IMPULSE, linearImpulse, modify.Physics.CenterOfMassWorld, angularImpulse); + if(recoil) + dest.Physics.AddForce(MyPhysicsForceType.APPLY_WORLD_IMPULSE_AND_WORLD_ANGULAR_IMPULSE, -linearImpulse, dest.Physics.CenterOfMassWorld, -angularImpulse); + + return powerCorrectionInJoules * MyEngineConstants.PHYSICS_STEP_SIZE_IN_SECONDS; + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/PacketManager.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/PacketManager.cs new file mode 100644 index 0000000..0f5cc15 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/PacketManager.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using VRage.Game; +using VRage.Utils; +using VRageMath; + +namespace ShipyardMod.ItemClasses +{ + public class PacketManager + { + private readonly List _packets = new List(); + private readonly double _spacingSq; + private bool _init; + private double _multiplier; + private double _travelDist; + public Vector4 Color; + public Vector3D Origin { get; private set; } + public Vector3D Target { get; private set; } + private readonly bool _invert; + + public PacketManager(Vector3D origin, Vector3D target, Vector4 color, double spacing = 10, bool invert = false) + { + Origin = origin; + Target = target; + Color = color; + _invert = invert; + _spacingSq = spacing * spacing; + } + + public void Update(Vector3D origin, Vector3D target) + { + if (_invert) + { + Origin = target; + Target = origin; + } + else + { + Origin = origin; + Target = target; + } + } + + private void Init() + { + _travelDist = Vector3D.Distance(Origin, Target); + _packets.Add(new PacketItem(Origin)); + //packets move at 20 - 40m/s + Color.W = 1; + double speed = Math.Max(20, Math.Min(40, _travelDist / 3)); + _multiplier = 1 / (_travelDist / speed * 60); + } + + public void DrawPackets() + { + UpdatePackets(); + + foreach (PacketItem packet in _packets) + { + //thanks to Digi for showing me how this thing works + MyTransparentGeometry.AddPointBillboard(MyStringId.GetOrCompute("ShipyardPacket"), Color, packet.Position, 0.3f, packet.Ticks); + } + } + + private void UpdatePackets() + { + if (!_init) + { + _init = true; + Init(); + } + + var toRemove = new List(); + foreach (PacketItem packet in _packets) + { + packet.Ticks++; + packet.Position = Vector3D.Lerp(Origin, Target, _multiplier * packet.Ticks); + + //delete the packet once it gets to the destination + if (_multiplier * packet.Ticks > 1) + toRemove.Add(packet); + } + + foreach (PacketItem removePacket in toRemove) + _packets.Remove(removePacket); + + //if the last packet to go out is more than 10m from origin, add a new one + PacketItem lastPacket = _packets.LastOrDefault(); + if (lastPacket != null && Vector3D.DistanceSquared(lastPacket.Position, Origin) > _spacingSq) + _packets.Add(new PacketItem(Origin)); + } + + private class PacketItem + { + public Vector3D Position; + public int Ticks; + + public PacketItem(Vector3D position) + { + Position = position; + Ticks = 0; + } + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessConveyorCache.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessConveyorCache.cs new file mode 100644 index 0000000..f6692b3 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessConveyorCache.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.Linq; +using Sandbox.Game; +using Sandbox.Game.Entities; +using Sandbox.Game.GameSystems; +using Sandbox.ModAPI; +using ShipyardMod.ItemClasses; +using ShipyardMod.Utility; +using VRage.Game.Entity; +using VRage.Game.ModAPI; + +namespace ShipyardMod.ProcessHandlers +{ + public class ProcessConveyorCache : ProcessHandlerBase + { + public override int GetUpdateResolution() + { + return 5000; + } + + public override bool ServerOnly() + { + return true; + } + + private int _currentShipyardIndex = 0; + + public override void Handle() + { + if (ProcessShipyardDetection.ShipyardsList.Count == 0) + { + Logging.Instance.WriteDebug("No shipyards in ShipyardsList. Exiting Handle."); + return; + } + + int index = _currentShipyardIndex % ProcessShipyardDetection.ShipyardsList.Count; + ShipyardItem currentItem = ProcessShipyardDetection.ShipyardsList.ElementAtOrDefault(index); + if (currentItem == null) + { + Logging.Instance.WriteDebug($"No shipyard found at index {index}. Incrementing _currentShipyardIndex."); + _currentShipyardIndex++; + return; + } + + var grid = (IMyCubeGrid)currentItem.YardEntity; + + if (grid.Physics == null || grid.Closed || currentItem.YardType == ShipyardType.Invalid) + { + Logging.Instance.WriteDebug($"Invalid or closed grid for shipyard {currentItem.EntityId}. Clearing ConnectedCargo."); + currentItem.ConnectedCargo.Clear(); + _currentShipyardIndex++; + return; + } + + Logging.Instance.WriteDebug($"Processing shipyard {currentItem.EntityId} at index {index}."); + + var gts = MyAPIGateway.TerminalActionsHelper.GetTerminalSystemForGrid(grid); + var blocks = new List(); + gts.GetBlocks(blocks); + + var cornerInventory = (IMyInventory)((MyEntity)currentItem.Tools[0]).GetInventory(); + var disconnectedInventories = new HashSet(); + + foreach (var block in currentItem.ConnectedCargo) + { + if (block.Closed || !blocks.Contains(block)) + disconnectedInventories.Add(block); + } + + Logging.Instance.WriteDebug($"Disconnected inventories identified: {disconnectedInventories.Count} for shipyard {currentItem.EntityId}."); + + foreach (var dis in disconnectedInventories) + { + currentItem.ConnectedCargo.Remove(dis); + Logging.Instance.WriteDebug($"Removed disconnected block {dis.CustomName} from ConnectedCargo."); + } + + var newConnections = new HashSet(); + Utilities.InvokeBlocking(() => + { + Logging.Instance.WriteDebug("InvokeBlocking started for connectivity check."); + + foreach (IMyTerminalBlock cargo in currentItem.ConnectedCargo) + { + if (cornerInventory == null) + { + Logging.Instance.WriteDebug("Corner inventory is null, aborting connectivity check."); + return; + } + + if (!cornerInventory.IsConnectedTo(((MyEntity)cargo).GetInventory())) + disconnectedInventories.Add(cargo); + } + + foreach (var block in blocks) + { + if (disconnectedInventories.Contains(block) || currentItem.ConnectedCargo.Contains(block)) + continue; + + if (block.BlockDefinition.SubtypeName.Contains("ShipyardCorner") || block is IMyReactor || block is IMyGasGenerator || block is IMyGasTank) + continue; + + if (((MyEntity)block).HasInventory && cornerInventory.IsConnectedTo(((MyEntity)block).GetInventory())) + { + newConnections.Add(block); + Logging.Instance.WriteDebug($"Added new connection: {block.CustomName}"); + } + } + + Logging.Instance.WriteDebug("InvokeBlocking completed for connectivity check."); + }); + + foreach (IMyTerminalBlock removeBlock in disconnectedInventories) + { + currentItem.ConnectedCargo.Remove(removeBlock); + Logging.Instance.WriteDebug($"Disconnected block removed: {removeBlock.CustomName}"); + } + + foreach (IMyTerminalBlock newBlock in newConnections) + { + currentItem.ConnectedCargo.Add(newBlock); + Logging.Instance.WriteDebug($"Newly connected block added: {newBlock.CustomName}"); + } + + _currentShipyardIndex++; + Logging.Instance.WriteDebug($"Completed processing for shipyard {currentItem.EntityId}. Moving to next index {_currentShipyardIndex}."); + } + } +} diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessHandlerBase.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessHandlerBase.cs new file mode 100644 index 0000000..cf0434b --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessHandlerBase.cs @@ -0,0 +1,57 @@ +using System; +using Sandbox.ModAPI; +using ShipyardMod.Utility; + +namespace ShipyardMod.ProcessHandlers +{ + public abstract class ProcessHandlerBase + { + protected DateTime _lastUpdate; + + public ProcessHandlerBase() + { + LastUpdate = DateTime.Now; + } + + public DateTime LastUpdate + { + get { return _lastUpdate; } + set { _lastUpdate = value; } + } + + public virtual int GetUpdateResolution() + { + return 1000; + } + + public virtual bool ServerOnly() + { + return true; + } + + public virtual bool ClientOnly() + { + return false; + } + + public bool CanRun() + { + if (DateTime.Now - LastUpdate > TimeSpan.FromMilliseconds(GetUpdateResolution())) + { + if (ServerOnly() && MyAPIGateway.Multiplayer.IsServer) + { + //Logging.Instance.WriteDebug($"[ProcessHandler] {GetType().Name} can run on server"); + return true; + } + if (ClientOnly() && MyAPIGateway.Session.Player != null) + { + //Logging.Instance.WriteDebug($"[ProcessHandler] {GetType().Name} can run on client"); + return true; + } + } + return false; + } + + public abstract void Handle(); + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessLCDMenu.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessLCDMenu.cs new file mode 100644 index 0000000..c91b9fe --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessLCDMenu.cs @@ -0,0 +1,609 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Sandbox.Definitions; +using Sandbox.ModAPI; +using ShipyardMod.ItemClasses; +using ShipyardMod.Utility; +using SpaceEngineers.Game.ModAPI; +using VRage.Game.GUI.TextPanel; +using VRage.Game.ModAPI; +using VRageMath; + +namespace ShipyardMod.ProcessHandlers +{ + public class ProcessLCDMenu : ProcessHandlerBase + { + private static readonly string FullName = typeof(ProcessLCDMenu).FullName; + private readonly Dictionary _stats = new Dictionary(); + + private int _updateCount; + + public override int GetUpdateResolution() + { + return 200; + } + + public override bool ServerOnly() + { + return true; + } + + public override bool ClientOnly() + { + return false; + } + + public override void Handle() + { + foreach (ShipyardItem item in ProcessShipyardDetection.ShipyardsList) + { + if (item.Menu != null) + { + if (item.Menu.Panel.Closed) + item.Menu = null; + } + + if (item.Menu != null) + Utilities.Invoke(() => item.Menu.UpdateLCD()); + else + { + if (_updateCount < 5) + { + _updateCount++; + return; + } + _updateCount = 0; + + var yardGrid = (IMyCubeGrid)item.YardEntity; + + var blocks = new List(); + yardGrid.GetBlocks(blocks); + bool superFound = false; + + foreach (IMySlimBlock slimBlock in blocks) + { + var panel = slimBlock.FatBlock as IMyTextPanel; + + if (panel == null) + continue; + + if (!panel.CustomName.ToLower().Contains("shipyard")) + continue; + + var bound = new BoundingSphereD(panel.GetPosition(), 5); + List nearblocks = yardGrid.GetBlocksInsideSphere(ref bound); + bool found = true; + + foreach (IMySlimBlock block in nearblocks) + { + var buttons = block.FatBlock as IMyButtonPanel; + + if (buttons == null) + continue; + + superFound = true; + found = true; + Logging.Instance.WriteDebug("Found LCD pair for grid: " + item.EntityId); + //Utilities.Invoke(() => + //{ + // long id = item.EntityId; + // //item.Menu = new LCDMenu(); + // ShipyardControlReader reader = new ShipyardControlReader(); + // reader.ReadLCDContent(panel); + + // panel.Enabled = true; + + // if (panel.ContentType != ContentType.SCRIPT) + // { + // panel.ContentType = ContentType.SCRIPT; + // } + + // if (panel.Script != "ShipyardControl") + // { + // panel.Script = "ShipyardControl"; + // } + + // var shipyardControlScript = panel.GetScript(); + // var targetsVariable = shipyardControlScript.GetVariable("shipyardTargetList"); + + // if (targetsVariable != null) + // { + // // Get the value of the 'shipyardTargetList' variable + // string targetsValue = targetsVariable.GetValue().ToString(); + + // // Update the menu or perform any other actions based on the variable value + // // For example, update the shipyardTargetList label in the LCDMenu: + // reader.shipyardTargetList.Text = $"Targets: {targetsValue}"; + // } + + // // Rest of the code... + //}); + + Utilities.Invoke(() => + { + long id = item.EntityId; + item.Menu = new LCDMenu(); + panel.Enabled = true; + //panel.RequestEnable(false); + //panel.RequestEnable(true); + //item.Menu.BindButtonPanel(buttons); + item.Menu.BindLCD(panel); + var mainMenu = new MenuItem("", "", null, MenuDel(id)); + var statusMenu = new MenuItem("", "", null, StatusDel(id)); + var scanMenu = new MenuItem("", "Get details for: \r\n"); + var grindStatsMenu = new MenuItem("SCANNING...", "", null, GrindStatsDel(id)); + var weldStatsMenu = new MenuItem("SCANNING...", "", null, WeldStatsDel(id)); + var returnMenu = new MenuItem("Return", "", ScanDel(item.Menu, mainMenu, 0)); + mainMenu.root = item.Menu; + mainMenu.Add(new MenuItem("Grind", "", GrindDel(item.Menu, statusMenu))); + mainMenu.Add(new MenuItem("Weld", "", WeldDel(item.Menu, statusMenu))); + mainMenu.Add(new MenuItem("Scan", "", ScanDel(item.Menu, scanMenu))); + statusMenu.root = item.Menu; + statusMenu.Add(new MenuItem("Stop", "", StopDel(item.Menu, mainMenu))); + scanMenu.Add(new MenuItem("Grind", "", ScanDel(item.Menu, grindStatsMenu, ShipyardType.Scanning))); + scanMenu.Add(new MenuItem("Weld", "", ScanDel(item.Menu, weldStatsMenu, ShipyardType.Scanning))); + grindStatsMenu.Add(returnMenu); + weldStatsMenu.Add(returnMenu); + item.Menu.Root = mainMenu; + item.Menu.SetCurrentItem(mainMenu); + item.Menu.UpdateLCD(); + Communication.SendNewYard(item); + }); + + break; + } + + if (found) + break; + } + Logging.Instance.WriteDebug($"SuperFind {item.EntityId}: {superFound}"); + } + } + } + + public string FormatStatus(ShipyardItem item) + { + bool welding = false; + var result = new StringBuilder(); + + result.Append("Shipyard Status:\r\n"); + switch (item.YardType) + { + case ShipyardType.Disabled: + result.Append("IDLE"); + break; + + case ShipyardType.Grind: + result.Append("GRINDING"); + break; + + case ShipyardType.Weld: + result.Append("WELDING"); + welding = true; + break; + + default: + result.Append("ERROR"); + return result.ToString(); + } + + result.Append("\r\n\r\n"); + + if (welding && item.MissingComponentsDict.Count > 0) + { + result.Append("Missing Components:\r\n"); + foreach (KeyValuePair component in item.MissingComponentsDict) + { + result.Append($"{component.Key}: {component.Value}\r\n"); + } + result.Append("\r\n"); + } + + float time = 0f; + + if (item.YardType == ShipyardType.Grind) + { + foreach (BlockTarget target in item.TargetBlocks) + { + if (target.Projector != null) + continue; + time += target.Block.Integrity / ((MyCubeBlockDefinition)target.Block.BlockDefinition).IntegrityPointsPerSec; + } + time /= item.Settings.GrindMultiplier; + time = time / (item.Settings.BeamCount * 8); + } + else //welding + { + foreach (BlockTarget target in item.TargetBlocks) + { + if (target.Projector != null) + time += target.BuildTime; + else + time += (target.Block.MaxIntegrity - target.Block.Integrity) / ((MyCubeBlockDefinition)target.Block.BlockDefinition).IntegrityPointsPerSec; + } + time /= item.Settings.WeldMultiplier; + time = time / (item.Settings.BeamCount * 8); + } + + int active = item.BlocksToProcess.Sum(entry => entry.Value.Count(target => target != null)); + + result.Append("Targets: " + item.YardGrids.Count); + result.Append("\r\n"); + result.Append($"Active beams: {active}/{item.Settings.BeamCount * 8}"); + result.Append("\r\n"); + result.Append("Blocks remaining: " + item.TargetBlocks.Count); + result.Append("\r\n"); + result.Append("Estimated time remaining: " + TimeSpan.FromSeconds((int)time).ToString("g")); + result.Append("\r\n\r\n"); + return result.ToString(); + } + + private string FormatGrindStats(ShipyardItem item) + { + if (!_stats.ContainsKey(item)) + _stats.Add(item, new StatInfo()); + + StatInfo stats = _stats[item]; + if (stats.StartTime == 0) + { + stats.StartTime = MyAPIGateway.Session.ElapsedPlayTime.TotalMilliseconds; + //calculating stats can take tens or hundreds of ms, so drop it in a thread and check back when the scan animation is finished. + MyAPIGateway.Parallel.StartBackground(() => + { + try + { + var statBlock = Profiler.Start(FullName, nameof(FormatGrindStats)); + stats.TotalComponents = new Dictionary(); + var blockList = new List(); + var processGrids = new HashSet(); + processGrids.UnionWith(item.YardGrids); + processGrids.UnionWith(item.ContainsGrids); + foreach (IMyCubeGrid grid in processGrids) + { + if (grid?.Physics == null || grid.Closed) + continue; + grid.GetBlocks(blockList); + stats.BlockCount += blockList.Count; + + var missingComponents = new Dictionary(); + foreach (IMySlimBlock block in blockList) + { + //var blockDef = (MyObjectBuilder_CubeBlockDefinition)block.BlockDefinition.GetObjectBuilder(); + //grindTime += Math.Max(blockDef.BuildTimeSeconds / ShipyardSettings.Instance.GrindMultiplier, 0.5f); + + var blockDef = (MyCubeBlockDefinition)block.BlockDefinition; + //IntegrityPointsPerSec = MaxIntegrity / BuildTimeSeconds + //this is much, much faster than pulling the objectbuilder and getting the data from there. + stats.GrindTime += Math.Max(block.BuildPercent() * (blockDef.MaxIntegrity / blockDef.IntegrityPointsPerSec) / MyAPIGateway.Session.GrinderSpeedMultiplier / item.Settings.GrindMultiplier, 0.5f); + + block.GetMissingComponents(missingComponents); + + foreach (MyCubeBlockDefinition.Component component in blockDef.Components) + { + if (stats.TotalComponents.ContainsKey(component.Definition.Id.SubtypeName)) + stats.TotalComponents[component.Definition.Id.SubtypeName] += component.Count; + else + stats.TotalComponents.Add(component.Definition.Id.SubtypeName, component.Count); + } + } + + foreach (KeyValuePair missing in missingComponents) + { + if (stats.TotalComponents.ContainsKey(missing.Key)) + stats.TotalComponents[missing.Key] -= missingComponents[missing.Key]; + } + + blockList.Clear(); + } + + var result = new StringBuilder(); + result.Append("Scan Results:\r\n\r\n"); + result.Append("Targets: " + item.YardGrids.Count); + result.Append("\r\n"); + result.Append("Block Count: " + stats.BlockCount); + result.Append("\r\n"); + float grindTime = stats.GrindTime / (item.Settings.BeamCount * 8); + if (grindTime >= 7200) + result.Append("Estimated Deconstruct Time: " + (grindTime / 3600).ToString("0.00") + " hours"); + else if (grindTime >= 120) + result.Append("Estimated Deconstruct Time: " + (grindTime / 60).ToString("0.00") + " min"); + else + result.Append("Estimated Deconstruct Time: " + grindTime.ToString("0.00") + "s"); + result.Append("\r\n"); + result.Append("Estimated Component Gain:\r\n\r\n"); + double multiplier = Math.Max(item.ShipyardBox.HalfExtent.LengthSquared() / 200000, 1); + foreach (KeyValuePair component in stats.TotalComponents) + { + if (component.Value != 0) + result.Append($"{component.Key}: {component.Value / multiplier}\r\n"); + } + + stats.Output = result.ToString(); + statBlock.End(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine(ex.ToString()); + } + }); + } + + if (!string.IsNullOrEmpty(stats.Output) && MyAPIGateway.Session.ElapsedPlayTime.TotalMilliseconds - stats.StartTime > 6000) + return stats.Output; + return "SCANNING...\r\n\r\n"; + } + + private string FormatWeldStats(ShipyardItem item) + { + if (!_stats.ContainsKey(item)) + _stats.Add(item, new StatInfo()); + + StatInfo stats = _stats[item]; + if (stats.StartTime == 0) + { + stats.StartTime = MyAPIGateway.Session.ElapsedPlayTime.TotalMilliseconds; + //calculating stats can take tens or hundreds of ms, so drop it in a thread and check back when the scan animation is finished. + MyAPIGateway.Parallel.StartBackground(() => + { + try + { + var statBlock = Profiler.Start(FullName, nameof(FormatWeldStats)); + stats.TotalComponents = new Dictionary(); + var blockList = new List(); + var processGrids = new HashSet(); + processGrids.UnionWith(item.YardGrids); + processGrids.UnionWith(item.ContainsGrids); + + var projections = new HashSet(); + + foreach (IMyCubeGrid grid in processGrids) + { + if (grid.MarkedForClose || grid.Closed) + continue; + + grid.GetBlocks(blockList); + + foreach (IMySlimBlock block in blockList) + { + var projector = block?.FatBlock as IMyProjector; + if (projector == null) + continue; + if (projector.IsProjecting && projector.ProjectedGrid != null) + projections.Add(projector.ProjectedGrid); + } + + blockList.Clear(); + } + + processGrids.UnionWith(projections); + + foreach (IMyCubeGrid grid in processGrids) + { + if (grid.MarkedForClose || grid.Closed) + continue; + + grid.GetBlocks(blockList); + stats.BlockCount += blockList.Count; + + var missingComponents = new Dictionary(); + foreach (IMySlimBlock block in blockList) + { + //var blockDef = (MyObjectBuilder_CubeBlockDefinition)block.BlockDefinition.GetObjectBuilder(); + //grindTime += Math.Max(blockDef.BuildTimeSeconds / ShipyardSettings.Instance.GrindMultiplier, 0.5f); + + var blockDef = (MyCubeBlockDefinition)block.BlockDefinition; + //IntegrityPointsPerSec = MaxIntegrity / BuildTimeSeconds + //this is much, much faster than pulling the objectbuilder and getting the data from there. + if (grid.Physics != null) + stats.GrindTime += Math.Max((1 - block.BuildPercent()) * (blockDef.MaxIntegrity / blockDef.IntegrityPointsPerSec) / MyAPIGateway.Session.WelderSpeedMultiplier / item.Settings.WeldMultiplier, 0.5f); + else + stats.GrindTime += Math.Max(blockDef.MaxIntegrity / blockDef.IntegrityPointsPerSec / MyAPIGateway.Session.WelderSpeedMultiplier / item.Settings.WeldMultiplier, 0.5f); + + block.GetMissingComponents(missingComponents); + + if (grid.Physics != null) + { + foreach (KeyValuePair missing in missingComponents) + { + if (!stats.TotalComponents.ContainsKey(missing.Key)) + stats.TotalComponents.Add(missing.Key, missing.Value); + else + stats.TotalComponents[missing.Key] += missing.Value; + } + } + else + { + //projections will always consume the fully component count + foreach (MyCubeBlockDefinition.Component component in blockDef.Components) + { + if (stats.TotalComponents.ContainsKey(component.Definition.Id.SubtypeName)) + stats.TotalComponents[component.Definition.Id.SubtypeName] += component.Count; + else + stats.TotalComponents.Add(component.Definition.Id.SubtypeName, component.Count); + } + } + missingComponents.Clear(); + } + + blockList.Clear(); + } + + var result = new StringBuilder(); + result.Append("Scan Results:\r\n\r\n"); + result.Append("Targets: " + item.YardGrids.Count); + result.Append("\r\n"); + result.Append("Block Count: " + stats.BlockCount); + result.Append("\r\n"); + float grindTime = stats.GrindTime / (item.Settings.BeamCount * 8); + if (grindTime >= 7200) + result.Append("Estimated Construct Time: " + (grindTime / 3600).ToString("0.00") + " hours"); + else if (grindTime >= 120) + result.Append("Estimated Construct Time: " + (grindTime / 60).ToString("0.00") + " min"); + else + result.Append("Estimated Construct Time: " + grindTime.ToString("0.00") + "s"); + result.Append("\r\n"); + if (stats.TotalComponents.Any()) + { + result.Append("Estimated Components Used:\r\n\r\n"); + double multiplier = Math.Max(item.ShipyardBox.HalfExtent.LengthSquared() / 200000, 1); + foreach (KeyValuePair component in stats.TotalComponents) + { + if (component.Value != 0) + result.Append($"{component.Key}: {component.Value / multiplier}\r\n"); + } + } + + stats.Output = result.ToString(); + statBlock.End(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine(ex.ToString()); + } + }); + } + + if (!string.IsNullOrEmpty(stats.Output) && MyAPIGateway.Session.ElapsedPlayTime.TotalMilliseconds - stats.StartTime > 6000) + return stats.Output; + return "SCANNING..."; + } + + private string FormatMainMenu(ShipyardItem item) + { + var result = new StringBuilder(); + result.Append("Automated Shipyard Main Menu\r\n\r\n"); + result.Append("Current targets: " + item.ContainsGrids.Count); + result.Append("\r\n\r\n"); + + result.Append(". Exit : Up :. Down :: Select"); + result.Append("\r\n\r\n"); + + return result.ToString(); + } + + private MenuItem.MenuAction WeldDel(LCDMenu root, MenuItem item) + { + MenuItem.MenuAction handler = () => + { + long id = root.Panel.CubeGrid.EntityId; + + root.SetCurrentItem(item); + Communication.SendYardCommand(id, ShipyardType.Weld); + }; + return handler; + } + + private MenuItem.MenuAction GrindDel(LCDMenu root, MenuItem item) + { + MenuItem.MenuAction handler = () => + { + long id = root.Panel.CubeGrid.EntityId; + + root.SetCurrentItem(item); + Communication.SendYardCommand(id, ShipyardType.Grind); + }; + return handler; + } + + private MenuItem.MenuAction ScanDel(LCDMenu root, MenuItem item, ShipyardType? itemType = null) + { + MenuItem.MenuAction handler = () => + { + long id = root.Panel.CubeGrid.EntityId; + + root.SetCurrentItem(item); + if (itemType.HasValue) + Communication.SendYardCommand(id, itemType.Value); + if (itemType == 0) + foreach (ShipyardItem yard in ProcessShipyardDetection.ShipyardsList) + { + if (yard.EntityId == id) + { + _stats.Remove(yard); + break; + } + } + }; + return handler; + } + + private MenuItem.MenuAction StopDel(LCDMenu root, MenuItem item) + { + MenuItem.MenuAction handler = () => + { + long id = root.Panel.CubeGrid.EntityId; + + root.SetCurrentItem(item); + Communication.SendYardCommand(id, ShipyardType.Disabled); + }; + return handler; + } + + private MenuItem.DescriptionAction StatusDel(long id) + { + MenuItem.DescriptionAction handler = () => + { + foreach (ShipyardItem item in ProcessShipyardDetection.ShipyardsList) + { + if (item.EntityId == id) + return FormatStatus(item); + } + return ""; + }; + return handler; + } + + private MenuItem.DescriptionAction GrindStatsDel(long id) + { + MenuItem.DescriptionAction handler = () => + { + foreach (ShipyardItem item in ProcessShipyardDetection.ShipyardsList) + { + if (item.EntityId == id) + return FormatGrindStats(item); + } + return ""; + }; + return handler; + } + + private MenuItem.DescriptionAction WeldStatsDel(long id) + { + MenuItem.DescriptionAction handler = () => + { + foreach (ShipyardItem item in ProcessShipyardDetection.ShipyardsList) + { + if (item.EntityId == id) + return FormatWeldStats(item); + } + return ""; + }; + return handler; + } + + private MenuItem.DescriptionAction MenuDel(long id) + { + MenuItem.DescriptionAction handler = () => + { + foreach (ShipyardItem item in ProcessShipyardDetection.ShipyardsList) + { + if (item.EntityId == id) + return FormatMainMenu(item); + } + return ""; + }; + return handler; + } + + private class StatInfo + { + public int BlockCount; + public float GrindTime; + public string Output; + public double StartTime; + public Dictionary TotalComponents; + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessLocalYards.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessLocalYards.cs new file mode 100644 index 0000000..7569d15 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessLocalYards.cs @@ -0,0 +1,224 @@ +using System.Collections.Generic; +using System.Linq; +using Sandbox.Definitions; +using Sandbox.Game.Entities; +using Sandbox.ModAPI; +using ShipyardMod.ItemClasses; +using ShipyardMod.Utility; +using VRage.Collections; +using VRage.Game.ModAPI; +using VRage.ModAPI; +using VRageMath; + +namespace ShipyardMod.ProcessHandlers +{ + internal class ProcessLocalYards : ProcessHandlerBase + { + public static MyConcurrentHashSet LocalYards = new MyConcurrentHashSet(); + private static readonly string FullName = typeof(ProcessLocalYards).FullName; + + public override int GetUpdateResolution() + { + return 500; + } + + public override bool ServerOnly() + { + return false; + } + + public override bool ClientOnly() + { + return true; + } + + public override void Handle() + { + Logging.Instance.WriteDebug("ProcessLocalYards Start"); + var removeYards = new HashSet(); + + foreach (ShipyardItem item in LocalYards) + { + //see if the shipyard has been deleted + if (item.YardEntity.Closed || item.YardEntity.Physics == null || item.YardType == ShipyardType.Invalid + || (item.StaticYard && !item.YardEntity.Physics.IsStatic)) + { + //the client shouldn't tell the server the yard is invalid + //item.Disable(); + removeYards.Add(item); + continue; + } + + if (item.StaticYard) + UpdateBoxLines(item); + + //don't draw boxes inside active yards, it's distracting + if (item.YardType != ShipyardType.Disabled) + continue; + + var corners = new Vector3D[8]; + item.ShipyardBox.GetCorners(corners, 0); + double dist = Vector3D.DistanceSquared(corners[0], item.ShipyardBox.Center); + + var sphere = new BoundingSphereD(item.ShipyardBox.Center, dist); + + //Utilities.InvokeBlocking(()=> entities = MyAPIGateway.Entities.GetTopMostEntitiesInSphere(ref sphere)); + List entities = MyAPIGateway.Entities.GetTopMostEntitiesInSphere(ref sphere); + + if (entities.Count == 0) + { + Logging.Instance.WriteDebug("Couldn't get entities in ProcessLocalYards"); + continue; + } + var removeGrids = new HashSet(); + foreach (IMyEntity entity in entities) + { + var grid = entity as IMyCubeGrid; + if (grid == null) + continue; + + if (grid.EntityId == item.EntityId) + continue; + + //workaround to not blind people with digi's helmet mod + if (grid.Physics == null && grid.Projector() == null) + continue; + + if (grid.Closed || grid.MarkedForClose) + { + removeGrids.Add(grid); + continue; + } + + if (LocalYards.Any(x => x.EntityId == grid.EntityId)) + continue; + + //create a bounding box around the ship + MyOrientedBoundingBoxD gridBox = MathUtility.CreateOrientedBoundingBox(grid); + + //check if the ship bounding box is completely inside the yard box + ContainmentType result = item.ShipyardBox.Contains(ref gridBox); + + if (result == ContainmentType.Contains) + { + item.ContainsGrids.Add(grid); + item.IntersectsGrids.Remove(grid); + } + else if (result == ContainmentType.Intersects) + { + item.IntersectsGrids.Add(grid); + item.ContainsGrids.Remove(grid); + } + else + { + removeGrids.Add(grid); + } + } + + foreach (IMyCubeGrid containGrid in item.ContainsGrids) + if (!entities.Contains(containGrid)) + removeGrids.Add(containGrid); + + foreach (IMyCubeGrid intersectGrid in item.IntersectsGrids) + if (!entities.Contains(intersectGrid)) + removeGrids.Add(intersectGrid); + + foreach (IMyCubeGrid removeGrid in removeGrids) + { + ShipyardCore.BoxDict.Remove(removeGrid.EntityId); + item.ContainsGrids.Remove(removeGrid); + item.IntersectsGrids.Remove(removeGrid); + } + } + + foreach (ShipyardItem removeItem in removeYards) + { + foreach (IMyCubeGrid grid in removeItem.ContainsGrids) + { + ShipyardCore.BoxDict.Remove(grid.EntityId); + } + foreach (IMyCubeGrid grid in removeItem.IntersectsGrids) + { + ShipyardCore.BoxDict.Remove(grid.EntityId); + } + + LocalYards.Remove(removeItem); + } + } + + // OBB corner structure + // ZMax ZMin + // 0----1 4----5 + // | | | | + // | | | | + // 3----2 7----6 + /// + /// Updates the internal list of lines so we only draw a laser if there is a frame to contain it + /// + /// + private void UpdateBoxLines(ShipyardItem item) + { + var lineBlock = Profiler.Start(FullName, nameof(UpdateBoxLines)); + var corners = new Vector3D[8]; + item.ShipyardBox.GetCorners(corners, 0); + var grid = (IMyCubeGrid)item.YardEntity; + + var gridCorners = new Vector3I[8]; + for (int i = 0; i < 8; i++) + gridCorners[i] = grid.WorldToGridInteger(corners[i]); + + item.BoxLines.Clear(); + + //okay, really long unrolled loop coming up, but it's the simplest way to do it + //zMax face + if (WalkLine(gridCorners[0], gridCorners[1], grid)) + item.BoxLines.Add(new LineItem(corners[0], corners[1])); + if (WalkLine(gridCorners[1], gridCorners[2], grid)) + item.BoxLines.Add(new LineItem(corners[1], corners[2])); + if (WalkLine(gridCorners[2], gridCorners[3], grid)) + item.BoxLines.Add(new LineItem(corners[2], corners[3])); + if (WalkLine(gridCorners[3], gridCorners[0], grid)) + item.BoxLines.Add(new LineItem(corners[3], corners[0])); + //zMin face + if (WalkLine(gridCorners[4], gridCorners[5], grid)) + item.BoxLines.Add(new LineItem(corners[4], corners[5])); + if (WalkLine(gridCorners[5], gridCorners[6], grid)) + item.BoxLines.Add(new LineItem(corners[5], corners[6])); + if (WalkLine(gridCorners[6], gridCorners[7], grid)) + item.BoxLines.Add(new LineItem(corners[6], corners[7])); + if (WalkLine(gridCorners[7], gridCorners[4], grid)) + item.BoxLines.Add(new LineItem(corners[7], corners[4])); + //connecting lines + if (WalkLine(gridCorners[0], gridCorners[4], grid)) + item.BoxLines.Add(new LineItem(corners[0], corners[4])); + if (WalkLine(gridCorners[1], gridCorners[5], grid)) + item.BoxLines.Add(new LineItem(corners[1], corners[5])); + if (WalkLine(gridCorners[2], gridCorners[6], grid)) + item.BoxLines.Add(new LineItem(corners[2], corners[6])); + if (WalkLine(gridCorners[3], gridCorners[7], grid)) + item.BoxLines.Add(new LineItem(corners[3], corners[7])); + + lineBlock.End(); + } + + private bool WalkLine(Vector3I start, Vector3I end, IMyCubeGrid grid) + { + var it = new MathUtility.Vector3ILineIterator(start, end); + while (it.IsValid()) + { + IMySlimBlock block = grid.GetCubeBlock(it.Current); + it.MoveNext(); + + if (block == null) + return false; + + if (!block.BlockDefinition.Id.SubtypeName.Contains("Shipyard")) + return false; + + if (block.BuildPercent() < ((MyCubeBlockDefinition)block.BlockDefinition).CriticalIntegrityRatio) + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessShipyardAction.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessShipyardAction.cs new file mode 100644 index 0000000..015b6ea --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessShipyardAction.cs @@ -0,0 +1,966 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Sandbox.Definitions; +using Sandbox.Game; +using Sandbox.Game.Entities; +using Sandbox.Game.Weapons; +using Sandbox.Game.World; +using Sandbox.ModAPI; +using ShipyardMod.ItemClasses; +using ShipyardMod.Utility; +using VRage; +using VRage.Game; +using VRage.Game.Entity; +using VRage.Game.ModAPI; +using VRage.ModAPI; +using VRageMath; + +namespace ShipyardMod.ProcessHandlers +{ + public class ProcessShipyardAction : ProcessHandlerBase + { + private static readonly string FullName = typeof(ProcessShipyardAction).FullName; + + private readonly HashSet _stalledTargets = new HashSet(); + + private readonly MyInventory _tmpInventory = new MyInventory(); + + public override int GetUpdateResolution() + { + return 5000; + } + + public override bool ServerOnly() + { + return true; + } + + public override void Handle() + { + if (ProcessShipyardDetection.ShipyardsList.Count == 0) + { + Logging.Instance.WriteDebug("[ProcessConveyorCache] No shipyards to process"); + return; + } + foreach (ShipyardItem item in ProcessShipyardDetection.ShipyardsList) + { + item.ProcessDisable(); + var startBlock = Profiler.Start(FullName, nameof(Handle), "Target Detection"); + //see if the shipyard has been deleted + //just disable the yard and skip it, the ShipyardHandler will take care of deleting the item + if (item.YardEntity.Closed || item.YardEntity.MarkedForClose) + { + item.Disable(); + item.ProcessDisable(); + continue; + } + + item.UpdatePowerUse(); + + if (item.Tools.Any(x => !((IMyFunctionalBlock)x).Enabled)) + { + item.Disable(); + item.ProcessDisable(); + continue; + } + + //look for grids inside the shipyard + if (item.YardType == ShipyardType.Disabled) + { + var allEntities = new HashSet(); + var grids = new HashSet(); + + MyAPIGateway.Entities.GetEntities(allEntities); + if (allEntities.Count == 0) + { + //this can happen when we pull the entitiy list outside of the game thread + Logging.Instance.WriteLine("Failed to get entity list in ShipyardAction"); + continue; + } + + //do a fast check to get entities near the shipyard + //we check against the OBB later, but it takes more time than this + double centerDist = item.ShipyardBox.HalfExtent.LengthSquared(); + + foreach (IMyEntity entity in allEntities) + { + if (!(entity is IMyCubeGrid) || entity.EntityId == item.EntityId) + continue; + + if (entity.Closed || entity.MarkedForClose) + continue; + + //use the center of the ship's bounding box. GetPosition() returns the pivot point, which can be far away from grid center + if (Vector3D.DistanceSquared(entity.Center(), item.ShipyardBox.Center) <= centerDist) + grids.Add(entity as IMyCubeGrid); + } + + if (grids.Count == 0) + continue; + + foreach (IMyCubeGrid grid in grids) + { + //create a bounding box around the ship + MyOrientedBoundingBoxD gridBox = MathUtility.CreateOrientedBoundingBox(grid); + + //check if the ship is completely inside the shipyard + if (item.ShipyardBox.Contains(ref gridBox) == ContainmentType.Contains) + item.ContainsGrids.Add(grid); + else + //in case it was previously inside, but moved + item.ContainsGrids.Remove(grid); + } + + continue; + } + + var toRemove = new List(); + + foreach (IMyCubeGrid yardGrid in item.YardGrids) + { + if (yardGrid.Closed || yardGrid.MarkedForClose) + { + toRemove.Add(yardGrid); + continue; + } + + //check if the target has left the shipyard + MyOrientedBoundingBoxD gridBox = MathUtility.CreateOrientedBoundingBox(yardGrid); + if (item.ShipyardBox.Contains(ref gridBox) != ContainmentType.Contains) + { + Logging.Instance.WriteLine("Target grid left yard"); + toRemove.Add(yardGrid); + } + } + + foreach (IMyCubeGrid removeGrid in toRemove) + { + item.YardGrids.Remove(removeGrid); + ((MyCubeGrid)removeGrid).OnGridSplit -= item.OnGridSplit; + var blocks = new List(); + removeGrid.GetBlocks(blocks); + var targetsToRemove = new HashSet(); + foreach (BlockTarget target in item.TargetBlocks.Where(x => blocks.Contains(x.Block))) + { + targetsToRemove.Add(target); + + foreach (KeyValuePair> entry in item.ProxDict) + entry.Value.Remove(target); + foreach (KeyValuePair procEntry in item.BlocksToProcess) + { + for (int i = 0; i < procEntry.Value.Length; i++) + { + if (procEntry.Value[i] == target) + { + procEntry.Value[i] = null; + Communication.ClearLine(procEntry.Key, i); + } + } + } + } + foreach (BlockTarget remove in targetsToRemove) + item.TargetBlocks.Remove(remove); + } + + if (item.YardGrids.Count == 0 || item.YardGrids.All(g => g.Physics == null && g.Projector() != null)) + { + Logging.Instance.WriteLine("Disabling shipyard; no more targets"); + //clear out and disable the shipyard + item.Disable(); + } + + startBlock.End(); + if (item.YardType == ShipyardType.Grind) + { + var grindBlock = Profiler.Start(FullName, "StepGrind"); + StepGrind(item); + grindBlock.End(); + } + else if (item.YardType == ShipyardType.Weld) + { + var weldBlock = Profiler.Start(FullName, "StepWeld"); + if (!StepWeld(item)) + item.Disable(); + weldBlock.End(); + } + + item.UpdatePowerUse(); + item.ProcessDisable(); + } + } + + private void StepGrind(ShipyardItem shipyardItem) + { + try + { + Logging.Instance.WriteDebug($"[StepGrind] Starting grind cycle for yard {shipyardItem.EntityId}"); + var targetsToRedraw = new HashSet(); + //we need to multiply this by MyShipGrinderConstants.GRINDER_AMOUNT_PER_SECOND / 2... which evaluates to 1 + float grindAmount = MyAPIGateway.Session.GrinderSpeedMultiplier * shipyardItem.Settings.GrindMultiplier; + + if (shipyardItem.TargetBlocks.Count == 0) + { + var targetblock = Profiler.Start(FullName, nameof(StepGrind), "Populate target blocks"); + var blocks = new List(); + Logging.Instance.WriteLine(shipyardItem.YardGrids.Count.ToString()); + foreach (IMyCubeGrid yardGrid in shipyardItem.YardGrids.Where(g => g.Physics != null)) //don't process projections + { + if (yardGrid.Closed || yardGrid.MarkedForClose) + { + Logging.Instance.WriteDebug($"[StepGrind] Skipping closed grid {yardGrid.EntityId}"); + continue; + } + + var tmpBlocks = new List(); + yardGrid.GetBlocks(tmpBlocks); + blocks.AddRange(tmpBlocks); + } + + foreach (IMySlimBlock tmpBlock in blocks.Where(b => b != null && !b.IsFullyDismounted)) + { + shipyardItem.TargetBlocks.Add(new BlockTarget(tmpBlock, shipyardItem)); + } + targetblock.End(); + } + + List allBlocks = shipyardItem.TargetBlocks.ToList(); + if (allBlocks.Count == 0) + return; + + if (!shipyardItem.ProxDict.Any()) + { + //sort blocks by distance to each tool + foreach (IMyCubeBlock tool in shipyardItem.Tools) + { + var targetSortBlock = Profiler.Start(FullName, nameof(StepGrind), "Sort Targets"); + List sortTargets = allBlocks.ToList(); + sortTargets.Sort((a, b) => a.ToolDist[tool.EntityId].CompareTo(b.ToolDist[tool.EntityId])); + shipyardItem.ProxDict[tool.EntityId] = sortTargets; + targetSortBlock.End(); + } + } + + var targetFindBlock = Profiler.Start(FullName, nameof(StepGrind), "Find Targets"); + foreach (IMyCubeBlock tool in shipyardItem.Tools) + { + if (tool?.GetInventory() == null || tool.Closed || tool.MarkedForClose) + { + //this is bad + shipyardItem.Disable(); + return; + } + + BlockTarget[] blockArray = shipyardItem.BlocksToProcess[tool.EntityId]; + + //find the next target for each grinder, if it needs one + for (int i = 0; i < shipyardItem.Settings.BeamCount; i++) + { + var toRemove = new HashSet(); + if (blockArray[i] != null) + continue; + + BlockTarget nextTarget = null; + + for (int b = 0; b < shipyardItem.ProxDict[tool.EntityId].Count; b++) + { + nextTarget = shipyardItem.ProxDict[tool.EntityId][b]; + + if (nextTarget?.CubeGrid == null || nextTarget.CubeGrid.Closed || nextTarget.CubeGrid.MarkedForClose) + continue; + + //one grinder per block, please + bool found = false; + foreach (KeyValuePair entry in shipyardItem.BlocksToProcess) + { + if (entry.Value.Contains(nextTarget)) + { + found = true; + break; + } + } + + if (found) + { + toRemove.Add(nextTarget); + continue; + } + + targetsToRedraw.Add(nextTarget); + break; + } + + foreach (BlockTarget removeTarget in toRemove) + { + shipyardItem.ProxDict[tool.EntityId].Remove(removeTarget); + shipyardItem.TargetBlocks.Remove(removeTarget); + } + + //we found a block to pair with our grinder, add it to the dictionary and carry on with destruction + if (nextTarget != null) + { + shipyardItem.BlocksToProcess[tool.EntityId][i] = nextTarget; + } + } + } + targetFindBlock.End(); + + shipyardItem.UpdatePowerUse(); + var grindActionBlock = Profiler.Start(FullName, nameof(StepGrind), "Grind action"); + var removeTargets = new List(); + + //do the grinding + Utilities.InvokeBlocking(() => + { + try + { + foreach (IMyCubeBlock tool in shipyardItem.Tools) + { + for (int b = 0; b < shipyardItem.BlocksToProcess[tool.EntityId].Length; b++) + { + BlockTarget target = shipyardItem.BlocksToProcess[tool.EntityId][b]; + if (target?.Block == null) + continue; + + if (target.CubeGrid.Closed || target.CubeGrid.MarkedForClose) + { + Logging.Instance.WriteDebug("Error in grind action: Target closed"); + removeTargets.Add(target); + continue; + } + + if (targetsToRedraw.Contains(target)) + { + var toolLine = new Communication.ToolLineStruct + { + ToolId = tool.EntityId, + GridId = target.CubeGrid.EntityId, + BlockPos = target.GridPosition, + PackedColor = Color.OrangeRed.PackedValue, + Pulse = false, + EmitterIndex = (byte)b + }; + + Communication.SendLine(toolLine, shipyardItem.ShipyardBox.Center); + } + + /* + * Grinding laser "efficiency" is a float between 0-1 where: + * 0.0 => 0% of components recovered + * 1.0 => 100% of components recovered + * + * Efficiency decays exponentially as distance to the target (length of the "laser") increases + * 0m => 1.0000 + * 10m => 0.9995 + * 25m => 0.9969 + * 50m => 0.9875 + * 100m => 0.9500 + * 150m => 0.8875 + * 250m => 0.6875 + * 400m => 0.2000 + * inf => 0.1000 + * We impose a minimum efficiency of 0.1 (10%), which happens at distances > ~450m + */ + + // edited, same thing as welders + double efficiency = 1; + if (efficiency < 0.1) + efficiency = 0.1; + + if (!shipyardItem.YardGrids.Contains(target.CubeGrid)) + { + //we missed this grid or its split at some point, so add it to the list and register the split event + shipyardItem.YardGrids.Add(target.CubeGrid); + ((MyCubeGrid)target.CubeGrid).OnGridSplit += shipyardItem.OnGridSplit; + } + + MyInventory grinderInventory = ((MyEntity)tool).GetInventory(); + if (grinderInventory == null) + continue; + + if (!target.Block.IsFullyDismounted) + { + var decreaseBlock = Profiler.Start(FullName, nameof(StepGrind), "DecreaseMountLevel"); + + // Add safety check before grinding + if (target.Block.Integrity > 0 && target.Block.BuildPercent() > 0) + { + try + { + target.Block.DecreaseMountLevel(grindAmount, grinderInventory); + } + catch + { + continue; + } + } + + decreaseBlock.End(); + + var inventoryBlock = Profiler.Start(FullName, nameof(StepGrind), "Grind Inventory"); + // First move everything into _tmpInventory + target.Block.MoveItemsFromConstructionStockpile(_tmpInventory); + + // Then move items into grinder inventory, factoring in our efficiency ratio + foreach (MyPhysicalInventoryItem item in _tmpInventory.GetItems()) + { + grinderInventory.Add(item, (MyFixedPoint)Math.Round((double)item.Amount * efficiency)); + } + + // Then clear out everything left in _tmpInventory + _tmpInventory.Clear(); + inventoryBlock.End(); + } + + // This isn't an clause because target.Block may have become FullyDismounted above, + // in which case we need to run both code blocks + if (target.Block.IsFullyDismounted) + { + var dismountBlock = Profiler.Start(FullName, nameof(StepGrind), "FullyDismounted"); + var tmpItemList = new List(); + var blockEntity = target.Block.FatBlock as MyEntity; + if (blockEntity != null && blockEntity.HasInventory) + { + var dismountInventory = Profiler.Start(FullName, nameof(StepGrind), "DismountInventory"); + for (int i = 0; i < blockEntity.InventoryCount; ++i) + { + MyInventory blockInventory = blockEntity.GetInventory(i); + if (blockInventory == null || blockInventory.Empty()) + continue; + + tmpItemList.Clear(); + tmpItemList.AddRange(blockInventory.GetItems()); + + foreach (MyPhysicalInventoryItem item in tmpItemList) + { + blockInventory.Remove(item, item.Amount); + grinderInventory.Add(item, (MyFixedPoint)Math.Round((double)item.Amount * efficiency)); + } + } + dismountInventory.End(); + } + target.Block.SpawnConstructionStockpile(); + target.CubeGrid.RazeBlock(target.GridPosition); + removeTargets.Add(target); + shipyardItem.TargetBlocks.Remove(target); + dismountBlock.End(); + } + } + } + } + catch (Exception ex) + { + Logging.Instance.WriteDebug($"[StepGrind] Error in grind process: {ex.Message}"); + } + }); + + foreach (KeyValuePair> entry in shipyardItem.ProxDict) + { + foreach (BlockTarget removeBlock in removeTargets) + entry.Value.Remove(removeBlock); + } + + foreach (BlockTarget removeBlock in removeTargets) + shipyardItem.TargetBlocks.Remove(removeBlock); + + //clear lines for any destroyed blocks and update our target count + foreach (KeyValuePair entry in shipyardItem.BlocksToProcess) + { + for (int i = 0; i < entry.Value.Length; i++) + { + BlockTarget removeBlock = entry.Value[i]; + if (removeTargets.Contains(removeBlock)) + { + Communication.ClearLine(entry.Key, i); + entry.Value[i] = null; + } + } + } + + grindActionBlock.End(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine($"[StepGrind] Unhandled error: {ex}"); + } + } + + private bool StepWeld(ShipyardItem shipyardItem) + { + Logging.Instance.WriteDebug($"[StepWeld] Starting weld cycle for yard {shipyardItem.EntityId}"); + Logging.Instance.WriteDebug($"[StepWeld] Current grid count: {shipyardItem.YardGrids.Count}"); + Logging.Instance.WriteDebug($"[StepWeld] Current target count: {shipyardItem.TargetBlocks.Count}"); + + // Log grid details + foreach (var grid in shipyardItem.YardGrids) + { + Logging.Instance.WriteDebug($"[StepWeld] Grid {grid.EntityId} status:"); + Logging.Instance.WriteDebug($" - Has physics: {grid.Physics != null}"); + Logging.Instance.WriteDebug($" - Closed: {grid.Closed}"); + Logging.Instance.WriteDebug($" - MarkedForClose: {grid.MarkedForClose}"); + Logging.Instance.WriteDebug($" - Is projection: {grid.Projector() != null}"); + } + + var targetsToRemove = new HashSet(); + var targetsToRedraw = new HashSet(); + + float weldAmount = MyAPIGateway.Session.WelderSpeedMultiplier * shipyardItem.Settings.WeldMultiplier; + float boneAmount = weldAmount * .1f; + + Logging.Instance.WriteDebug($"[StepWeld] Calculated weldAmount: {weldAmount}, boneAmount: {boneAmount}."); + + if (shipyardItem.TargetBlocks.Count == 0) + { + Logging.Instance.WriteDebug("[StepWeld] No target blocks, starting scan"); + var sortBlock = Profiler.Start(FullName, nameof(StepWeld), "Sort Targets"); + shipyardItem.TargetBlocks.Clear(); + shipyardItem.ProxDict.Clear(); + + var gridTargets = new Dictionary>(); + + foreach (IMyCubeGrid targetGrid in shipyardItem.YardGrids) + { + if (targetGrid.Closed || targetGrid.MarkedForClose) + { + Logging.Instance.WriteDebug($"[StepWeld] Skipping closed/marked grid {targetGrid.EntityId}"); + continue; + } + + var tmpBlocks = new List(); + targetGrid.GetBlocks(tmpBlocks); + Logging.Instance.WriteDebug($"[StepWeld] Found {tmpBlocks.Count} total blocks in grid {targetGrid.EntityId}"); + + gridTargets.Add(targetGrid.EntityId, new List(tmpBlocks.Count)); + int skippedBlocks = 0; + int addedBlocks = 0; + + foreach (IMySlimBlock block in tmpBlocks.ToArray()) + { + if (block == null) + { + skippedBlocks++; + continue; + } + + if (targetGrid.Physics != null && block.IsFullIntegrity && !block.HasDeformation) + { + skippedBlocks++; + continue; + } + + var target = new BlockTarget(block, shipyardItem); + shipyardItem.TargetBlocks.Add(target); + gridTargets[targetGrid.EntityId].Add(target); + addedBlocks++; + } + + Logging.Instance.WriteDebug($"[StepWeld] Grid {targetGrid.EntityId} processing results:"); + Logging.Instance.WriteDebug($" - Skipped blocks: {skippedBlocks}"); + Logging.Instance.WriteDebug($" - Added targets: {addedBlocks}"); + } + + // Log total processing results + int totalTargets = 0; + foreach (KeyValuePair> entry in gridTargets) + { + totalTargets += entry.Value.Count; + Logging.Instance.WriteDebug($"[StepWeld] Grid {entry.Key} final target count: {entry.Value.Count}"); + } + + // Sort and assign targets to tools + foreach (IMyCubeBlock tool in shipyardItem.Tools) + { + Logging.Instance.WriteDebug($"[StepWeld] Setting up tool {tool.EntityId}"); + shipyardItem.ProxDict.Add(tool.EntityId, new List()); + + shipyardItem.YardGrids.Sort((a, b) => + Vector3D.DistanceSquared(a.Center(), tool.GetPosition()).CompareTo( + Vector3D.DistanceSquared(b.Center(), tool.GetPosition()))); + + foreach (IMyCubeGrid grid in shipyardItem.YardGrids) + { + if (!gridTargets.ContainsKey(grid.EntityId)) + { + Logging.Instance.WriteDebug($"[StepWeld] Warning: Grid {grid.EntityId} missing from gridTargets"); + continue; + } + + List list = gridTargets[grid.EntityId]; + list.Sort((a, b) => a.CenterDist.CompareTo(b.CenterDist)); + shipyardItem.ProxDict[tool.EntityId].AddRange(list); + } + + Logging.Instance.WriteDebug($"[StepWeld] Tool {tool.EntityId} assigned {shipyardItem.ProxDict[tool.EntityId].Count} targets"); + } + + sortBlock.End(); + } + + //nothing to do + if (shipyardItem.TargetBlocks.Count == 0) + { + Logging.Instance.WriteDebug($"[StepWeld] Populated {shipyardItem.TargetBlocks.Count} target blocks."); + return false; + } + + Logging.Instance.WriteDebug("[StepWeld] Beginning welding loop."); + //assign blocks to our welders + foreach (IMyCubeBlock welder in shipyardItem.Tools) + { + Logging.Instance.WriteDebug($"[StepWeld] Processing welder {welder.EntityId}"); + + for (int i = 0; i < shipyardItem.Settings.BeamCount; i++) + { + if (shipyardItem.BlocksToProcess[welder.EntityId][i] != null) + { + Logging.Instance.WriteDebug($"[StepWeld] Beam {i} already has assigned target, skipping"); + continue; + } + + var toRemove = new List(); + BlockTarget nextTarget = null; + + foreach (BlockTarget target in shipyardItem.ProxDict[welder.EntityId]) + { + bool found = false; + foreach (KeyValuePair entry in shipyardItem.BlocksToProcess) + { + if (entry.Value.Contains(target)) + { + found = true; + break; + } + } + + if (found) + { + toRemove.Add(target); + Logging.Instance.WriteDebug($"[StepWeld] Target at {target.GridPosition} already being processed"); + continue; + } + + if (target.Projector != null) + { + BuildCheckResult res = target.Projector.CanBuild(target.Block, false); + Logging.Instance.WriteDebug($"[StepWeld] Projection build check for {target.GridPosition}: {res}"); + + if (res == BuildCheckResult.AlreadyBuilt) + { + target.UpdateAfterBuild(); + } + else if (res != BuildCheckResult.OK) + { + continue; + } + else + { + bool success = false; + Utilities.InvokeBlocking(() => success = BuildTarget(target, shipyardItem, welder)); + if (!success) + { + Logging.Instance.WriteDebug($"[StepWeld] Failed to build projection at {target.GridPosition}"); + continue; + } + target.UpdateAfterBuild(); + } + } + + if (target.Block.IsFullIntegrity && !target.Block.HasDeformation) + { + Logging.Instance.WriteDebug($"[StepWeld] Block at {target.GridPosition} is already complete"); + toRemove.Add(target); + continue; + } + + nextTarget = target; + Logging.Instance.WriteDebug($"[StepWeld] Selected target at {target.GridPosition} for beam {i}"); + break; + } + + if (nextTarget != null) + { + targetsToRedraw.Add(nextTarget); + shipyardItem.BlocksToProcess[welder.EntityId][i] = nextTarget; + } + + foreach (BlockTarget removeTarget in toRemove) + { + shipyardItem.ProxDict[welder.EntityId].Remove(removeTarget); + shipyardItem.TargetBlocks.Remove(removeTarget); + } + } + } + //update lasers + foreach (KeyValuePair entry in shipyardItem.BlocksToProcess) + { + for (int i = 0; i < entry.Value.Length; i++) + { + BlockTarget targetBlock = entry.Value[i]; + + if (targetBlock == null || _stalledTargets.Contains(targetBlock)) + continue; + + if (targetsToRedraw.Contains(targetBlock)) + { + var toolLine = new Communication.ToolLineStruct + { + ToolId = entry.Key, + GridId = targetBlock.CubeGrid.EntityId, + BlockPos = targetBlock.GridPosition, + PackedColor = Color.DarkCyan.PackedValue, + Pulse = false, + EmitterIndex = (byte)i + }; + + Communication.SendLine(toolLine, shipyardItem.ShipyardBox.Center); + } + } + } + + if (shipyardItem.BlocksToProcess.All(e => e.Value.All(t => t == null))) + { + shipyardItem.Disable(true, "No more blocks to weld"); + return false; + } + + if (shipyardItem.TargetBlocks.Count == 0) + { + Logging.Instance.WriteDebug($"[StepWeld] Populated {shipyardItem.TargetBlocks.Count} target blocks."); + shipyardItem.Disable(true, "No blocks require welding"); + return false; + } + + shipyardItem.UpdatePowerUse(); + targetsToRedraw.Clear(); + + Utilities.InvokeBlocking(() => + { + foreach (IMyCubeBlock welder in shipyardItem.Tools) + { + var tool = (IMyCollector)welder; + MyInventory welderInventory = ((MyEntity)tool).GetInventory(); + int i = 0; + foreach (BlockTarget target in shipyardItem.BlocksToProcess[tool.EntityId]) + { + if (target == null) + continue; + + if (target.CubeGrid.Physics == null || target.CubeGrid.Closed || target.CubeGrid.MarkedForClose) + { + targetsToRemove.Add(target); + continue; + } + + // if (MyAPIGateway.Session.CreativeMode) + // { + /* + * Welding laser "efficiency" is a float between 0-1 where: + * 0.0 => 0% of component stock used for construction (100% loss) + * 1.0 => 100% of component stock used for construction (0% loss) + * + * Efficiency decay/distance formula is the same as above for grinder + */ + + // Set efficiency to 1 because wtf ew no, good ideas aren't allowed + //double efficiency = 1 - (target.ToolDist[tool.EntityId] / 200000); + double efficiency = 1; + //if (!shipyardItem.StaticYard) + // efficiency /= 2; + if (efficiency < 0.1) + efficiency = 0.1; + //Logging.Instance.WriteDebug(String.Format("Welder[{0}]block[{1}] distance=[{2:F2}m] efficiency=[{3:F5}]", tool.DisplayNameText, i, Math.Sqrt(target.ToolDist[tool.EntityId]), efficiency)); + /* + * We have to factor in our efficiency ratio before transferring to the block "construction stockpile", + * but that math isn't nearly as easy as it was with the grinder. + * + * For each missing component type, we know how many items are "missing" from the construction model, M + * + * The simplest approach would be to pull M items from the conveyor system (if enabled), then move + * (M*efficiency) items to the "construction stockpile" and vaporize the other (M*(1-efficiency)) items. + * However, this approach would leave the construction stockpile incomplete and require several iterations + * before M items have actually been copied. + * + * A better solution is to pull enough "extra" items from the conveyors that the welder has enough to + * move M items to the construction stockpile even after losses due to distance inefficiency + * + * For example, if the target block is missing M=9 items and we are running at 0.9 (90%) efficiency, + * ideally that means we should pull 10 units, vaporize 1, and still have 9 for construction. However, + * if the conveyor system was only able to supply us with 2 components, we should not continue to blindly + * vaporize 1 unit. + * + * Instead, we have to consult the post-conveyor-pull welder inventory to determine if it has at least + * the required number of components. If it does, we vaporize M*(1-efficiency). Otherwise we only + * vaporize current_count*(1-efficiency) and transfer the rest to the construction stockpile + */ + var missingComponents = new Dictionary(); + target.Block.GetMissingComponents(missingComponents); + + var wasteComponents = new Dictionary(); + foreach (KeyValuePair entry in missingComponents) + { + var componentId = new MyDefinitionId(typeof(MyObjectBuilder_Component), entry.Key); + int missing = entry.Value; + MyFixedPoint totalRequired = (MyFixedPoint)(missing / efficiency); + + MyFixedPoint currentStock = welderInventory.GetItemAmount(componentId); + + //Logging.Instance.WriteDebug(String.Format("Welder[{0}]block[{1}] Component[{2}] missing[{3:F3}] inefficiency requires[{4:F3}] in_stock[{5:F3}]", tool.DisplayNameText, i, entry.Key, missing, totalRequired, currentStock)); + if (currentStock < totalRequired && tool.UseConveyorSystem) + { + // Welder doesn't have totalRequired, so try to pull the difference from conveyors + welderInventory.PullAny(shipyardItem.ConnectedCargo, entry.Key, (int)Math.Ceiling((double)(totalRequired - currentStock))); + currentStock = welderInventory.GetItemAmount(componentId); + //Logging.Instance.WriteDebug(String.Format("Welder[{0}]block[{1}] Component[{2}] - after conveyor pull - in_stock[{3:F3}]", tool.DisplayNameText, i, entry.Key, currentStock)); + } + + // Now compute the number of components to delete + // MoveItemsToConstructionPile() below won't move anything if we have less than 1 unit, + // so don't bother "losing" anything due to ineffeciency + if (currentStock >= 1) + { + // The lesser of (missing, currentStock), times (1 minus) our efficiency fraction + MyFixedPoint toDelete = MyFixedPoint.Min(MyFixedPoint.Floor(currentStock), missing) * (MyFixedPoint)(1 - efficiency); + //Logging.Instance.WriteDebug(String.Format("Welder[{0}]block[{1}] Component[{2}] amount lost due to distance [{3:F3}]", tool.DisplayNameText, i, entry.Key, toDelete)); + welderInventory.RemoveItemsOfType(toDelete, componentId); + } + } + + target.Block.MoveItemsToConstructionStockpile(welderInventory); + + missingComponents.Clear(); + target.Block.GetMissingComponents(missingComponents); + + if (missingComponents.Any() && !target.Block.HasDeformation) + { + if (_stalledTargets.Add(target)) + targetsToRedraw.Add(target); + } + else + { + if (_stalledTargets.Remove(target)) + targetsToRedraw.Add(target); + } + + target.Block.IncreaseMountLevel(weldAmount, 0, welderInventory, boneAmount, true); + if (target.Block.IsFullIntegrity && !target.Block.HasDeformation) + targetsToRemove.Add(target); + // } + i++; + } + } + }); + + shipyardItem.MissingComponentsDict.Clear(); + + foreach (KeyValuePair entry in shipyardItem.BlocksToProcess) + { + for (int i = 0; i < entry.Value.Length; i++) + { + BlockTarget target = entry.Value[i]; + + if (target == null) + continue; + + if (targetsToRemove.Contains(target)) + { + Communication.ClearLine(entry.Key, i); + _stalledTargets.Remove(target); + shipyardItem.TargetBlocks.Remove(target); + entry.Value[i] = null; + continue; + } + + if (_stalledTargets.Contains(target)) + { + var blockComponents = new Dictionary(); + target.Block.GetMissingComponents(blockComponents); + + foreach (KeyValuePair component in blockComponents) + { + if (shipyardItem.MissingComponentsDict.ContainsKey(component.Key)) + shipyardItem.MissingComponentsDict[component.Key] += component.Value; + else + shipyardItem.MissingComponentsDict.Add(component.Key, component.Value); + } + + if (shipyardItem.MissingComponentsDict.Any()) + { + shipyardItem.Disable(true, "Insufficient components to continue welding"); + return false; + } + + var toolLine = new Communication.ToolLineStruct + { + ToolId = entry.Key, + GridId = target.CubeGrid.EntityId, + BlockPos = target.GridPosition, + PackedColor = Color.Purple.PackedValue, + Pulse = true, + EmitterIndex = (byte)i + }; + + if (targetsToRedraw.Contains(target)) + { + Communication.ClearLine(entry.Key, i); + Communication.SendLine(toolLine, shipyardItem.ShipyardBox.Center); + } + continue; + } + } + } + + return true; + } + + private bool BuildTarget(BlockTarget target, ShipyardItem item, IMyCubeBlock tool) + { + IMyProjector projector = target.Projector; + IMySlimBlock block = target.Block; + + if (projector == null || block == null) + return false; + + // Check if the projector is allowed to build the block + if (projector.CanBuild(block, false) != BuildCheckResult.OK) + return false; + + // If in Creative Mode, build the block immediately + if (MyAPIGateway.Session.CreativeMode) + { + try + { + projector.Build(block, projector.OwnerId, projector.EntityId, false, projector.OwnerId); + } + catch (NullReferenceException ex) + { + // Log and ignore the exception to prevent a crash + Logging.Instance.WriteDebug($"Build failed due to missing DLC reference: {ex.Message}"); + return false; + } + return projector.CanBuild(block, true) != BuildCheckResult.OK; + } + + // Attempt to pull required components for building in survival mode + var blockDefinition = block.BlockDefinition as MyCubeBlockDefinition; + string componentName = blockDefinition.Components[0].Definition.Id.SubtypeName; + if (_tmpInventory.PullAny(item.ConnectedCargo, componentName, 1)) + { + _tmpInventory.Clear(); + try + { + projector.Build(block, projector.OwnerId, projector.EntityId, false, projector.OwnerId); + } + catch (NullReferenceException ex) + { + // Log and ignore the exception to prevent a crash + Logging.Instance.WriteDebug($"Build failed due to missing DLC reference: {ex.Message}"); + return false; + } + return projector.CanBuild(block, true) != BuildCheckResult.OK; + } + + return false; // Return false if the block cannot be built + } + + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessShipyardDetection.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessShipyardDetection.cs new file mode 100644 index 0000000..b25af21 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ProcessShipyardDetection.cs @@ -0,0 +1,363 @@ +using System.Collections.Generic; +using System.Linq; +using Sandbox.Game.Entities; +using Sandbox.ModAPI; +using ShipyardMod.ItemClasses; +using ShipyardMod.Settings; +using ShipyardMod.Utility; +using VRage.Game.Entity; +using VRage.Game.ModAPI; +using VRage.ModAPI; +using VRageMath; + +namespace ShipyardMod.ProcessHandlers +{ + public class ProcessShipyardDetection : ProcessHandlerBase + { + public static HashSet ShipyardsList = new HashSet(); + private readonly List _corners = new List(); + private readonly string FullName = typeof(ProcessShipyardDetection).FullName; + + public override int GetUpdateResolution() + { + return 5000; + } + + public override bool ServerOnly() + { + return true; + } + + public override void Handle() + { + var tmpEntities = new HashSet(); + MyAPIGateway.Entities.GetEntities(tmpEntities); + if (tmpEntities.Count == 0) + { + Logging.Instance.WriteLine("Failed to get list of entities in ShipyardDetection."); + return; + } + + //copy the list of entities because concurrency + IMyEntity[] entities = tmpEntities.ToArray(); + + //run through our current list of shipyards and make sure they're still valid + var itemsToRemove = new HashSet(); + var firstCheckBlock = Profiler.Start(FullName, nameof(Handle), "First Check"); + foreach (ShipyardItem item in ShipyardsList) + { + if (!AreToolsConnected(item.Tools)) + { + Logging.Instance.WriteLine("remove item tools " + item.Tools.Length); + item.Disable(); + itemsToRemove.Add(item); + foreach(var tool in item.Tools) + Communication.SendCustomInfo(tool.EntityId, "Invalid Shipyard: All tools must be on the same conveyor network!"); + continue; + } + + if (item.Tools.Any(x => x.Closed || x.MarkedForClose)) + { + Logging.Instance.WriteLine("remove item closed tools " + item.Tools.Length); + item.Disable(); + itemsToRemove.Add(item); + continue; + } + + if (!entities.Contains(item.YardEntity) || item.YardEntity.Closed || item.YardEntity.MarkedForClose) + { + Logging.Instance.WriteLine("remove item entity"); + item.Disable(); + itemsToRemove.Add(item); + continue; + } + + using (Profiler.Start(FullName, nameof(Handle), "Physics Check")) + { + if (item.YardEntity.Physics == null + || item.StaticYard && (!item.YardEntity.Physics.IsStatic) ) + { + Logging.Instance.WriteLine("remove item physics"); + itemsToRemove.Add(item); + item.Disable(); + foreach (var tool in item.Tools) + Communication.SendCustomInfo(tool.EntityId, "Invalid Shipyard: Shipyard must be Static!"); + continue; + } + } + + if (item.Tools.Any(t => ((IMyTerminalBlock)t).CustomInfo.Contains("Invalid Shipyard"))) + { + foreach (var tool in item.Tools) + Communication.SendCustomInfo(tool.EntityId, string.Empty); + } + } + firstCheckBlock.End(); + + foreach (ShipyardItem item in itemsToRemove) + { + item.YardType = ShipyardType.Invalid; + Communication.SendYardState(item); + ShipyardsList.Remove(item); + } + + foreach (IMyEntity entity in entities) + { + _corners.Clear(); + var grid = entity as IMyCubeGrid; + + if (grid?.Physics == null || grid.Closed || grid.MarkedForClose ) + continue; + + if (ShipyardsList.Any(x => x.EntityId == entity.EntityId)) + continue; + + var gridBlocks = new List(); + grid.GetBlocks(gridBlocks); + + foreach (IMySlimBlock slimBlock in gridBlocks) + { + var collector = slimBlock.FatBlock as IMyCollector; + if (collector == null) + continue; + + if (collector.BlockDefinition.SubtypeId.StartsWith("ShipyardCorner")) + { + _corners.Add(slimBlock.FatBlock); + } + } + + if (_corners.Count != 8) + { + foreach (var tool in _corners) + Communication.SendCustomInfo(tool.EntityId, $"Invalid Shipyard: Must be 8 corner blocks, there are {_corners.Count} on this grid!"); + continue; + } + + if (_corners.Any(c => c.BlockDefinition.SubtypeId != _corners[0].BlockDefinition.SubtypeId)) + { + foreach (var tool in _corners) + Communication.SendCustomInfo(tool.EntityId, $"Invalid Shipyard: All 8 corner blocks must be the same type!"); + continue; + } + + using (Profiler.Start(FullName, nameof(Handle), "Static Check")) + { + if (_corners[0].BlockDefinition.SubtypeId == "ShipyardCorner_Large" && !ShipyardCore.Debug) + { + if (!grid.IsStatic) + { + Logging.Instance.WriteDebug($"Yard {grid.EntityId} failed: Static check"); + foreach (var tool in _corners) + Communication.SendCustomInfo(tool.EntityId, "Invalid Shipyard: Shipyard must be static!"); + continue; + } + } + } + + if (!IsYardValid(entity, _corners)) + continue; + + //add an offset of 2.5m because the corner points are at the center of a 3^3 block, and the yard will be 2.5m short in all dimensions + MyOrientedBoundingBoxD testBox = MathUtility.CreateOrientedBoundingBox((IMyCubeGrid)entity, _corners.Select(x => x.GetPosition()).ToList(), 2.5); + + Logging.Instance.WriteLine("Found yard"); + var item = new ShipyardItem( + testBox, + _corners.ToArray(), + ShipyardType.Disabled, + entity); + item.Settings = ShipyardSettings.Instance.GetYardSettings(item.EntityId); + foreach (IMyCubeBlock tool in _corners) + item.BlocksToProcess.Add(tool.EntityId, new BlockTarget[3]); + + ShipyardsList.Add(item); + Communication.SendNewYard(item); + foreach (var tool in item.Tools) + Communication.SendCustomInfo(tool.EntityId, ""); + } + + Communication.SendYardCount(); + } + + /// + /// This makes sure all the tools are connected to the same conveyor system + /// + /// + /// + private bool AreToolsConnected(IReadOnlyList tools) + { + bool found = true; + + if (tools.Any(x => x.Closed || x.MarkedForClose)) + { + Logging.Instance.WriteDebug("tools closed?"); + return false; + } + + Utilities.InvokeBlocking(() => + { + using (Profiler.Start(FullName, nameof(AreToolsConnected))) + { + IMyInventory toolInventory = ((MyEntity)tools[0]).GetInventory(); + + if (toolInventory == null) + { + Logging.Instance.WriteDebug("null toolInventory"); + return; + } + + for (int i = 1; i < tools.Count; ++i) + { + IMyInventory compareInventory = ((MyEntity)tools[i]).GetInventory(); + + if (compareInventory == null) + { + Logging.Instance.WriteDebug($"Null inventory at {i}"); + found = false; + return; + } + + if (!toolInventory.IsConnectedTo(compareInventory)) + { + Logging.Instance.WriteDebug($"Tool not connected at {i}"); + found = false; + return; + } + } + } + }); + + return found; + } + + // OBB corner structure + // ZMax ZMin + // 0----1 4----5 + // | | | | + // | | | | + // 3----2 7----6 + + /// + /// Makes sure the shipyard has a complete frame made of shipyard conveyor blocks + /// + /// + /// + private bool IsFrameComplete(ShipyardItem item) + { + using (Profiler.Start(FullName, nameof(IsFrameComplete))) + { + var corners = new Vector3D[8]; + item.ShipyardBox.GetCorners(corners, 0); + + var gridCorners = new Vector3I[8]; + for (int i = 0; i < 8; i++) + gridCorners[i] = ((IMyCubeGrid)item.YardEntity).WorldToGridInteger(corners[i]); + + LinePair[] lines = + { + new LinePair(0, 1), + new LinePair(0, 4), + new LinePair(1, 2), + new LinePair(1, 5), + new LinePair(2, 3), + new LinePair(2, 6), + new LinePair(3, 0), + new LinePair(3, 7), + new LinePair(4, 5), + new LinePair(5, 6), + new LinePair(6, 7), + new LinePair(7, 4), + }; + + var grid = (IMyCubeGrid)item.YardEntity; + foreach (LinePair line in lines) + { + var it = new MathUtility.Vector3ILineIterator(gridCorners[line.Start], gridCorners[line.End]); + while (it.IsValid()) + { + IMySlimBlock block = grid.GetCubeBlock(it.Current); + it.MoveNext(); + if (block == null) + return false; + + if (!block.BlockDefinition.Id.SubtypeName.Contains("Shipyard")) + return false; + + if (block.BuildPercent() < .8) + return false; + } + } + + return true; + } + } + + /// + /// Checks if tools are on the same cargo system and are arranged orthogonally. + /// + /// + /// + /// + private bool IsYardValid(IMyEntity entity, List tools) + { + using (Profiler.Start(FullName, nameof(IsYardValid))) + { + var gridPoints = new List(); + foreach (IMyCubeBlock tool in tools) + { + Vector3D point = tool.PositionComp.GetPosition(); + Vector3I adjustedPoint = ((IMyCubeGrid)entity).WorldToGridInteger(point); + gridPoints.Add(adjustedPoint); + } + + if (!MathUtility.ArePointsOrthogonal(gridPoints)) + { + Logging.Instance.WriteDebug($"Yard {entity.EntityId} failed: APO"); + foreach (var tool in tools) + Communication.SendCustomInfo(tool.EntityId, "Invalid Shipyard: Corners not aligned!"); + return false; + } + + // Check if any two corner points are more than 1km apart + const double maxDistanceSquared = 1000 * 1000; // 1km squared + for (int i = 0; i < tools.Count - 1; i++) + { + for (int j = i + 1; j < tools.Count; j++) + { + double distanceSquared = Vector3D.DistanceSquared(tools[i].GetPosition(), tools[j].GetPosition()); + if (distanceSquared > maxDistanceSquared) + { + Logging.Instance.WriteDebug($"Yard {entity.EntityId} failed: Corner distance exceeds 1km"); + foreach (var tool in tools) + Communication.SendCustomInfo(tool.EntityId, "Invalid Shipyard: Corner distance exceeds 1km!"); + return false; + } + } + } + + if (!AreToolsConnected(tools)) + { + Logging.Instance.WriteDebug($"Yard {entity.EntityId} failed: ATC"); + foreach (var tool in tools) + Communication.SendCustomInfo(tool.EntityId, "Invalid Shipyard: All tools must be on the same conveyor network!"); + return false; + } + + return true; + } + } + + private struct LinePair + { + public LinePair(int start, int end) + { + Start = start; + End = end; + } + + public readonly int Start; + public readonly int End; + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/Profiler.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/Profiler.cs new file mode 100644 index 0000000..f23c968 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/Profiler.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml.Serialization; +using Sandbox.ModAPI; + +namespace ShipyardMod.Utility +{ + public static class Profiler + { + [Serializable] + public class Namespace + { + public List Classes; + public string Name; + + public Namespace() + { + } + + public Namespace(string name) + { + Name = name; + Classes = new List(); + } + } + + [Serializable] + public class Class + { + public double AvgRuntime; + public double MaxRuntime; + public List Members; + public string Name; + [XmlIgnore] + public List Runtimes; + + public Class() + { + } + + public Class(string name) + { + Name = name; + Members = new List(); + Runtimes = new List(); + } + } + + [Serializable] + public class Member + { + public double AvgRuntime; + public List Blocks; + public double MaxRuntime; + public string Name; + [XmlIgnore] + public List Runtimes; + + public Member() + { + } + + public Member(string name) + { + Name = name; + Blocks = new List(); + Runtimes = new List(); + } + } + + [Serializable] + public class Block + { + public double AvgRuntime; + public double MaxRuntime; + public string Name; + [XmlIgnore] + public List Runtimes; + + public Block() + { + } + + public Block(string name) + { + Name = name; + Runtimes = new List(); + } + } + + public static ProfilingBlockBase EmptyBlock = new ProfilingBlockBase(); + + public class ProfilingBlockBase : IDisposable + { + public virtual void End() { } + + public void Dispose() + { + End(); + } + } + + public class ProfilingBlock : ProfilingBlockBase + { + public readonly string Block; + public readonly string Class; + public readonly string Member; + + public readonly string Namespace; + public readonly Stopwatch Stopwatch; + private bool _stopped; + + public ProfilingBlock(string namespaceName, string className, string memberName = null, string blockName = null) + { + Namespace = namespaceName; + Class = className; + Member = memberName; + Block = blockName; + Stopwatch = new Stopwatch(); + } + + public override void End() + { + if (_stopped) + return; + + _stopped = true; + + Profiler.End(this); + } + } + + private static readonly List Namespaces = new List(); + + public static ProfilingBlockBase Start(string className, string memberName = null, string blockName = null) + { + if(!ShipyardCore.Debug) + return EmptyBlock; + + string[] splits = className.Split('.'); + + var profileblock = new ProfilingBlock(splits[1], splits[2], memberName, blockName); + profileblock.Stopwatch.Start(); + return profileblock; + } + + private static void End(ProfilingBlock profilingBlock) + { + profilingBlock.Stopwatch.Stop(); + + Utilities.QueueAction(() => + { + try + { + double runtime = 1000d * profilingBlock.Stopwatch.ElapsedTicks / Stopwatch.Frequency; + + Namespace thisNamespace = Namespaces.FirstOrDefault(n => n.Name == profilingBlock.Namespace); + if (thisNamespace == null) + { + thisNamespace = new Namespace(profilingBlock.Namespace); + Namespaces.Add(thisNamespace); + } + Class thisClass = thisNamespace.Classes.FirstOrDefault(c => c.Name == profilingBlock.Class); + if (thisClass == null) + { + thisClass = new Class(profilingBlock.Class); + thisNamespace.Classes.Add(thisClass); + } + + if (profilingBlock.Member == null) + { + if (thisClass.Runtimes.Count >= int.MaxValue) + thisClass.Runtimes.RemoveAt(0); + thisClass.Runtimes.Add(runtime); + thisClass.MaxRuntime = thisClass.Runtimes.Max(); + thisClass.AvgRuntime = thisClass.Runtimes.Average(); + return; + } + + Member thisMember = thisClass.Members.FirstOrDefault(m => m.Name == profilingBlock.Member); + if (thisMember == null) + { + thisMember = new Member(profilingBlock.Member); + thisClass.Members.Add(thisMember); + } + + if (profilingBlock.Block == null) + { + if (thisMember.Runtimes.Count >= int.MaxValue) + thisMember.Runtimes.RemoveAt(0); + thisMember.Runtimes.Add(runtime); + thisMember.MaxRuntime = thisMember.Runtimes.Max(); + thisMember.AvgRuntime = thisMember.Runtimes.Average(); + return; + } + + Block thisBlock = thisMember.Blocks.FirstOrDefault(b => b.Name == profilingBlock.Block); + if (thisBlock == null) + { + thisBlock = new Block(profilingBlock.Block); + thisMember.Blocks.Add(thisBlock); + } + + if (thisBlock.Runtimes.Count >= int.MaxValue) + thisBlock.Runtimes.RemoveAt(0); + thisBlock.Runtimes.Add(runtime); + thisBlock.MaxRuntime = thisBlock.Runtimes.Max(); + thisBlock.AvgRuntime = thisBlock.Runtimes.Average(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine(ex.ToString()); + } + }); + } + + private static bool _saving; + + public static void Save() + { + if (_saving) + return; + _saving = true; + MyAPIGateway.Parallel.Start(() => + { + TextWriter writer = MyAPIGateway.Utilities.WriteFileInLocalStorage("profiler.xml", typeof(Profiler)); + + writer.Write(MyAPIGateway.Utilities.SerializeToXML(Namespaces)); + writer.Flush(); + + writer.Close(); + _saving = false; + }); + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ScanAnimation.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ScanAnimation.cs new file mode 100644 index 0000000..1c8a6a7 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ScanAnimation.cs @@ -0,0 +1,134 @@ +using ShipyardMod.Utility; +using VRage.Game; +using VRage.Utils; +using VRageMath; + +namespace ShipyardMod.ItemClasses +{ + public class ScanAnimation + { + //each sweep takes 3 seconds (times 60 updates) + //Lerp takes a percentage, to just take the reciprocal of 3*60 + private const double MULTIPLIER = 1d / 180d; + private static readonly MyStringId TextureId = MyStringId.GetOrCompute("ScanTexture"); + private readonly Vector4 _color = Color.Green.ToVector4(); + private readonly Vector3D[] _endpoints = new Vector3D[4]; + + private readonly ShipyardItem _shipyardItem; + private bool _init; + private ScanLine _line; + private bool _scanningZ = true; + private int _ticks; + + public ScanAnimation(ShipyardItem item) + { + _shipyardItem = item; + } + + // OBB corner structure + // ZMax ZMin + // 0----1 4----5 + // | | | | + // | | | | + // 3----2 7----6 + + private void Init() + { + var corners = new Vector3D[8]; + _shipyardItem.ShipyardBox.GetCorners(corners, 0); + + //our endpoints are in the center of the faces + //z plane + _endpoints[0] = (corners[0] + corners[1] + corners[2] + corners[3]) / 4; + _endpoints[1] = (corners[4] + corners[5] + corners[6] + corners[7]) / 4; + // x plane + _endpoints[2] = (corners[2] + corners[3] + corners[6] + corners[7]) / 4; + _endpoints[3] = (corners[0] + corners[1] + corners[4] + corners[5]) / 4; + + /* + * Scanning Z moves from [0] to [4] + * Scanning X moves from [0] to [3] + */ + + //start by scanning the line on the z plane, from zmax to zmin + _line = new ScanLine + { + Origin = _endpoints[0], + //get half the dimensions for each face + ZWidth = (float)_shipyardItem.ShipyardBox.HalfExtent.X, + ZLength = (float)_shipyardItem.ShipyardBox.HalfExtent.Y, + XWidth = (float)_shipyardItem.ShipyardBox.HalfExtent.X, + XLength = (float)_shipyardItem.ShipyardBox.HalfExtent.Z, + //we need the up and left vectors to align the billboard to the shipyard grid + ZLeft = _shipyardItem.ShipyardBox.Orientation.Right, + ZUp = -_shipyardItem.ShipyardBox.Orientation.Up, + XLeft = -_shipyardItem.ShipyardBox.Orientation.Right, + XUp = _shipyardItem.ShipyardBox.Orientation.Forward + }; + } + + public bool Draw() + { + if (!Update()) + return false; + + //draw the texture oriented to the shipyard grid + if (_scanningZ) + MyTransparentGeometry.AddBillboardOriented(TextureId, _color, _line.Origin, _line.ZLeft, _line.ZUp, _line.ZWidth, _line.ZLength); + else + MyTransparentGeometry.AddBillboardOriented(TextureId, _color, _line.Origin, _line.XLeft, _line.XUp, _line.XWidth, _line.XLength); + + return true; + } + + private bool Update() + { + if (!_init) + { + _init = true; + Init(); + } + + if (_scanningZ) + { + //calculate the next position + _line.Origin = Vector3D.Lerp(_endpoints[0], _endpoints[1], _ticks * MULTIPLIER); + + //line has reached the end + //flip the flag so we start scanning the x plane + if (_ticks * MULTIPLIER >= 1) + { + _ticks = 0; + _scanningZ = false; + } + } + else + { + _line.Origin = Vector3D.Lerp(_endpoints[2], _endpoints[3], _ticks * MULTIPLIER); + + //line has reached the end + //we're done, so return false to let the caller know to stop drawing + if (_ticks * MULTIPLIER >= 1) + { + return false; + } + } + + _ticks++; + return true; + } + + private class ScanLine + { + public Vector3D Origin; + public Vector3D XLeft; + public float XLength; + public Vector3D XUp; + public float XWidth; + public Vector3D ZLeft; + public float ZLength; + public Vector3D ZUp; + public float ZWidth; + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardCore.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardCore.cs new file mode 100644 index 0000000..9ee0c0b --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardCore.cs @@ -0,0 +1,507 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using ParallelTasks; +using Sandbox.Game.Entities; +using Sandbox.ModAPI; +using ShipyardMod.ItemClasses; +using ShipyardMod.ProcessHandlers; +using ShipyardMod.Settings; +using ShipyardMod.Utility; +using VRage.Collections; +using VRage.Game; +using VRage.Game.Components; +using VRage.Game.ModAPI; +using VRage.Utils; +using VRageMath; +using static VRageRender.MyBillboard; + +/* Hello there! + * + * One of the best things about the SE modding community is sharing information, so + * if you find something in here useful or interesting, feel free to use it in your own mod! + * I just ask that you leave a comment saying something like 'shamelessly stolen from rexxar' :) + * Or consider donating a few dollars at https://paypal.me/rexxar if you're able. + * Or hell, even just leaving a comment on the mod page or the forum or wherever letting me know + * you found an interesting tidbit in here is good enough for me. + * + * This mod never would have happened without the huge amount of help from the other modders + * in the KSH Discord server. Come hang out with some really cool people: https://discord.gg/Dqfhtuu + * + * + * This mod is something I've wanted in the game since I started playing SE. I spent over 8 + * months on it, and I sincerely hope you enjoy it :) + * + * <3 rexxar + */ + +namespace ShipyardMod +{ + [MySessionComponentDescriptor(MyUpdateOrder.BeforeSimulation)] + public class ShipyardCore : MySessionComponentBase + { + private const string Version = "v2.5"; + + //TODO + public static volatile bool Debug = false; + + //private readonly List _tasks = new List(); + private static Task _task; + public static readonly MyConcurrentDictionary BoxDict = new MyConcurrentDictionary(); + + private bool _initialized; + + private DateTime _lastMessageTime = DateTime.Now; + private ProcessHandlerBase[] _processHandlers; + private int _updateCount; + + private void Initialize() + { + AddMessageHandler(); + + _processHandlers = new ProcessHandlerBase[] + { + new ProcessShipyardAction(), + new ProcessLocalYards(), + new ProcessLCDMenu(), + new ProcessShipyardDetection(), + new ProcessConveyorCache(), + }; + + Logging.Instance.WriteLine($"Shipyard Script Initialized: {Version}"); + } + + private void HandleMessageEntered(string messageText, ref bool sendToOthers) + { + string messageLower = messageText.ToLower(); + + if (!messageLower.StartsWith("/shipyard")) + return; + + if (DateTime.Now - _lastMessageTime < TimeSpan.FromMilliseconds(200)) + return; + + if (messageLower.Equals("/shipyard debug on")) + { + Logging.Instance.WriteLine("Debug turned on"); + Debug = true; + } + else if (messageLower.Equals("/shipyard debug off")) + { + Logging.Instance.WriteLine("Debug turned off"); + Debug = false; + } + + _lastMessageTime = DateTime.Now; + + sendToOthers = false; + + byte[] commandBytes = Encoding.UTF8.GetBytes(messageLower); + byte[] idBytes = BitConverter.GetBytes(MyAPIGateway.Session.Player.SteamUserId); + + var message = new byte[commandBytes.Length + sizeof(ulong)]; + + idBytes.CopyTo(message, 0); + commandBytes.CopyTo(message, idBytes.Length); + + Communication.SendMessageToServer(Communication.MessageTypeEnum.ClientChat, message); + } + + private void CalculateBoxesContaining() + { + foreach (ShipyardItem item in ProcessLocalYards.LocalYards) + { + foreach (IMyCubeGrid grid in item.ContainsGrids) + { + if (item.YardType != ShipyardType.Disabled || grid.Closed || !ShipyardSettings.Instance.GetYardSettings(item.EntityId).GuideEnabled) + { + BoxDict.Remove(grid.EntityId); + continue; + } + //if (BoxDict.ContainsKey(grid.EntityId) && Vector3D.DistanceSquared(BoxDict[grid.EntityId].LastPos, grid.GetPosition()) < 0.01) + // continue; + + uint color; + + if (grid.Physics != null) + color = Color.Green.PackedValue; + else + { + var proj = grid.Projector(); + + if (proj == null) //ghost grid like Digi's helmet + continue; + + if (proj.RemainingBlocks == 0) //projection is complete + continue; + + color = Color.Cyan.PackedValue; + } + + BoxDict[grid.EntityId] = new BoxItem + { + Lines = MathUtility.CalculateObbLines(MathUtility.CreateOrientedBoundingBox(grid)), + GridId = grid.EntityId, + //PackedColor = grid.Physics == null ? Color.Cyan.PackedValue : Color.Green.PackedValue, + PackedColor = color, + LastPos = grid.GetPosition() + }; + } + } + } + + private void CalculateBoxesIntersecting() + { + foreach (var item in ProcessLocalYards.LocalYards) + { + foreach (IMyCubeGrid grid in item.IntersectsGrids) + { + if (item.YardType != ShipyardType.Disabled || grid.Closed || !ShipyardSettings.Instance.GetYardSettings(item.EntityId).GuideEnabled) + { + BoxDict.Remove(grid.EntityId); + continue; + } + //if (BoxDict.ContainsKey(grid.EntityId) && Vector3D.DistanceSquared(BoxDict[grid.EntityId].LastPos, grid.GetPosition()) < 0.01) + // continue; + + uint color; + + if (grid.Physics != null) + color = Color.Yellow.PackedValue; + else + { + var proj = grid.Projector(); + + if (proj == null) //ghost grid like Digi's helmet + continue; + + if (proj.RemainingBlocks == 0) //projection is complete + continue; + + color = Color.CornflowerBlue.PackedValue; + } + + BoxDict[grid.EntityId] = new BoxItem + { + Lines = MathUtility.CalculateObbLines(MathUtility.CreateOrientedBoundingBox(grid)), + GridId = grid.EntityId, + //PackedColor = grid.Physics == null ? Color.CornflowerBlue.PackedValue : Color.Yellow.PackedValue, + PackedColor = color, + LastPos = grid.GetPosition() + }; + } + } + } + + private void CalculateLines() + { + foreach (var e in Communication.LineDict) + { + foreach (var line in e.Value) + { + line.Start = MathUtility.CalculateEmitterOffset(line.EmitterBlock, line.Index); + var target = line.TargetGrid.GetCubeBlock(line.TargetBlock); + if (target == null || target.Closed()) + continue; + + line.End = target.GetPosition(); + + if (line.LinePackets != null) + { + line.LinePackets.Update(line.Start, line.End); + } + } + } + } + + private void AddMessageHandler() + { + MyAPIGateway.Utilities.MessageEntered += HandleMessageEntered; + Communication.RegisterHandlers(); + } + + private void RemoveMessageHandler() + { + MyAPIGateway.Utilities.MessageEntered -= HandleMessageEntered; + Communication.UnregisterHandlers(); + } + + public override void Draw() + { + if (MyAPIGateway.Session?.Player == null || !_initialized) + return; + + try + { + //these tasks are too simple to use Parallel.ForEach or similar in the body, but + //can all safely be run simultaneously, so do that. + var t1 = MyAPIGateway.Parallel.Start(CalculateBoxesContaining); + var t2 = MyAPIGateway.Parallel.Start(CalculateBoxesIntersecting); + var t3 = MyAPIGateway.Parallel.Start(CalculateLines); + //wait for all three to finish + t1.Wait(); + t2.Wait(); + t3.Wait(); + DrawLines(); + FadeLines(); + DrawScanning(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine($"Draw(): {ex}"); + MyLog.Default.WriteLineAndConsole("##SHIPYARD MOD: ENCOUNTERED ERROR DURING DRAW UPDATE. CHECK MOD LOG"); + if (Debug) + throw; + } + } + + public override void UpdateBeforeSimulation() + { + try + { + if (MyAPIGateway.Session == null) + return; + + if (!_initialized) + { + _initialized = true; + Initialize(); + } + + RunProcessHandlers(); + + foreach (var item in ProcessShipyardDetection.ShipyardsList) + { + if (item.StaticYard) + { + foreach (IMyCubeGrid yardGrid in item.YardGrids) + yardGrid.Stop(); + } + else + { + item.UpdatePosition(); + item.NudgeGrids(); + } + } + + foreach (var item in ProcessLocalYards.LocalYards) + { + if (!item.StaticYard) + item.UpdatePosition(); + } + + if (_updateCount++ % 10 != 0) + return; + + CheckAndDamagePlayer(); + Utilities.ProcessActionQueue(); + + if (Debug) + Profiler.Save(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine($"UpdateBeforeSimulation(): {ex}"); + MyLog.Default.WriteLineAndConsole("##SHIPYARD MOD: ENCOUNTERED ERROR DURING MOD UPDATE. CHECK MOD LOG"); + if (Debug) + throw; + } + } + + private void CheckAndDamagePlayer() + { + var character = MyAPIGateway.Session.Player?.Controller?.ControlledEntity?.Entity as IMyCharacter; + + if (character == null) + return; + + var damageBlock = Profiler.Start("0.ShipyardMod.ShipyardCore", nameof(CheckAndDamagePlayer)); + BoundingBoxD charbox = character.WorldAABB; + + MyAPIGateway.Parallel.ForEach(Communication.LineDict.Values.ToArray(), lineList => + { + foreach (LineItem line in lineList) + { + var ray = new Ray(line.Start, line.End - line.Start); + double? intersection = charbox.Intersects(ray); + if (intersection.HasValue) + { + if (Vector3D.DistanceSquared(charbox.Center, line.Start) < Vector3D.DistanceSquared(line.Start, line.End)) + { + Utilities.Invoke(() => character.DoDamage(5, MyStringHash.GetOrCompute("ShipyardLaser"), true)); + } + } + } + }); + damageBlock.End(); + } + + private void RunProcessHandlers() + { + //wait for execution to complete before starting up a new thread + if (!_task.IsComplete) + return; + + //exceptions are suppressed in tasks, so re-throw if one happens + if (_task.Exceptions != null && _task.Exceptions.Length > 0) + { + MyLog.Default.WriteLineAndConsole("##SHIPYARD MOD: THREAD EXCEPTION, CHECK MOD LOG FOR MORE INFO."); + MyLog.Default.WriteLineAndConsole("##SHIPYARD MOD: EXCEPTION: " + _task.Exceptions[0]); + if (Debug) + throw _task.Exceptions[0]; + } + + //run all process handlers in serial so we don't have to design for concurrency + _task = MyAPIGateway.Parallel.Start(() => + { + string handlerName = ""; + try + { + var processBlock = Profiler.Start("0.ShipyardMod.ShipyardCore", nameof(RunProcessHandlers)); + foreach (ProcessHandlerBase handler in _processHandlers) + { + if (handler.CanRun()) + { + handlerName = handler.GetType().Name; + var handlerBlock = Profiler.Start(handler.GetType().FullName); + //Logging.Instance.WriteDebug(handlerName + " start"); + handler.Handle(); + handler.LastUpdate = DateTime.Now; + handlerBlock.End(); + } + } + processBlock.End(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine($"Thread Exception: {handlerName}: {ex}"); + Logging.Instance.Debug_obj("Thread exception! Check the log!"); + throw; + } + }); + } + + private void DrawScanning() + { + var toRemove = new List(); + foreach (ScanAnimation animation in Communication.ScanList) + { + if (!animation.Draw()) + toRemove.Add(animation); + } + + foreach (ScanAnimation removeAnim in toRemove) + Communication.ScanList.Remove(removeAnim); + } + + private void DrawLines() + { + foreach (KeyValuePair> kvp in Communication.LineDict) + { + foreach (LineItem line in kvp.Value) + { + if (Communication.FadeList.Any(x => x.Start == line.Start)) + continue; + + if (line.Pulse) + { + PulseLines(line); + continue; + } + + line.LinePackets?.DrawPackets(); + + MySimpleObjectDraw.DrawLine(line.Start, line.End, MyStringId.GetOrCompute("ShipyardLaser"), ref line.Color, 0.4f, BlendTypeEnum.SDR); + } + } + + foreach (KeyValuePair entry in BoxDict) + { + BoxItem box = entry.Value; + Vector4 color = new Color(box.PackedColor).ToVector4(); + foreach (LineItem line in box.Lines) + { + MySimpleObjectDraw.DrawLine(line.Start, line.End, MyStringId.GetOrCompute("WeaponLaserIgnoreDepth"), ref color, 1f, BlendTypeEnum.SDR); + } + } + + foreach (ShipyardItem item in ProcessLocalYards.LocalYards) + { + Vector4 color = Color.White; + if (item.YardType == ShipyardType.Disabled || item.YardType == ShipyardType.Invalid) + continue; + + foreach (LineItem line in item.BoxLines) + { + MySimpleObjectDraw.DrawLine(line.Start, line.End, MyStringId.GetOrCompute("WeaponLaserIgnoreDepth"), ref color, 1f, BlendTypeEnum.SDR); + } + } + } + + private void PulseLines(LineItem item) + { + if (item.Descend) + item.PulseVal -= 0.025; + else + item.PulseVal += 0.025; + + Vector4 drawColor = item.Color; + drawColor.W = (float)((Math.Sin(item.PulseVal) + 1) / 2); + if (drawColor.W <= 0.05) + item.Descend = !item.Descend; + MySimpleObjectDraw.DrawLine(item.Start, item.End, MyStringId.GetOrCompute("ShipyardLaser"), ref drawColor, drawColor.W * 0.4f, BlendTypeEnum.SDR); + } + + private void FadeLines() + { + var linesToRemove = new List(); + foreach (LineItem line in Communication.FadeList) + { + line.FadeVal -= 0.075f; + if (line.FadeVal <= 0) + { + //blank the line for a couple frames. Looks better that way. + if (line.FadeVal <= -0.2f) + linesToRemove.Add(line); + continue; + } + Vector4 drawColor = line.Color; + //do a cubic fade out + drawColor.W = line.FadeVal * line.FadeVal * line.FadeVal; + MySimpleObjectDraw.DrawLine(line.Start, line.End, MyStringId.GetOrCompute("ShipyardLaser"), ref drawColor, drawColor.W * 0.4f, BlendTypeEnum.SDR); + } + + foreach (LineItem removeLine in linesToRemove) + { + Communication.FadeList.Remove(removeLine); + } + } + + protected override void UnloadData() + { + try + { + Utilities.SessionClosing = true; + + if (Utilities.AbortAllTasks()) + Logging.Instance.WriteDebug("CAUGHT AND ABORTED TASK!!!!"); + + RemoveMessageHandler(); + + if (Logging.Instance != null) + Logging.Instance.Close(); + + Communication.UnregisterHandlers(); + + foreach (ShipyardItem yard in ProcessShipyardDetection.ShipyardsList.ToArray()) + yard.Disable(false); + } + catch + { + //ignore errors on session close + } + } + } +} diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardCorner.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardCorner.cs new file mode 100644 index 0000000..a823f77 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardCorner.cs @@ -0,0 +1,581 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Sandbox.Common.ObjectBuilders; +using Sandbox.Game.EntityComponents; +using Sandbox.ModAPI; +using Sandbox.ModAPI.Interfaces; +using Sandbox.ModAPI.Interfaces.Terminal; +using ShipyardMod.ItemClasses; +using ShipyardMod.Settings; +using ShipyardMod.Utility; +using VRage.Game; +using VRage.Game.Components; +using VRage.Game.ModAPI; +using VRage.ModAPI; +using VRageMath; +using VRage.ObjectBuilders; +using VRage.Utils; +using VRageRender.Import; +using Sandbox.Engine.Physics; +using System.Text.RegularExpressions; +using ShipyardMod.ProcessHandlers; + +namespace ShipyardMod +{ + [MyEntityComponentDescriptor(typeof(MyObjectBuilder_Collector), false, "ShipyardCorner_Large", "ShipyardCorner_Small")] + public class ShipyardCorner : MyGameLogicComponent + { + private static bool _init; + private static readonly MyDefinitionId PowerDef = MyResourceDistributorComponent.ElectricityId; + private static readonly List Controls = new List(); + private IMyCollector _block; + private float _maxpower; + private float _power; + private string _info = String.Empty; + + private MyResourceSinkComponent _sink = new MyResourceSinkComponent(); + + public ShipyardItem Shipyard = null; + + public override void Init(MyObjectBuilder_EntityBase objectBuilder) + { + _block = (IMyCollector)Container.Entity; + _block.Components.TryGet(out _sink); + //_block.NeedsUpdate = MyEntityUpdateEnum.NONE; + NeedsUpdate |= MyEntityUpdateEnum.BEFORE_NEXT_FRAME; + NeedsUpdate |= MyEntityUpdateEnum.EACH_FRAME; + NeedsUpdate |= MyEntityUpdateEnum.EACH_10TH_FRAME; + _block.OnClosing += OnClosing; + _block.AppendingCustomInfo += AppendingCustomInfo; + } + + private void OnClosing(IMyEntity obj) + { + _block.OnClosing -= OnClosing; + _block.AppendingCustomInfo -= AppendingCustomInfo; + NeedsUpdate = MyEntityUpdateEnum.NONE; + } + + public override void Close() + { + NeedsUpdate = MyEntityUpdateEnum.NONE; + } + + public override void UpdateAfterSimulation() + { + if (MyAPIGateway.Utilities.IsDedicated) + { + return; + } + if (_block?.CubeGrid?.Physics == null) + { + return; + } + + // Check if the corner is part of a valid shipyard + bool isValidShipyard = ProcessShipyardDetection.ShipyardsList.Any(yard => yard.Tools.Contains(_block)); + + if (!isValidShipyard) + { + var pos = _block.GetPosition(); + var offset = 2.5f; + var mat = _block.WorldMatrix; + float width = 0.125f; + pos += -mat.Forward * offset; + pos += -mat.Left * offset; + pos += -mat.Up * offset; + + Vector3D[] directions = new Vector3D[] { + mat.Forward, + mat.Left, + mat.Up + }; + + var red = (VRageMath.Vector4)Color.Red; + var yellow = (VRageMath.Vector4)Color.Yellow; + var blue = (VRageMath.Vector4)Color.Blue; + + var material = MyStringId.GetOrCompute("Square"); + var blend = VRageRender.MyBillboard.BlendTypeEnum.PostPP; + + string subtypeShipyardCorner = _block.BlockDefinition.SubtypeId.ToString(); + float maxLength = 1000.0f; + var cells = new List(); + foreach (var direction in directions) + { + var endPoint = pos + direction * maxLength; + cells.Clear(); + _block.CubeGrid.RayCastCells(pos, endPoint, cells); + + int indexOfOpposingShipyardCorner = -1; + for (int i = 0; i < cells.Count; i++) + { + var block = _block.CubeGrid.GetCubeBlock(cells[i]); + if (block != null) + { + var subtypeOther = block.BlockDefinition.Id.SubtypeId.ToString(); + if (subtypeOther == subtypeShipyardCorner && block.SlimId() != _block.SlimBlock.SlimId()) + { + indexOfOpposingShipyardCorner = i; + break; + } + } + } + if (indexOfOpposingShipyardCorner > -1) + { + endPoint = pos + direction * (indexOfOpposingShipyardCorner + 4.5); + var blockedByInvalid = false; + var pathHasEmptyCells = false; + for (int i = 3; i < indexOfOpposingShipyardCorner - 1; i++) + { + var block = _block.CubeGrid.GetCubeBlock(cells[i]); + if (block != null) + { + var subtypeOther = block.BlockDefinition.Id.SubtypeId.ToString(); + if (!(subtypeOther == "ShipyardConveyor_Large" || subtypeOther == "ShipyardConveyorMount_Large")) + { + blockedByInvalid = true; + break; + } + } + else + { + pathHasEmptyCells = true; + } + } + if (blockedByInvalid) + { + MySimpleObjectDraw.DrawLine(pos, endPoint, material, ref red, width, blend); + continue; + } + else if (pathHasEmptyCells) + { + MySimpleObjectDraw.DrawLine(pos, endPoint, material, ref yellow, width, blend); + continue; + } + } + else + { + MySimpleObjectDraw.DrawLine(pos, endPoint, material, ref blue, width, blend); + continue; + } + } + } + } + + public override void UpdateOnceBeforeFrame() + { + if (_init) + return; + + _init = true; + _block = Entity as IMyCollector; + + if (_block == null) + return; + + //create terminal controls + IMyTerminalControlSeparator sep = MyAPIGateway.TerminalControls.CreateControl(string.Empty); + sep.Visible = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + MyAPIGateway.TerminalControls.AddControl(sep); + + IMyTerminalControlOnOffSwitch guideSwitch = MyAPIGateway.TerminalControls.CreateControl("Shipyard_GuideSwitch"); + guideSwitch.Title = MyStringId.GetOrCompute("Guide Boxes"); + guideSwitch.Tooltip = MyStringId.GetOrCompute("Toggles the guide boxes drawn around grids in the shipyard."); + guideSwitch.OnText = MyStringId.GetOrCompute("On"); + guideSwitch.OffText = MyStringId.GetOrCompute("Off"); + guideSwitch.Visible = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + guideSwitch.Enabled = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner") && GetYard(b) != null; + guideSwitch.SupportsMultipleBlocks = true; + guideSwitch.Getter = GetGuideEnabled; + guideSwitch.Setter = SetGuideEnabled; + MyAPIGateway.TerminalControls.AddControl(guideSwitch); + Controls.Add(guideSwitch); + + var lockSwitch = MyAPIGateway.TerminalControls.CreateControl("Shipyard_LockSwitch"); + lockSwitch.Title = MyStringId.GetOrCompute("Advanced Locking"); + lockSwitch.Tooltip = MyStringId.GetOrCompute("Toggles locking grids in the shipyard when grinding or welding while moving."); + lockSwitch.OnText = MyStringId.GetOrCompute("On"); + lockSwitch.OffText = MyStringId.GetOrCompute("Off"); + lockSwitch.Visible = b => b.BlockDefinition.SubtypeId.Equals("ShipyardCorner_Small"); + lockSwitch.Enabled = b => b.BlockDefinition.SubtypeId.Equals("ShipyardCorner_Small") && GetYard(b) != null; + lockSwitch.SupportsMultipleBlocks = true; + lockSwitch.Getter = GetLockEnabled; + lockSwitch.Setter = SetLockEnabled; + MyAPIGateway.TerminalControls.AddControl(lockSwitch); + Controls.Add(lockSwitch); + + IMyTerminalControlButton grindButton = MyAPIGateway.TerminalControls.CreateControl("Shipyard_GrindButton"); + IMyTerminalControlButton weldButton = MyAPIGateway.TerminalControls.CreateControl("Shipyard_WeldButton"); + IMyTerminalControlButton stopButton = MyAPIGateway.TerminalControls.CreateControl("Shipyard_StopButton"); + + grindButton.Title = MyStringId.GetOrCompute("Grind"); + grindButton.Tooltip = MyStringId.GetOrCompute("Begins grinding ships in the yard."); + grindButton.Enabled = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner") && GetYard(b)?.YardType == ShipyardType.Disabled; + grindButton.Visible = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + grindButton.SupportsMultipleBlocks = true; + grindButton.Action = b => Communication.SendYardCommand(b.CubeGrid.EntityId, ShipyardType.Grind); + MyAPIGateway.TerminalControls.AddControl(grindButton); + Controls.Add(grindButton); + + weldButton.Title = MyStringId.GetOrCompute("Weld"); + weldButton.Tooltip = MyStringId.GetOrCompute("Begins welding ships in the yard."); + weldButton.Enabled = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner") && GetYard(b)?.YardType == ShipyardType.Disabled; + weldButton.Visible = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + weldButton.SupportsMultipleBlocks = true; + weldButton.Action = b => Communication.SendYardCommand(b.CubeGrid.EntityId, ShipyardType.Weld); + MyAPIGateway.TerminalControls.AddControl(weldButton); + Controls.Add(weldButton); + + stopButton.Title = MyStringId.GetOrCompute("Stop"); + stopButton.Tooltip = MyStringId.GetOrCompute("Stops the shipyard."); + stopButton.Enabled = b => + { + if (!b.BlockDefinition.SubtypeId.Contains("ShipyardCorner")) + return false; + + ShipyardItem yard = GetYard(b); + + return yard?.YardType == ShipyardType.Weld || yard?.YardType == ShipyardType.Grind; + }; + stopButton.Visible = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + stopButton.SupportsMultipleBlocks = true; + stopButton.Action = b => Communication.SendYardCommand(b.CubeGrid.EntityId, ShipyardType.Disabled); + MyAPIGateway.TerminalControls.AddControl(stopButton); + Controls.Add(stopButton); + + IMyTerminalControlSlider beamCountSlider = MyAPIGateway.TerminalControls.CreateControl("Shipyard_BeamCount"); + beamCountSlider.Title = MyStringId.GetOrCompute("Beam Count"); + + beamCountSlider.Tooltip = MyStringId.GetOrCompute("Number of beams this shipyard can use per corner."); + beamCountSlider.SetLimits(1, 3); + beamCountSlider.Writer = (b, result) => result.Append(GetBeamCount(b)); + beamCountSlider.Visible = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + beamCountSlider.Enabled = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner") && GetYard(b) != null; + beamCountSlider.Getter = b => GetBeamCount(b); + beamCountSlider.Setter = (b, v) => + { + SetBeamCount(b, (int)Math.Round(v, 0, MidpointRounding.ToEven)); + beamCountSlider.UpdateVisual(); + }; + beamCountSlider.SupportsMultipleBlocks = true; + MyAPIGateway.TerminalControls.AddControl(beamCountSlider); + Controls.Add(beamCountSlider); + + IMyTerminalControlSlider grindSpeedSlider = MyAPIGateway.TerminalControls.CreateControl("Shipyard_GrindSpeed"); + grindSpeedSlider.Title = MyStringId.GetOrCompute("Grind Speed"); + + grindSpeedSlider.Tooltip = MyStringId.GetOrCompute("How fast this shipyard grinds grids."); + grindSpeedSlider.SetLimits(0.01f, 2); + grindSpeedSlider.Writer = (b, result) => result.Append(GetGrindSpeed(b)); + grindSpeedSlider.Visible = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + grindSpeedSlider.Enabled = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner") && GetYard(b) != null; + grindSpeedSlider.Getter = GetGrindSpeed; + grindSpeedSlider.Setter = (b, v) => + { + SetGrindSpeed(b, (float)Math.Round(v, 2, MidpointRounding.ToEven)); + grindSpeedSlider.UpdateVisual(); + }; + grindSpeedSlider.SupportsMultipleBlocks = true; + MyAPIGateway.TerminalControls.AddControl(grindSpeedSlider); + Controls.Add(grindSpeedSlider); + + IMyTerminalControlSlider weldSpeedSlider = MyAPIGateway.TerminalControls.CreateControl("Shipyard_WeldSpeed"); + weldSpeedSlider.Title = MyStringId.GetOrCompute("Weld Speed"); + + weldSpeedSlider.Tooltip = MyStringId.GetOrCompute("How fast this shipyard welds grids."); + weldSpeedSlider.SetLimits(0.01f, 2); + weldSpeedSlider.Writer = (b, result) => result.Append(GetWeldSpeed(b)); + weldSpeedSlider.Visible = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + weldSpeedSlider.Enabled = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner") && GetYard(b) != null; + weldSpeedSlider.Getter = GetWeldSpeed; + weldSpeedSlider.Setter = (b, v) => + { + SetWeldSpeed(b, (float)Math.Round(v, 2, MidpointRounding.ToEven)); + weldSpeedSlider.UpdateVisual(); + }; + weldSpeedSlider.SupportsMultipleBlocks = true; + MyAPIGateway.TerminalControls.AddControl(weldSpeedSlider); + Controls.Add(weldSpeedSlider); + + IMyTerminalAction grindAction = MyAPIGateway.TerminalControls.CreateAction("Shipyard_GrindAction"); + grindAction.Enabled = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + grindAction.Name = new StringBuilder("Grind"); + grindAction.Icon = @"Textures\GUI\Icons\Actions\Start.dds"; + grindAction.Action = b => Communication.SendYardCommand(b.CubeGrid.EntityId, ShipyardType.Grind); + MyAPIGateway.TerminalControls.AddAction(grindAction); + + IMyTerminalAction weldAction = MyAPIGateway.TerminalControls.CreateAction("Shipyard_WeldAction"); + weldAction.Enabled = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + weldAction.Name = new StringBuilder("Weld"); + weldAction.Icon = @"Textures\GUI\Icons\Actions\Start.dds"; + weldAction.Action = b => Communication.SendYardCommand(b.CubeGrid.EntityId, ShipyardType.Weld); + MyAPIGateway.TerminalControls.AddAction(weldAction); + + IMyTerminalAction stopAction = MyAPIGateway.TerminalControls.CreateAction("Shipyard_StopAction"); + stopAction.Enabled = b => b.BlockDefinition.SubtypeId.Contains("ShipyardCorner"); + stopAction.Name = new StringBuilder("Stop"); + stopAction.Icon = @"Textures\GUI\Icons\Actions\Reset.dds"; + stopAction.Action = b => Communication.SendYardCommand(b.CubeGrid.EntityId, ShipyardType.Disabled); + MyAPIGateway.TerminalControls.AddAction(stopAction); + } + + private string _lastStatus = ""; // Add this as a class field + + private void AppendingCustomInfo(IMyTerminalBlock b, StringBuilder arg2) + { + try + { + var sb = new StringBuilder(); + ShipyardItem yard = GetYard(b); + + if (yard == null) + { + _lastStatus = "Not part of a valid shipyard"; + sb.AppendLine(_lastStatus); + arg2.Append(sb); + return; + } + + sb.Append("Required Input: "); + MyValueFormatter.AppendWorkInBestUnit(_power, sb); + sb.AppendLine(); + sb.Append("Max required input: "); + MyValueFormatter.AppendWorkInBestUnit(_maxpower, sb); + sb.AppendLine(); + sb.AppendLine($"Shipyard Status: {yard.YardType}"); + + // Use the DisableReason from ShipyardItem + switch (yard.YardType) + { + case ShipyardType.Disabled: + _lastStatus = $"Status Reason: {yard.DisableReason}"; + sb.AppendLine(_lastStatus); + break; + + case ShipyardType.Invalid: + _lastStatus = "Status Reason: Shipyard configuration is invalid"; + sb.AppendLine(_lastStatus); + break; + + case ShipyardType.Weld: + if (yard.MissingComponentsDict.Any()) + { + sb.AppendLine("Status Reason: Missing components - Welding paused"); + sb.AppendLine("Missing Components:"); + foreach (var component in yard.MissingComponentsDict) + { + sb.AppendLine($" {component.Key}: {component.Value}"); + } + } + else if (!yard.Tools.Any(x => ((IMyFunctionalBlock)x).Enabled)) + { + sb.AppendLine("Status Reason: One or more corner blocks are disabled"); + } + break; + + case ShipyardType.Grind: + if (!yard.Tools.Any(x => ((IMyFunctionalBlock)x).Enabled)) + { + sb.AppendLine("Status Reason: One or more corner blocks are disabled"); + } + break; + } + + sb.AppendLine($"Blocks remaining: {yard.TargetBlocks.Count}"); + + if (ShipyardCore.Debug) + { + sb.AppendLine($"Connected cargo containers: {yard.ConnectedCargo.Count}"); + sb.AppendLine($"Contained grids: {yard.ContainsGrids.Count}"); + sb.AppendLine($"Intersecting grids: {yard.IntersectsGrids.Count}"); + } + + sb.Append(_info); + arg2.Append(sb); + } + catch (Exception ex) + { + arg2.AppendLine($"Error in custom info: {ex.Message}"); + } + } + + private int GetBeamCount(IMyCubeBlock b) + { + if (GetYard(b) == null) + return 3; + + return ShipyardSettings.Instance.GetYardSettings(b.CubeGrid.EntityId).BeamCount; + } + + private void SetBeamCount(IMyCubeBlock b, int value) + { + if (GetYard(b) == null) + return; + + //this value check stops infinite loops of sending the setting to server and immediately getting the same value back + if (value == GetBeamCount(b)) + return; + + YardSettingsStruct settings = ShipyardSettings.Instance.GetYardSettings(b.CubeGrid.EntityId); + settings.BeamCount = value; + + ShipyardSettings.Instance.SetYardSettings(b.CubeGrid.EntityId, settings); + + Communication.SendShipyardSettings(b.CubeGrid.EntityId, settings); + } + + private bool GetGuideEnabled(IMyCubeBlock b) + { + if (GetYard(b) == null) + return true; + + return ShipyardSettings.Instance.GetYardSettings(b.CubeGrid.EntityId).GuideEnabled; + } + + private void SetGuideEnabled(IMyCubeBlock b, bool value) + { + if (GetYard(b) == null) + return; + + if (value == GetGuideEnabled(b)) + return; + + YardSettingsStruct settings = ShipyardSettings.Instance.GetYardSettings(b.CubeGrid.EntityId); + settings.GuideEnabled = value; + + ShipyardSettings.Instance.SetYardSettings(b.CubeGrid.EntityId, settings); + + Communication.SendShipyardSettings(b.CubeGrid.EntityId, settings); + } + + private bool GetLockEnabled(IMyCubeBlock b) + { + if (GetYard(b) == null) + return false; + + return ShipyardSettings.Instance.GetYardSettings(b.CubeGrid.EntityId).AdvancedLocking; + } + + private void SetLockEnabled(IMyCubeBlock b, bool value) + { + if (GetYard(b) == null) + return; + + if (value == GetLockEnabled(b)) + return; + + YardSettingsStruct settings = ShipyardSettings.Instance.GetYardSettings(b.CubeGrid.EntityId); + settings.AdvancedLocking = value; + + ShipyardSettings.Instance.SetYardSettings(b.CubeGrid.EntityId, settings); + + Communication.SendShipyardSettings(b.CubeGrid.EntityId, settings); + } + + private float GetGrindSpeed(IMyCubeBlock b) + { + if (GetYard(b) == null) + return 0.1f; + + return ShipyardSettings.Instance.GetYardSettings(b.CubeGrid.EntityId).GrindMultiplier; + } + + private void SetGrindSpeed(IMyCubeBlock b, float value) + { + if (GetYard(b) == null) + return; + + if (value == GetGrindSpeed(b)) + return; + + YardSettingsStruct settings = ShipyardSettings.Instance.GetYardSettings(b.CubeGrid.EntityId); + settings.GrindMultiplier = value; + + ShipyardSettings.Instance.SetYardSettings(b.CubeGrid.EntityId, settings); + + Communication.SendShipyardSettings(b.CubeGrid.EntityId, settings); + } + + private float GetWeldSpeed(IMyCubeBlock b) + { + if (GetYard(b) == null) + return 0.1f; + + return ShipyardSettings.Instance.GetYardSettings(b.CubeGrid.EntityId).WeldMultiplier; + } + + private void SetWeldSpeed(IMyCubeBlock b, float value) + { + if (GetYard(b) == null) + return; + + if (value == GetWeldSpeed(b)) + return; + + YardSettingsStruct settings = ShipyardSettings.Instance.GetYardSettings(b.CubeGrid.EntityId); + settings.WeldMultiplier = value; + + ShipyardSettings.Instance.SetYardSettings(b.CubeGrid.EntityId, settings); + + Communication.SendShipyardSettings(b.CubeGrid.EntityId, settings); + } + + private ShipyardItem GetYard(IMyCubeBlock b) + { + return b.GameLogic.GetAs()?.Shipyard; + } + + public void SetPowerUse(float req) + { + _power = req; + } + + public void SetMaxPower(float req) + { + _maxpower = req; + } + + public void SetInfo(string info) + { + _info = info; + } + + public void UpdateVisuals() + { + foreach (IMyTerminalControl control in Controls) + control.UpdateVisual(); + } + + public override void UpdateBeforeSimulation() + { + if (!((IMyCollector)Container.Entity).Enabled) + _power = 0f; + _sink.SetMaxRequiredInputByType(PowerDef, _power); + _sink.SetRequiredInputByType(PowerDef, _power); + //sink.Update(); + } + + public override void UpdateBeforeSimulation10() + { + try + { + if (_block != null && !string.IsNullOrEmpty(_lastStatus)) + { + ((IMyTerminalBlock)Container.Entity).RefreshCustomInfo(); + } + } + catch (Exception ex) + { + Logging.Instance.WriteLine($"Error in UpdateBeforeSimulation10: {ex.Message}"); + } + } + + public override MyObjectBuilder_EntityBase GetObjectBuilder(bool copy = false) + { + return Entity.GetObjectBuilder(copy); + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardItem.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardItem.cs new file mode 100644 index 0000000..f6d63c6 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardItem.cs @@ -0,0 +1,449 @@ +using System.Collections.Generic; +using System.Linq; +using Sandbox.Game.Entities; +using Sandbox.ModAPI; +using ShipyardMod.Settings; +using ShipyardMod.Utility; +using VRage; +using VRage.Game.ModAPI; +using VRage.ModAPI; +using VRageMath; +using System; +using VRage.Collections; +using VRage.Game.Components; + +namespace ShipyardMod.ItemClasses +{ + public enum ShipyardType : byte + { + Disabled, + Weld, + Grind, + Invalid, + Scanning + } + + public class ShipyardItem + { + private MyTuple _shouldDisable; + //public int ActiveTargets; + + //these are set when processing a grid + //public IMyCubeGrid Grid; + //tool, target block + public Dictionary BlocksToProcess = new Dictionary(); + + public List BoxLines = new List(12); + public HashSet ConnectedCargo = new HashSet(); + + public MyConcurrentHashSet ContainsGrids = new MyConcurrentHashSet(); + public HashSet IntersectsGrids = new HashSet(); + + public LCDMenu Menu = null; + + public Dictionary MissingComponentsDict = new Dictionary(); + public Dictionary> ProxDict = new Dictionary>(); + + public YardSettingsStruct Settings; + public MyOrientedBoundingBoxD ShipyardBox; + public HashSet TargetBlocks = new HashSet(); + public IMyCubeBlock[] Tools; + //public int TotalBlocks; + public IMyEntity YardEntity; + public List YardGrids = new List(); + + public ShipyardType YardType; + public bool StaticYard; + public string DisableReason { get; private set; } = "Shipyard is idle"; + + public ShipyardItem(MyOrientedBoundingBoxD box, IMyCubeBlock[] tools, ShipyardType yardType, IMyEntity yardEntity) + { + ShipyardBox = box; + Tools = tools; + YardType = yardType; + YardEntity = yardEntity; + StaticYard = tools[0].BlockDefinition.SubtypeId == "ShipyardCorner_Large"; + } + + public long EntityId + { + get { return YardEntity.EntityId; } + } + + public void Init(ShipyardType yardType) + { + lock (_stateLock) + { + if (_isDisabling) + { + Logging.Instance.WriteDebug($"[ShipyardItem.Init] Cannot initialize while disabling: {EntityId}"); + return; + } + + if (YardType == yardType) + { + Logging.Instance.WriteDebug($"[ShipyardItem.Init] Already in state {yardType}"); + return; + } + + try + { + _isOperating = true; + Logging.Instance.WriteLine($"[ShipyardItem.Init] Starting initialization for yard {EntityId} to type {yardType}"); + Logging.Instance.WriteDebug($"YardItem.Init: {yardType}"); + + TargetBlocks.Clear(); + ProxDict.Clear(); + + foreach (var entry in BlocksToProcess) + { + for (int i = 0; i < entry.Value.Length; i++) + { + if (entry.Value[i] != null) + { + Communication.ClearLine(entry.Key, i); + entry.Value[i] = null; + } + } + } + + foreach (var grid in YardGrids) + { + if (grid != null) + { + ((MyCubeGrid)grid).OnGridSplit -= OnGridSplit; + } + } + + YardGrids.Clear(); + YardType = yardType; + + foreach (IMyCubeGrid grid in ContainsGrids.Where(g => g != null && !g.MarkedForClose)) + { + ((MyCubeGrid)grid).OnGridSplit += OnGridSplit; + } + + YardGrids = ContainsGrids.Where(x => x != null && !x.MarkedForClose).ToList(); + ContainsGrids.Clear(); + IntersectsGrids.Clear(); + + Utilities.Invoke(() => + { + foreach (IMyCubeBlock tool in Tools) + { + if (tool != null && !tool.Closed && !tool.MarkedForClose) + { + var myFunctionalBlock = tool as IMyFunctionalBlock; + if (myFunctionalBlock != null) + { + myFunctionalBlock.Enabled = true; + } + } + } + }); + + Logging.Instance.WriteDebug($"[ShipyardItem.Init] Initialized with {YardGrids.Count} grids"); + Communication.SendYardState(this); + } + finally + { + _isOperating = false; + } + } + } + + private readonly object _stateLock = new object(); + private volatile bool _isDisabling; + private volatile bool _isOperating; + + public void Disable(bool broadcast = true, string reason = null) + { + lock (_stateLock) + { + if (_isDisabling) + { + Logging.Instance.WriteDebug($"[ShipyardItem.Disable] Already disabling yard {EntityId}"); + return; + } + + // Set the disable reason + DisableReason = reason ?? (MissingComponentsDict.Any() + ? "Insufficient components to continue welding" + : "Shipyard is idle"); + + _isDisabling = true; + _isOperating = false; + Logging.Instance.WriteDebug($"[ShipyardItem.Disable] Starting disable for yard {EntityId}, broadcast={broadcast}, reason: {DisableReason}"); + _shouldDisable.Item1 = true; + _shouldDisable.Item2 = broadcast; + } + } + + public bool CanProcessCommand(ShipyardType newType) + { + if (_isDisabling || _isOperating) + { + Logging.Instance.WriteDebug($"[ShipyardItem.CanProcessCommand] Yard {EntityId} is busy (Disabling: {_isDisabling}, Operating: {_isOperating})"); + return false; + } + + if (Tools.Any(t => t.Closed || t.MarkedForClose)) + { + Logging.Instance.WriteDebug($"[ShipyardItem.CanProcessCommand] Yard {EntityId} has closed tools"); + return false; + } + + if (YardEntity.Closed || YardEntity.MarkedForClose) + { + Logging.Instance.WriteDebug($"[ShipyardItem.CanProcessCommand] Yard {EntityId} entity is closed"); + return false; + } + + return true; + } + + public void ProcessDisable() + { + if (!_shouldDisable.Item1) + return; + + lock (_stateLock) + { + try + { + Logging.Instance.WriteDebug($"[ShipyardItem.ProcessDisable] Processing disable for yard {EntityId}"); + + foreach (IMyCubeGrid grid in YardGrids) + { + Logging.Instance.WriteDebug($"[ShipyardItem.ProcessDisable] Unregistering grid split for grid {grid.EntityId}"); + ((MyCubeGrid)grid).OnGridSplit -= OnGridSplit; + } + + YardGrids.Clear(); + MissingComponentsDict.Clear(); + ContainsGrids.Clear(); + IntersectsGrids.Clear(); + ProxDict.Clear(); + TargetBlocks.Clear(); + YardType = ShipyardType.Disabled; + + // Clear all processing state + foreach (var entry in BlocksToProcess) + { + for (int i = 0; i < entry.Value.Length; i++) + { + if (entry.Value[i] != null) + { + Communication.ClearLine(entry.Key, i); + entry.Value[i] = null; + } + } + } + + if (_shouldDisable.Item2 && MyAPIGateway.Multiplayer.IsServer) + { + Logging.Instance.WriteDebug("[ShipyardItem.ProcessDisable] Broadcasting state change"); + Communication.SendYardState(this); + } + + Logging.Instance.WriteDebug("[ShipyardItem.ProcessDisable] Disable complete"); + } + finally + { + _shouldDisable.Item1 = false; + _shouldDisable.Item2 = false; + _isDisabling = false; + _isOperating = false; + } + } + } + public void HandleButtonPressed(int index) + { + Communication.SendButtonAction(YardEntity.EntityId, index); + } + + /* + * Worst case power usage would be a grid of 24+ blocks immediately + * inside one corner of our yard. All (up to) 24 of our lasers will be + * required to weld/grind this grid, but the lasers from the opposite + * corner would be the longest, effectively equal to the diagonal + * length of our yard. + * + * (each corner block) + * base power = 5 + * (each laser) + * base power = 30 + * additional power = 300 * Max(WeldMultiplier, GrindMultiplier) * (YardDiagonal / 200000) + * + * For balance, we scale power usage so that when YardDiag^2 == 200,000 + * (the same distance that our component effeciency bottoms out: ~450m), + * each //LASER// at 1x multiplier consumes the full output of a Large + * Reactor (300 MW). + * + * To put that in perspective, a cubical shipyard with a ~450m diagonal + * would be ~250m on each edge, or about 100 large blocks. But keep in + * mind: even with a shipyard this big, as long as the target grid is + * centered within the volume of the shipyard, laserLength would only + * be 225m, not the full 450m. And at 225m, each laser consumes only + * 75 MW: 25% of the max. So with a shipyard of this size, centering + * your target grid gets more and more important. + * + * Unlike component efficiency, however, which bottoms out to a fixed + * "minimum" efficiency past 450m, power requirements for lasers longer + * than this will continue to increase exponentially. + * + * This really only needs to be called whenever yard settings are changed + * (since BeamCount and multipliers affect this calculation), or when the + * yard changes state (weld/grind/disable) + */ + public void UpdateMaxPowerUse() + { + float multiplier; + var corners = new Vector3D[8]; + ShipyardBox.GetCorners(corners, 0); + + if (YardType == ShipyardType.Weld) + { + multiplier = Settings.WeldMultiplier; + } + else if (YardType == ShipyardType.Grind) + { + multiplier = Settings.GrindMultiplier; + } + else + { + // Yard is neither actively welding or grinding right now, so just show worst case + multiplier = Math.Max(Settings.WeldMultiplier, Settings.GrindMultiplier); + } + + float maxpower = 5 + Settings.BeamCount * (30 + 300 * multiplier * (float)Vector3D.DistanceSquared(corners[0], corners[6]) / 200000); + + if (!StaticYard) + maxpower *= 2; + + foreach (IMyCubeBlock tool in Tools) + { + ((IMyCollector)tool).GameLogic.GetAs().SetMaxPower(maxpower); + } + } + + public void UpdatePowerUse(float addedPower = 0) + { + addedPower /= 8; + if (YardType == ShipyardType.Disabled || YardType == ShipyardType.Invalid) + { + Utilities.Invoke(() => + { + if (Tools != null) + { + foreach (IMyCubeBlock tool in Tools) + { + var shipyardCorner = tool?.GameLogic?.GetAs(); + if (shipyardCorner != null) + { + shipyardCorner.SetPowerUse(5 + addedPower); + Communication.SendToolPower(tool.EntityId, 5 + addedPower); + } + } + } + }); + } + else + { + float multiplier; + if (YardType == ShipyardType.Weld) + { + multiplier = Settings.WeldMultiplier; + } + else + { + multiplier = Settings.GrindMultiplier; + } + + Utilities.Invoke(() => + { + if (Tools != null) + { + foreach (IMyCubeBlock tool in Tools) + { + float power = 5; + //Logging.Instance.WriteDebug(String.Format("Tool[{0}] Base power usage [{1:F1} MW]", tool.DisplayNameText, power)); + + if (BlocksToProcess != null && BlocksToProcess.ContainsKey(tool.EntityId)) + { + int i = 0; + foreach (BlockTarget blockTarget in BlocksToProcess[tool.EntityId]) + { + if (blockTarget == null) + continue; + + float laserPower = 30 + 300 * multiplier * (float)blockTarget.ToolDist[tool.EntityId] / 200000; + //Logging.Instance.WriteDebug(String.Format("Tool[{0}] laser[{1}] distance[{2:F1}m] multiplier[{3:F1}x] additional power req [{4:F1} MW]", tool.DisplayNameText, i, Math.Sqrt(blockTarget.ToolDist[tool.EntityId]), multiplier, laserPower)); + power += laserPower; + i++; + } + } + + if (!StaticYard) + power *= 2; + + power += addedPower; + + //Logging.Instance.WriteDebug(String.Format("Tool[{0}] Total computed power [{1:F1} MW]", tool.DisplayNameText, power)); + var shipyardCorner = tool?.GameLogic?.GetAs(); + if (shipyardCorner != null) + { + shipyardCorner.SetPowerUse(power); + Communication.SendToolPower(tool.EntityId, power); + } + } + } + }); + } + } + + public void OnGridSplit(MyCubeGrid oldGrid, MyCubeGrid newGrid) + { + if (YardGrids.Any(g => g.EntityId == oldGrid.EntityId)) + { + newGrid.OnGridSplit += OnGridSplit; + YardGrids.Add(newGrid); + } + } + + public void UpdatePosition() + { + ShipyardBox = MathUtility.CreateOrientedBoundingBox((IMyCubeGrid)YardEntity, Tools.Select(x => x.GetPosition()).ToList(), 2.5); + } + + /// + /// Gives grids in the shipyard a slight nudge to help them match velocity when the shipyard is moving. + /// + /// Code donated by Equinox + /// + public void NudgeGrids() + { + //magic value of 0.005 here was determined experimentally. + //value is just enough to assist with matching velocity to the shipyard, but not enough to prevent escape + foreach (var grid in ContainsGrids) + { + if (Vector3D.IsZero(grid.Physics.LinearVelocity - YardEntity.Physics.LinearVelocity)) + continue; + + grid.Physics.ApplyImpulse(grid.Physics.Mass * Vector3D.ClampToSphere((YardEntity.Physics.LinearVelocity - grid.Physics.LinearVelocity), 0.005), grid.Physics.CenterOfMassWorld); + } + + foreach (var grid in YardGrids) + { + if (!Settings.AdvancedLocking) + grid.Physics.ApplyImpulse(grid.Physics.Mass * Vector3D.ClampToSphere((YardEntity.Physics.LinearVelocity - grid.Physics.LinearVelocity), 0.005), grid.Physics.CenterOfMassWorld); + else + { + double powerUse = MathUtility.MatchShipVelocity(grid, YardEntity, true); + if(powerUse > 0) + UpdatePowerUse((float)powerUse); + } + } + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardSettings.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardSettings.cs new file mode 100644 index 0000000..0c170a3 --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/ShipyardSettings.cs @@ -0,0 +1,101 @@ +using System; +using System.Xml.Serialization; +using Sandbox.ModAPI; +using ShipyardMod.Utility; +using VRage.Serialization; + +namespace ShipyardMod.Settings +{ + public struct YardSettingsStruct + { + public YardSettingsStruct(long entityId) + { + EntityId = entityId; + BeamCount = 3; + GuideEnabled = true; + WeldMultiplier = 0.1f; + GrindMultiplier = 0.1f; + AdvancedLocking = false; + } + + public readonly long EntityId; + public int BeamCount; + public bool GuideEnabled; + public float WeldMultiplier; + public float GrindMultiplier; + public bool AdvancedLocking; + } + + [XmlInclude(typeof(YardSettingsStruct))] + public class ShipyardSettings + { + private static ShipyardSettings _instance; + + public SerializableDictionary BlockSettings; + + public ShipyardSettings() + { + BlockSettings = new SerializableDictionary(); + } + + public static ShipyardSettings Instance + { + get + { + if (_instance != null) + return _instance; + + if (!Load()) + _instance = new ShipyardSettings(); + + return _instance; + } + } + + public YardSettingsStruct GetYardSettings(long entityId) + { + YardSettingsStruct result; + if (!BlockSettings.Dictionary.TryGetValue(entityId, out result)) + { + result = new YardSettingsStruct(entityId); + SetYardSettings(entityId, result); + } + return result; + } + + public void SetYardSettings(long entityId, YardSettingsStruct newSet) + { + BlockSettings[entityId] = newSet; + } + + public void Save() + { + Logging.Instance.WriteLine("Saving settings"); + string serialized = MyAPIGateway.Utilities.SerializeToXML(this); + MyAPIGateway.Utilities.SetVariable("ShipyardSettings", serialized); + Logging.Instance.WriteLine("Done saving settings"); + } + + private static bool Load() + { + Logging.Instance.WriteLine("Loading settings"); + try + { + string value; + if (!MyAPIGateway.Utilities.GetVariable("ShipyardSettings", out value)) + { + Logging.Instance.WriteLine("Settings do not exist in world file"); + return false; + } + _instance = MyAPIGateway.Utilities.SerializeFromXML(value); + Logging.Instance.WriteLine("Done loading settings"); + return true; + } + catch (Exception ex) + { + Logging.Instance.WriteLine("Error loading settings: " + ex); + return false; + } + } + } +} \ No newline at end of file diff --git a/ShipyardsRevival/Data/Scripts/ShipyardMod/Utilities.cs b/ShipyardsRevival/Data/Scripts/ShipyardMod/Utilities.cs new file mode 100644 index 0000000..2f65c8c --- /dev/null +++ b/ShipyardsRevival/Data/Scripts/ShipyardMod/Utilities.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ParallelTasks; +using Sandbox.ModAPI; +using ShipyardMod.ItemClasses; +using ShipyardMod.ProcessHandlers; +using VRage; +using VRage.Collections; +using VRage.Game.ModAPI; +using VRage.ModAPI; +using VRageMath; + +namespace ShipyardMod.Utility +{ + public static class Utilities + { + private static readonly MyConcurrentHashSet ThreadLocks = new MyConcurrentHashSet(); + private static readonly MyConcurrentQueue ActionQueue = new MyConcurrentQueue(); + public static volatile bool SessionClosing; + private static readonly string FullName = typeof(Utilities).FullName; + + private static volatile bool Processing; + private static Task lastInvokeTask; + + /// + /// Invokes actions on the game thread, and blocks until completion + /// + /// + public static void InvokeBlocking(Action action) + { + var threadLock = new FastResourceLock(); + + if (!SessionClosing) + ThreadLocks.Add(threadLock); + + threadLock.AcquireExclusive(); + try + { + MyAPIGateway.Utilities.InvokeOnGameThread(() => + { + try + { + var invokeBlock = Profiler.Start(FullName, nameof(InvokeBlocking)); + action(); + invokeBlock.End(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine("Exception on blocking game thread invocation: " + ex); + + if (!SessionClosing && ShipyardCore.Debug) + throw; + } + finally + { + threadLock.ReleaseExclusive(); + } + }); + } + catch (Exception ex) + { + Logging.Instance.WriteLine("Exception in Utilities.InvokeBlocking: " + ex); + threadLock.ReleaseExclusive(); + + if (!SessionClosing && ShipyardCore.Debug) + throw; + } + + threadLock.AcquireExclusive(); + threadLock.ReleaseExclusive(); + + if (!SessionClosing) + ThreadLocks.Remove(threadLock); + } + + /// + /// Wraps InvokeOnGameThread in lots of try/catch to reduce failure on session close + /// + /// + public static void Invoke(Action action) + { + try + { + MyAPIGateway.Utilities.InvokeOnGameThread(() => + { + try + { + var invokeBlock = Profiler.Start(FullName, nameof(Invoke)); + action(); + invokeBlock.End(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine("Exception on game thread invocation: " + ex); + if (!SessionClosing && ShipyardCore.Debug) + throw; + } + }); + } + catch (Exception ex) + { + Logging.Instance.WriteLine("Exception in Utilities.Invoke: " + ex); + if (!SessionClosing && ShipyardCore.Debug) + throw; + } + } + + /// + /// Invokes an action on the game thread with a callback + /// + /// + /// + public static void Invoke(Action action, Action callback) + { + MyAPIGateway.Parallel.StartBackground(() => { InvokeBlocking(action); }, callback); + } + + /// + /// Enqueus Actions to be executed in an worker thread separate from the game thread + /// + /// + public static void QueueAction(Action action) + { + ActionQueue.Enqueue(action); + } + + /// + /// Processes the action queue + /// + public static void ProcessActionQueue() + { + if (Processing || ActionQueue.Count == 0) + return; + + if (lastInvokeTask.Exceptions != null && lastInvokeTask.Exceptions.Length > 0) + throw lastInvokeTask.Exceptions[0]; + + Processing = true; + lastInvokeTask = MyAPIGateway.Parallel.Start(() => + { + try + { + var queueBlock = Profiler.Start(FullName, nameof(ProcessActionQueue)); + while (ActionQueue.Count > 0) + { + Action action = ActionQueue.Dequeue(); + action(); + } + queueBlock.End(); + } + catch (Exception ex) + { + Logging.Instance.WriteLine("Exception in ProcessActionQueue: " + ex); + if (!SessionClosing && ShipyardCore.Debug) + throw; + } + finally + { + Processing = false; + } + }); + } + + /// + /// Causes any waiting calls to InvokeBlocking() to return immediately + /// + /// + public static bool AbortAllTasks() + { + bool result = false; + foreach (FastResourceLock threadLock in ThreadLocks) + { + result = true; + threadLock.ReleaseExclusive(); + } + + return result; + } + + /// + /// Gets the entity from the given list that is closest to the given point + /// + /// + /// + /// + public static IMyEntity GetNearestTo(Vector3D target, IEnumerable candidates) + { + IMyEntity result = null; + double minDist = 0; + + foreach (IMyEntity entity in candidates) + { + double distance = Vector3D.DistanceSquared(target, entity.GetPosition()); + if (result == null || distance < minDist) + { + minDist = distance; + result = entity; + } + } + + return result; + } + + /// + /// Finds the shipyard nearest to the given point + /// + /// + /// + public static ShipyardItem GetNearestYard(Vector3D target) + { + double minDist = 0; + IMyCubeBlock closestCorner = null; + + foreach (ShipyardItem yard in ProcessShipyardDetection.ShipyardsList.ToArray()) + { + foreach (IMyCubeBlock corner in yard.Tools) + { + double distance = Vector3D.DistanceSquared(target, corner.GetPosition()); + + if (closestCorner == null || distance < minDist) + { + minDist = distance; + closestCorner = corner; + } + } + } + + foreach (ShipyardItem toVerify in ProcessShipyardDetection.ShipyardsList.ToArray()) + { + if (toVerify.Tools.Contains(closestCorner)) + return toVerify; + } + + return null; + } + + public static bool TryGetPlayerFromSteamId(ulong steamId, out IMyPlayer result) + { + var players = new List(); + MyAPIGateway.Players.GetPlayers(players, x => x.SteamUserId == steamId); + + if (players.Count == 0) + { + result = null; + return false; + } + + result = players[0]; + return true; + } + } +} \ No newline at end of file