#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Barotrauma.Networking; using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components { internal sealed partial class CircuitBox : ItemComponent, IClientSerializable, IServerSerializable { public static readonly ImmutableHashSet UnrealiableOpcodes = ImmutableHashSet.Create(CircuitBoxOpcode.Cursor); public ImmutableArray Inputs; public ImmutableArray Outputs; public readonly List Components = new List(); public readonly List InputOutputNodes = new(); public readonly List Labels = new(); public readonly List Wires = new List(); public override bool IsActive => true; // We don't want the components and wires to transfer between subs as it would cause issues. public override bool DontTransferInventoryBetweenSubs => true; // We don't want to sell the components and wires inside the circuit box public override bool DisallowSellingItemsFromContainer => true; public Option FindInputOutputConnection(Identifier connectionName) { foreach (CircuitBoxInputConnection input in Inputs) { if (input.Name != connectionName) { continue; } return Option.Some(input); } foreach (CircuitBoxOutputConnection output in Outputs) { if (output.Name != connectionName) { continue; } return Option.Some(output); } return Option.None; } public Option FindInputOutputConnection(Connection connection) { foreach (CircuitBoxInputConnection input in Inputs) { if (input.Connection != connection) { continue; } return Option.Some(input); } foreach (CircuitBoxOutputConnection output in Outputs) { if (output.Connection != connection) { continue; } return Option.Some(output); } return Option.None; } public readonly ItemContainer[] containers; private const int ComponentContainerIndex = 0, WireContainerIndex = 1; public ItemContainer? ComponentContainer => GetContainerOrNull(ComponentContainerIndex); // wire container falls back to the main container if one isn't specified public ItemContainer? WireContainer => GetContainerOrNull(WireContainerIndex) ?? GetContainerOrNull(ComponentContainerIndex); public bool IsFull => ComponentContainer?.Inventory is { } inventory && inventory.IsFull(true); [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Locked circuit boxes can only be viewed and not interacted with.")] public bool Locked { get; set; } public CircuitBox(Item item, ContentXElement element) : base(item, element) { containers = item.GetComponents().ToArray(); if (containers.Length < 1) { DebugConsole.ThrowError("Circuit box must have at least one item container to function."); } InitProjSpecific(element); var inputBuilder = ImmutableArray.CreateBuilder(); var outputBuilder = ImmutableArray.CreateBuilder(); foreach (Connection conn in Item.Connections) { if (conn.IsOutput) { outputBuilder.Add(new CircuitBoxOutputConnection(Vector2.Zero, conn, this)); } else { inputBuilder.Add(new CircuitBoxInputConnection(Vector2.Zero, conn, this)); } } Inputs = inputBuilder.ToImmutable(); Outputs = outputBuilder.ToImmutable(); InputOutputNodes.Add(new CircuitBoxInputOutputNode(Inputs, new Vector2(-512, 0f), CircuitBoxInputOutputNode.Type.Input, this)); InputOutputNodes.Add(new CircuitBoxInputOutputNode(Outputs, new Vector2(512, 0f), CircuitBoxInputOutputNode.Type.Output, this)); item.OnDeselect += OnDeselected; } /// /// We want to load the components after the map has loaded since we need to link up the components to their items /// and pretty much all items have higher ID than the circuit box. /// private Option delayedElementToLoad; public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); if (delayedElementToLoad.IsSome()) { return; } delayedElementToLoad = Option.Some(componentElement); } public override void OnInventoryChanged() => OnViewUpdateProjSpecific(); public override void Update(float deltaTime, Camera cam) { #if CLIENT // When loading from the server the wires cannot be properly loaded and connected up because we might not be loaded in properly yet. // So we need to wait until the circuit box starts updating and then we can ensure the wires are connected. if (wasInitializedByServer) { foreach (var w in Wires) { w.EnsureWireConnected(); } wasInitializedByServer = false; } #endif TryInitializeNodes(); } public override void OnMapLoaded() => TryInitializeNodes(); private void TryInitializeNodes() { if (!delayedElementToLoad.TryUnwrap(out var loadElement)) { return; } LoadFromXML(loadElement); delayedElementToLoad = Option.None; } public void LoadFromXML(ContentXElement loadElement) { foreach (var subElement in loadElement.Elements()) { string elementName = subElement.Name.ToString().ToLowerInvariant(); switch (elementName) { case "component" when CircuitBoxComponent.TryLoadFromXML(subElement, this).TryUnwrap(out var comp): Components.Add(comp); break; case "wire" when CircuitBoxWire.TryLoadFromXML(subElement, this).TryUnwrap(out var wire): Wires.Add(wire); break; case "inputnode": LoadFor(CircuitBoxInputOutputNode.Type.Input, subElement); break; case "outputnode": LoadFor(CircuitBoxInputOutputNode.Type.Output, subElement); break; case "label": Labels.Add(CircuitBoxLabelNode.LoadFromXML(subElement, this)); break; } } #if SERVER // We need to let the clients know of the loaded data if (needsServerInitialization) { CreateInitializationEvent(); needsServerInitialization = false; } #endif void LoadFor(CircuitBoxInputOutputNode.Type type, ContentXElement subElement) { foreach (var node in InputOutputNodes) { if (node.NodeType != type) { continue; } node.Load(subElement); break; } } } public void CloneFrom(CircuitBox original, Dictionary clonedContainedItems) { Components.Clear(); Wires.Clear(); Labels.Clear(); foreach (var label in original.Labels) { var newLabel = new CircuitBoxLabelNode(label.ID, label.Color, label.Position, this); newLabel.EditText(label.HeaderText, label.BodyText); newLabel.ApplyResize(label.Size, label.Position); Labels.Add(newLabel); } for (int ioIndex = 0; ioIndex < original.InputOutputNodes.Count; ioIndex++) { var origNode = original.InputOutputNodes[ioIndex]; var cloneNode = InputOutputNodes[ioIndex]; cloneNode.Position = origNode.Position; } if (!clonedContainedItems.Any()) { return; } foreach (var origComp in original.Components) { if (!clonedContainedItems.TryGetValue(origComp.Item.ID, out var clonedItem)) { continue; } var newComponent = new CircuitBoxComponent(origComp.ID, clonedItem, origComp.Position, this, origComp.UsedResource); Components.Add(newComponent); } foreach (var origWire in original.Wires) { Option to = CircuitBoxConnectorIdentifier.FromConnection(origWire.To).FindConnection(this), from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this); if (!to.TryUnwrap(out var toConn) || !from.TryUnwrap(out var fromConn)) { DebugConsole.ThrowError($"Error while cloning item \"{Name}\" - failed to find a connection for a wire. "); continue; } var wireItem = origWire.BackingWire.Select(w => clonedContainedItems[w.ID]); var newWire = new CircuitBoxWire(this, origWire.ID, wireItem, fromConn, toConn, origWire.UsedItemPrefab); Wires.Add(newWire); } } public override XElement Save(XElement parentElement) { XElement componentElement = base.Save(parentElement); foreach (CircuitBoxInputOutputNode node in InputOutputNodes) { componentElement.Add(node.Save()); } foreach (CircuitBoxComponent node in Components) { componentElement.Add(node.Save()); } foreach (CircuitBoxWire wire in Wires) { componentElement.Add(wire.Save()); } foreach (var label in Labels) { componentElement.Add(label.Save()); } return componentElement; } public partial void OnDeselected(Character c); public record struct CreatedWire(CircuitBoxConnectorIdentifier Start, CircuitBoxConnectorIdentifier End, Option Item, ushort ID); public bool Connect(CircuitBoxConnection one, CircuitBoxConnection two, Action onCreated, ItemPrefab selectedWirePrefab) { if (!VerifyConnection(one, two)) { return false; } ushort id = ICircuitBoxIdentifiable.FindFreeID(Wires); switch (one.IsOutput) { case true when !two.IsOutput: { CircuitBoxConnectorIdentifier start = CircuitBoxConnectorIdentifier.FromConnection(one), end = CircuitBoxConnectorIdentifier.FromConnection(two); if (IsExternalConnection(one) || IsExternalConnection(two)) { CreateWireWithoutItem(one, two, id, selectedWirePrefab); onCreated(new CreatedWire(start, end, Option.None, id)); return true; } CreateWireWithItem(one, two, selectedWirePrefab, id, i => onCreated(new CreatedWire(start, end, Option.Some(i), id))); return true; } case false when two.IsOutput: { CircuitBoxConnectorIdentifier start = CircuitBoxConnectorIdentifier.FromConnection(two), end = CircuitBoxConnectorIdentifier.FromConnection(one); if (IsExternalConnection(one) || IsExternalConnection(two)) { CreateWireWithoutItem(two, one, id, selectedWirePrefab); onCreated(new CreatedWire(start, end, Option.None, id)); return true; } CreateWireWithItem(two, one, selectedWirePrefab, id, i => onCreated(new CreatedWire(start, end, Option.Some(i), id))); return true; } } return false; } private static bool VerifyConnection(CircuitBoxConnection one, CircuitBoxConnection two) { if (one.IsOutput == two.IsOutput || one == two) { return false; } if (one is CircuitBoxNodeConnection oneNodeConnection && two is CircuitBoxNodeConnection twoNodeConnection) { if (oneNodeConnection.Component == twoNodeConnection.Component) { return false; } } if (one is CircuitBoxNodeConnection { HasAvailableSlots: false } || two is CircuitBoxNodeConnection { HasAvailableSlots: false }) { return one is not CircuitBoxNodeConnection || two is not CircuitBoxNodeConnection; } return true; } private void AddLabelInternal(ushort id, Color color, Vector2 pos, NetLimitedString header, NetLimitedString body) { var newLabel = new CircuitBoxLabelNode(id, color, pos, this); newLabel.EditText(header, body); Labels.Add(newLabel); OnViewUpdateProjSpecific(); } private void RemoveLabelInternal(IReadOnlyCollection ids) { foreach (CircuitBoxLabelNode node in Labels.ToImmutableArray()) { if (!ids.Contains(node.ID)) { continue; } Labels.Remove(node); } OnViewUpdateProjSpecific(); } private void ResizeLabelInternal(ushort id, Vector2 pos, Vector2 size) { size = Vector2.Max(size, CircuitBoxLabelNode.MinSize); foreach (CircuitBoxLabelNode node in Labels) { if (node.ID != id) { continue; } node.ApplyResize(size, pos); break; } OnViewUpdateProjSpecific(); } private void RenameConnectionLabelsInternal(CircuitBoxInputOutputNode.Type type, Dictionary overrides) { foreach (var node in InputOutputNodes) { if (node.NodeType != type) { continue; } node.ReplaceAllConnectionLabelOverrides(overrides); break; } OnViewUpdateProjSpecific(); } private static bool IsExternalConnection(CircuitBoxConnection conn) => conn is (CircuitBoxInputConnection or CircuitBoxOutputConnection); private void CreateWireWithoutItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort id, ItemPrefab prefab) { bool hasExternalConnection = false; if (one is CircuitBoxInputConnection input) { hasExternalConnection = true; input.ExternallyConnectedTo.Add(two); } if (two is CircuitBoxOutputConnection output) { hasExternalConnection = true; one.Connection.CircuitBoxConnections.Add(output); } if (hasExternalConnection) { two.ExternallyConnectedFrom.Add(one); } AddWireDirect(id, prefab, Option.None, one, two); } private void CreateWireWithItem(CircuitBoxConnection one, CircuitBoxConnection two, ItemPrefab prefab, ushort wireId, Action onItemSpawned) { if (WireContainer is null) { return; } if (IsExternalConnection(one) || IsExternalConnection(two)) { DebugConsole.ThrowError("Cannot add a wire between an external connection and a component connection."); return; } SpawnItem(prefab, user: null, container: WireContainer, onSpawned: wire => { AddWireDirect(wireId, prefab, Option.Some(wire), one, two); onItemSpawned(wire); }); } private void CreateWireWithItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort wireId, Item it) { if (IsExternalConnection(one) || IsExternalConnection(two)) { DebugConsole.ThrowError("Cannot add a wire between an external connection and a component connection."); return; } AddWireDirect(wireId, it.Prefab, Option.Some(it), one, two); } private void AddWireDirect(ushort id, ItemPrefab prefab, Option backingItem, CircuitBoxConnection one, CircuitBoxConnection two) => Wires.Add(new CircuitBoxWire(this, id, backingItem, one, two, prefab)); private void RenameLabelInternal(ushort id, Color color, NetLimitedString header, NetLimitedString body) { foreach (CircuitBoxLabelNode node in Labels) { if (node.ID != id) { continue; } node.EditText(header, body); node.Color = color; break; } } private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Character? user, Action? onItemSpawned) { if (id is ICircuitBoxIdentifiable.NullComponentID) { DebugConsole.ThrowError("Unable to add component because there are no free IDs."); return false; } if (ComponentContainer?.Inventory is { } inventory && inventory.HowManyCanBePut(prefab) <= 0) { DebugConsole.ThrowError("Unable to add component because there is no space in the inventory."); return false; } SpawnItem(prefab, user, ComponentContainer, spawnedItem => { Components.Add(new CircuitBoxComponent(id, spawnedItem, pos, this, usedResource)); onItemSpawned?.Invoke(spawnedItem); OnViewUpdateProjSpecific(); }); OnViewUpdateProjSpecific(); return true; } // Unsafe because it doesn't perform error checking since it's data we get from the server private void AddComponentInternalUnsafe(ushort id, Item backingItem, ItemPrefab usedResource, Vector2 pos) { Components.Add(new CircuitBoxComponent(id, backingItem, pos, this, usedResource)); OnViewUpdateProjSpecific(); } private static void ClearSelectionFor(ushort characterId, IReadOnlyCollection nodes) { foreach (var node in nodes) { if (node.SelectedBy != characterId) { continue; } node.SetSelected(Option.None); } } private void ClearAllSelectionsInternal(ushort characterId) { ClearSelectionFor(characterId, Components); ClearSelectionFor(characterId, InputOutputNodes); ClearSelectionFor(characterId, Wires); ClearSelectionFor(characterId, Labels); } private void SelectLabelsInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) { if (overwrite) { ClearSelectionFor(characterId, Labels); } if (!ids.Any()) { return; } foreach (CircuitBoxLabelNode node in Labels) { if (!ids.Contains(node.ID)) { continue; } node.SetSelected(Option.Some(characterId)); } } private void SelectComponentsInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) { if (overwrite) { ClearSelectionFor(characterId, Components); } if (!ids.Any()) { return; } foreach (CircuitBoxComponent node in Components) { if (!ids.Contains(node.ID)) { continue; } node.SetSelected(Option.Some(characterId)); } } private void UpdateSelections(ImmutableDictionary> nodeIds, ImmutableDictionary> wireIds, ImmutableDictionary> inputOutputs, ImmutableDictionary> labels) { foreach (var wire in Wires) { if (!wireIds.TryGetValue(wire.ID, out var selectedBy)) { continue; } if (selectedBy.TryUnwrap(out var id)) { wire.IsSelected = true; wire.SelectedBy = id; continue; } wire.IsSelected = false; wire.SelectedBy = 0; } foreach (var node in Components) { if (!nodeIds.TryGetValue(node.ID, out var selectedBy)) { continue; } node.SetSelected(selectedBy); } foreach (var node in InputOutputNodes) { if (!inputOutputs.TryGetValue(node.NodeType, out var selectedBy)) { continue; } node.SetSelected(selectedBy); } foreach (var node in Labels) { if (!labels.TryGetValue(node.ID, out var selectedBy)) { continue; } node.SetSelected(selectedBy); } } private void SelectWiresInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) { if (overwrite) { ClearSelectionFor(characterId, Wires); } foreach (CircuitBoxWire wire in Wires) { if (!ids.Contains(wire.ID)) { continue; } wire.SetSelected(Option.Some(characterId)); } } private void SelectInputOutputInternal(IReadOnlyCollection io, ushort characterId, bool overwrite) { if (overwrite) { ClearSelectionFor(characterId, InputOutputNodes); } foreach (var node in InputOutputNodes) { if (!io.Contains(node.NodeType)) { continue; } node.SetSelected(Option.Some(characterId)); } } private void RemoveComponentInternal(IReadOnlyCollection ids) { foreach (CircuitBoxComponent node in Components.ToImmutableArray()) { if (!ids.Contains(node.ID)) { continue; } Components.Remove(node); node.Remove(); foreach (CircuitBoxWire wire in Wires.ToImmutableArray()) { if (node.Connectors.Contains(wire.From) || node.Connectors.Contains(wire.To)) { RemoveWireCollectionUnsafe(wire); } } } OnViewUpdateProjSpecific(); } private void RemoveWireInternal(IReadOnlyCollection ids) { foreach (CircuitBoxWire wire in Wires.ToImmutableArray()) { if (!ids.Contains(wire.ID)) { continue; } RemoveWireCollectionUnsafe(wire); } OnViewUpdateProjSpecific(); } private void RemoveWireCollectionUnsafe(CircuitBoxWire wire) { foreach (CircuitBoxOutputConnection output in Outputs) { output.Connection.CircuitBoxConnections.Remove(wire.From); } wire.From.Connection.CircuitBoxConnections.Remove(wire.To); if (wire.From is CircuitBoxInputConnection input) { input.ExternallyConnectedTo.Remove(wire.To); } wire.To.ExternallyConnectedFrom.Remove(wire.From); wire.From.ExternallyConnectedFrom.Remove(wire.To); wire.Remove(); Wires.Remove(wire); } private void MoveNodesInternal(IReadOnlyCollection ids, IReadOnlyCollection ios, IReadOnlyCollection labels, Vector2 moveAmount) { IEnumerable nodes = Components.Where(node => ids.Contains(node.ID)); foreach (CircuitBoxComponent node in nodes) { node.Position += moveAmount; } foreach (var label in Labels.Where(n => labels.Contains(n.ID))) { label.Position += moveAmount; } foreach (var io in InputOutputNodes) { if (!ios.Contains(io.NodeType)) { continue; } io.Position += moveAmount; } OnViewUpdateProjSpecific(); } public override bool Select(Character character) => item.GetComponent() is not { Attached: false } && base.Select(character); public partial void OnViewUpdateProjSpecific(); partial void InitProjSpecific(ContentXElement element); public override void ReceiveSignal(Signal signal, Connection connection) { foreach (var input in Inputs) { if (input.Connection != connection) { continue; } input.ReceiveSignal(signal); break; } } public static bool IsRoundRunning() => !Submarine.Unloading && GameMain.GameSession is { IsRunning: true }; public static Option FindCircuitBox(ushort itemId, byte componentIndex) { if (!IsRoundRunning() || Entity.FindEntityByID(itemId) is not Item item) { return Option.None; } if (componentIndex >= item.Components.Count) { return Option.None; } ItemComponent targetComponent = item.Components[componentIndex]; if (targetComponent is CircuitBox circuitBox) { return Option.Some(circuitBox); } return Option.None; } private ItemContainer? GetContainerOrNull(int index) => index >= 0 && index < containers.Length ? containers[index] : null; public void CreateRefundItemsForUsedResources(IReadOnlyCollection ids, Character? character) { if (!IsInGame()) { return; } var prefabsToCreate = Components.Where(comp => ids.Contains(comp.ID)) .Select(static comp => comp.UsedResource) .ToImmutableArray(); foreach (ItemPrefab prefab in prefabsToCreate) { if (character?.Inventory is null) { Entity.Spawner?.AddItemToSpawnQueue(prefab, item.Position, item.Submarine); } else { Entity.Spawner?.AddItemToSpawnQueue(prefab, character.Inventory); } } } public static ImmutableArray GetSortedCircuitBoxItemsFromPlayer(Character? character) => character?.Inventory?.FindAllItems(predicate: CanItemBeAccessed, recursive: true) .OrderBy(static i => i.Prefab.Identifier == Tags.FPGACircuit) .ToImmutableArray() ?? ImmutableArray.Empty; public static bool CanItemBeAccessed(Item item) => item.ParentInventory switch { ItemInventory ii => ii.Container.DrawInventory, _ => true }; public static Option GetApplicableResourcePlayerHas(ItemPrefab prefab, Character? character) { if (character is null) { return Option.None; } return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxItemsFromPlayer(character)); } public static Option GetApplicableResourcePlayerHas(ItemPrefab prefab, ImmutableArray playerItems) { foreach (var invItem in playerItems) { if (invItem.Prefab == prefab || invItem.Prefab.Identifier == Tags.FPGACircuit) { return Option.Some(invItem); } } return Option.None; } public static void SpawnItem(ItemPrefab prefab, Character? user, ItemContainer? container, Action onSpawned) { if (container is null) { throw new Exception("Circuit box has no inventory"); } if (IsInGame()) { Entity.Spawner?.AddItemToSpawnQueue(prefab, container.Inventory, onSpawned: it => { AssignWifiComponentTeam(it, user); onSpawned(it); }); return; } Item forceSpawnedItem = new Item(prefab, Vector2.Zero, null); container.Inventory.TryPutItem(forceSpawnedItem, null); onSpawned(forceSpawnedItem); AssignWifiComponentTeam(forceSpawnedItem, user); static void AssignWifiComponentTeam(Item item, Character? user) { if (user == null) { return; } foreach (WifiComponent wifiComponent in item.GetComponents()) { wifiComponent.TeamID = user.TeamID; } } } public static void RemoveItem(Item item) { if (IsInGame()) { Entity.Spawner?.AddItemToRemoveQueue(item); return; } item.Remove(); } public static bool IsInGame() => Screen.Selected is not { IsEditor: true }; public static bool IsCircuitBoxSelected(Character character) => character.SelectedItem?.GetComponent() is not null; } }