394 lines
18 KiB
C#
394 lines
18 KiB
C#
#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
|
|
{
|
|
/// <summary>
|
|
/// If the server needs to initialize the circuit box to the clients
|
|
/// instead of the clients loading it from the save file.
|
|
/// </summary>
|
|
private bool needsServerInitialization;
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="CircuitBoxInitializeStateFromServerEvent"/>.
|
|
/// </summary>
|
|
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<Client, bool>? 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<CircuitBoxEventData>(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<CircuitBoxAddComponentEvent>(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<CircuitBoxMoveComponentEvent>(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<CircuitBoxRemoveComponentEvent>(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<CircuitBoxSelectNodesEvent>(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<CircuitBoxSelectWiresEvent>(msg);
|
|
if (!item.CanClientAccess(c)) { break; }
|
|
|
|
SelectWiresInternal(data.TargetIDs, c.CharacterID, data.Overwrite);
|
|
BroadcastSelectionStatus();
|
|
break;
|
|
}
|
|
case CircuitBoxOpcode.AddWire:
|
|
{
|
|
var data = INetSerializableStruct.Read<CircuitBoxClientAddWireEvent>(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<CircuitBoxRemoveWireEvent>(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<CircuitBoxRenameLabelEvent>(msg);
|
|
if (!CanAccessAndUnlocked(c)) { break; }
|
|
|
|
RenameLabelInternal(data.LabelId, data.Color, data.NewHeader, data.NewBody);
|
|
CreateServerEvent(data);
|
|
break;
|
|
}
|
|
case CircuitBoxOpcode.AddLabel:
|
|
{
|
|
var data = INetSerializableStruct.Read<CircuitBoxAddLabelEvent>(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<CircuitBoxRemoveLabelEvent>(msg);
|
|
if (!CanAccessAndUnlocked(c)) { break; }
|
|
|
|
RemoveLabelInternal(data.TargetIDs);
|
|
CreateServerEvent(data);
|
|
break;
|
|
}
|
|
case CircuitBoxOpcode.ResizeLabel:
|
|
{
|
|
var data = INetSerializableStruct.Read<CircuitBoxResizeLabelEvent>(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<CircuitBoxRenameConnectionLabelsEvent>(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<ushort> ids)
|
|
{
|
|
if (ids.Count > 1) { return $"{ids.Count} components"; }
|
|
return Components.FirstOrDefault(comp => ids.Contains(comp.ID))?.Item.Name ?? "[UNKNOWN]";
|
|
}
|
|
|
|
string GetLogWireName(IReadOnlyList<ushort> 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) =>
|
|
!IsLocked() &&
|
|
item.CanClientAccess(client) &&
|
|
ClientHasRequiredItems(client);
|
|
|
|
bool ClientHasRequiredItems(Client client) =>
|
|
client.Character is { } chara && HasRequiredItems(chara, addMessage: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
}
|
|
} |