#nullable enable using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; namespace Barotrauma.Items.Components { internal sealed partial class CircuitBox { /// /// If the server needs to initialize the circuit box to the clients /// instead of the clients loading it from the save file. /// private bool needsServerInitialization; /// /// When in multiplayer and the circuit box are loaded from the player inventory, /// We only load the components from XML on the server side /// since only the server has access to CharacterCampaignData /// and then send a network event syncing the loaded properties. /// But circuit box properties are too complex to /// sync using the existing syncing logic, /// so we instead send the state using . /// public void MarkServerRequiredInitialization() => needsServerInitialization = true; public partial void OnDeselected(Character c) { ClearAllSelectionsInternal(c.ID); BroadcastSelectionStatus(); } public void ServerRead(INetSerializableStruct data, Client c) { switch (data) { case NetCircuitBoxCursorInfo { RecordedPositions.Length: 10 } cursorInfo: { RelayCursorState(cursorInfo, c); break; } } } private void RelayCursorState(NetCircuitBoxCursorInfo data, Client sender) { if (GameMain.Server is null || !IsRoundRunning()) { return; } SendToAll(CircuitBoxOpcode.Cursor, data with { CharacterID = sender.CharacterID }, FilterClients); bool FilterClients(Client client) { // ReSharper disable once RedundantAssignment bool isSender = client == sender; #if DEBUG // Shown own cursor in debug builds isSender = false; #endif return !isSender && client.Character is not null && client.Character.SelectedItem == item; } } public void SendToClient(CircuitBoxOpcode opcode, INetSerializableStruct data, Client targetClient) { var (msg, deliveryMethod) = PrepareToSend(opcode, data); GameMain.Server?.ServerPeer?.Send(msg, targetClient.Connection, deliveryMethod); } public void SendToAll(CircuitBoxOpcode opcode, INetSerializableStruct data, Func? predicate = null) { var (msg, deliveryMethod) = PrepareToSend(opcode, data); foreach (Client client in GameMain.Server.ConnectedClients) { if (predicate is not null && !predicate(client)) { continue; } GameMain.Server?.ServerPeer?.Send(msg, client.Connection, deliveryMethod); } } private (IWriteMessage Message, DeliveryMethod DeliveryMethod) PrepareToSend(CircuitBoxOpcode opcode, INetSerializableStruct data) { IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.CIRCUITBOX); msg.WriteNetSerializableStruct(new NetCircuitBoxHeader( Opcode: opcode, ItemID: item.ID, ComponentIndex: (byte)item.GetComponentIndex(this))); msg.WriteNetSerializableStruct(data); DeliveryMethod deliveryMethod = UnrealiableOpcodes.Contains(opcode) ? DeliveryMethod.Unreliable : DeliveryMethod.Reliable; return (msg, deliveryMethod); } public void CreateServerEvent(INetSerializableStruct data) => item.CreateServerEvent(this, new CircuitBoxEventData(data)); public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null) { if (extraData is null) { return; } var eventData = ExtractEventData(extraData); msg.WriteByte((byte)eventData.Opcode); msg.WriteNetSerializableStruct(eventData.Data); } public void ServerEventRead(IReadMessage msg, Client c) { var header = (CircuitBoxOpcode)msg.ReadByte(); switch (header) { case CircuitBoxOpcode.AddComponent: { var data = INetSerializableStruct.Read(msg); if (!CanAccessAndUnlocked(c)) { break; } var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.PrefabIdentifier); if (prefab is null) { ThrowError("Unable to add component because the prefab was not found.", c); return; } if (IsFull || !GetApplicableResourcePlayerHas(prefab, c.Character).TryUnwrap(out var resource)) { return; } ushort id = ICircuitBoxIdentifiable.FindFreeID(Components); if (id is ICircuitBoxIdentifiable.NullComponentID) { ThrowError("Unable to add component because there are no available IDs left.", c); return; } bool result = AddComponentInternal(id, prefab, resource.Prefab, data.Position, c.Character, it => { CreateServerEvent(new CircuitBoxServerCreateComponentEvent(it.ID, resource.Prefab.UintIdentifier, id, data.Position)); }); if (!result) { ThrowError("Unable to add component because the component could not be created.", c); return; } GameServer.Log($"{NetworkMember.ClientLogName(c)} added a {prefab.Name} into a circuit box.", ServerLog.MessageType.Wiring); RemoveItem(resource); break; } case CircuitBoxOpcode.MoveComponent: { var data = INetSerializableStruct.Read(msg); if (!item.CanClientAccess(c)) { break; } MoveNodesInternal(data.TargetIDs, data.IOs, data.LabelIDs, data.MoveAmount); CreateServerEvent(data); break; } case CircuitBoxOpcode.DeleteComponent: { var data = INetSerializableStruct.Read(msg); if (!data.TargetIDs.Any() || !CanAccessAndUnlocked(c)) { break; } CreateRefundItemsForUsedResources(data.TargetIDs, c.Character); GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogComponentName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring); RemoveComponentInternal(data.TargetIDs); CreateServerEvent(data); break; } case CircuitBoxOpcode.SelectComponents: { var data = INetSerializableStruct.Read(msg); if (!item.CanClientAccess(c)) { break; } SelectComponentsInternal(data.TargetIDs, c.CharacterID, data.Overwrite); SelectInputOutputInternal(data.IOs, c.CharacterID, data.Overwrite); SelectLabelsInternal(data.LabelIDs, c.CharacterID, data.Overwrite); BroadcastSelectionStatus(); break; } case CircuitBoxOpcode.SelectWires: { var data = INetSerializableStruct.Read(msg); if (!item.CanClientAccess(c)) { break; } SelectWiresInternal(data.TargetIDs, c.CharacterID, data.Overwrite); BroadcastSelectionStatus(); break; } case CircuitBoxOpcode.AddWire: { var data = INetSerializableStruct.Read(msg); if (!CanAccessAndUnlocked(c)) { break; } var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.SelectedWirePrefabIdentifier); if (prefab is null) { ThrowError($"Unable to connect wire because wire by identifier \"{data.SelectedWirePrefabIdentifier}\" was not found.", c); break; } if (data.Start.FindConnection(this).TryUnwrap(out var start) && data.End.FindConnection(this).TryUnwrap(out var end)) { bool result = Connect(start, end, wire => { CreateServerEvent(new CircuitBoxServerCreateWireEvent(data with { Start = wire.Start, End = wire.End }, wire.ID, wire.Item.Select(static i => i.ID))); }, prefab); if (!result) { ThrowError("Unable to connect wire because the circuit box rejected it.", c); } GameServer.Log($"{NetworkMember.ClientLogName(c)} connected a wire from {start.Name} to {end.Name} in a circuit box.", ServerLog.MessageType.Wiring); } else { ThrowError($"Unable to connect wire because the start or end connection was not found. (start: {data.Start}, end: {data.End})", c); } break; } case CircuitBoxOpcode.RemoveWire: { var data = INetSerializableStruct.Read(msg); if (!data.TargetIDs.Any() || !CanAccessAndUnlocked(c)) { break; } GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogWireName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring); RemoveWireInternal(data.TargetIDs); CreateServerEvent(data); break; } case CircuitBoxOpcode.RenameLabel: { var data = INetSerializableStruct.Read(msg); if (!CanAccessAndUnlocked(c)) { break; } RenameLabelInternal(data.LabelId, data.Color, data.NewHeader, data.NewBody); CreateServerEvent(data); break; } case CircuitBoxOpcode.AddLabel: { var data = INetSerializableStruct.Read(msg); if (!CanAccessAndUnlocked(c)) { break; } ushort id = ICircuitBoxIdentifiable.FindFreeID(Labels); if (id is ICircuitBoxIdentifiable.NullComponentID) { ThrowError("Unable to add label because there are no available IDs left.", c); return; } AddLabelInternal(id, data.Color, data.Position, data.Header, data.Body); CreateServerEvent(new CircuitBoxServerAddLabelEvent(id, data.Position, new Vector2(256), data.Color, data.Header, data.Body)); break; } case CircuitBoxOpcode.RemoveLabel: { var data = INetSerializableStruct.Read(msg); if (!CanAccessAndUnlocked(c)) { break; } RemoveLabelInternal(data.TargetIDs); CreateServerEvent(data); break; } case CircuitBoxOpcode.ResizeLabel: { var data = INetSerializableStruct.Read(msg); if (!CanAccessAndUnlocked(c)) { break; } ResizeLabelInternal(data.ID, data.Position, data.Size); CreateServerEvent(data with { Size = Vector2.Max(data.Size, CircuitBoxLabelNode.MinSize) }); break; } case CircuitBoxOpcode.RenameConnections: { var data = INetSerializableStruct.Read(msg); if (!CanAccessAndUnlocked(c)) { break; } RenameConnectionLabelsInternal(data.Type, data.Override.ToDictionary()); CreateServerEvent(data); break; } default: throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); } string GetLogComponentName(IReadOnlyList ids) { if (ids.Count > 1) { return $"{ids.Count} components"; } return Components.FirstOrDefault(comp => ids.Contains(comp.ID))?.Item.Name ?? "[UNKNOWN]"; } string GetLogWireName(IReadOnlyList ids) { if (ids.Count > 1) { return $"{ids.Count} wires"; } if (Wires.FirstOrDefault(w => ids.Contains(w.ID)) is not { } wire) { return "[UNKNOWN]"; } return wire.BackingWire.TryUnwrap(out var backingWire) ? backingWire.Name : "a wire"; } bool CanAccessAndUnlocked(Client client) => item.CanClientAccess(client) && !Locked; } /// /// Creates an event that overrides the state of the circuit box for all clients. /// This is only required if the circuit box is loaded from the players inventory in multiplayer. /// public void CreateInitializationEvent() { Vector2 inputPos = Vector2.Zero, outputPos = Vector2.Zero; foreach (var ioNode in InputOutputNodes) { switch (ioNode.NodeType) { case CircuitBoxInputOutputNode.Type.Input: inputPos = ioNode.Position; break; case CircuitBoxInputOutputNode.Type.Output: outputPos = ioNode.Position; break; } } CircuitBoxInitializeStateFromServerEvent data = new( Components: Components.Select(EventFromComponent).ToImmutableArray(), Wires: Wires.Select(EventFromWire).ToImmutableArray(), Labels: Labels.Select(EventFromLabel).ToImmutableArray(), LabelOverrides: InputOutputNodes.Select(EventFromLabelOverride).ToImmutableArray(), InputPos: inputPos, OutputPos: outputPos); CreateServerEvent(data); static CircuitBoxServerCreateComponentEvent EventFromComponent(CircuitBoxComponent component) => new(component.Item.ID, component.UsedResource.UintIdentifier, component.ID, component.Position); static CircuitBoxServerCreateWireEvent EventFromWire(CircuitBoxWire wire) { var backingWire = wire.BackingWire.Select(static i => i.ID); var from = CircuitBoxConnectorIdentifier.FromConnection(wire.From); var to = CircuitBoxConnectorIdentifier.FromConnection(wire.To); var request = new CircuitBoxClientAddWireEvent(wire.Color, from, to, wire.UsedItemPrefab.UintIdentifier); return new CircuitBoxServerCreateWireEvent(request, wire.ID, backingWire); } static CircuitBoxServerAddLabelEvent EventFromLabel(CircuitBoxLabelNode label) => new(label.ID, label.Position, label.Size, label.Color, label.HeaderText, label.BodyText); static CircuitBoxRenameConnectionLabelsEvent EventFromLabelOverride(CircuitBoxInputOutputNode node) => new(node.NodeType, node.ConnectionLabelOverrides.ToNetDictionary()); } // we don't care about updating the view on server public partial void OnViewUpdateProjSpecific() { } private void ThrowError(string message, Client c) { DebugConsole.ThrowError(message, contentPackage: item.Prefab.ContentPackage); SendToClient(CircuitBoxOpcode.Error, new CircuitBoxErrorEvent(message), c); } private void BroadcastSelectionStatus() { var nodes = Components.Select(static c => new CircuitBoxIdSelectionPair(c.ID, c.IsSelected ? Option.Some(c.SelectedBy) : Option.None)).ToImmutableArray(); var wires = Wires.Select(static w => new CircuitBoxIdSelectionPair(w.ID, w.IsSelected ? Option.Some(w.SelectedBy) : Option.None)).ToImmutableArray(); var ios = InputOutputNodes.Select(static n => new CircuitBoxTypeSelectionPair(n.NodeType, n.IsSelected ? Option.Some(n.SelectedBy) : Option.None)).ToImmutableArray(); var labels = Labels.Select(static n => new CircuitBoxIdSelectionPair(n.ID, n.IsSelected ? Option.Some(n.SelectedBy) : Option.None)).ToImmutableArray(); CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios, labels)); } } }