436 lines
15 KiB
C#
436 lines
15 KiB
C#
using System;
|
|
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable
|
|
{
|
|
const int MaxConnectionCount = 256;
|
|
public readonly List<Connection> Connections = new List<Connection>();
|
|
|
|
private Character user;
|
|
|
|
/// <summary>
|
|
/// Wires that have been disconnected from the panel, but not removed completely (visible at the bottom of the connection panel).
|
|
/// </summary>
|
|
public readonly HashSet<Wire> DisconnectedWires = new HashSet<Wire>();
|
|
|
|
private List<ushort> disconnectedWireIds;
|
|
|
|
/// <summary>
|
|
/// Allows rewiring the connection panel despite rewiring being disabled on a server
|
|
/// </summary>
|
|
public bool AlwaysAllowRewiring
|
|
{
|
|
get
|
|
{
|
|
if (item.Submarine == null) { return true; }
|
|
switch (item.Submarine.Info.Type)
|
|
{
|
|
case SubmarineType.Wreck:
|
|
case SubmarineType.BeaconStation:
|
|
case SubmarineType.EnemySubmarine:
|
|
case SubmarineType.Ruin:
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)]
|
|
public bool Locked
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public bool TemporarilyLocked
|
|
{
|
|
get { return Level.IsLoadedOutpost && (item.GetComponent<DockingPort>()?.Docked ?? false); }
|
|
}
|
|
|
|
//connection panels can't be deactivated externally (by signals or status effects)
|
|
public override bool IsActive
|
|
{
|
|
get { return base.IsActive; }
|
|
set { /*do nothing*/ }
|
|
}
|
|
|
|
public Character User
|
|
{
|
|
get { return user; }
|
|
}
|
|
|
|
public ConnectionPanel(Item item, ContentXElement element)
|
|
: base(item, element)
|
|
{
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
if (Connections.Count == MaxConnectionCount)
|
|
{
|
|
DebugConsole.ThrowError($"Too many connections in the item {item.Prefab.Identifier} (> {MaxConnectionCount}).");
|
|
break;
|
|
}
|
|
switch (subElement.Name.ToString())
|
|
{
|
|
case "input":
|
|
Connections.Add(new Connection(subElement, connectionIndex: Connections.Count, this, IdRemap.DiscardId, isItemSwap: false));
|
|
break;
|
|
case "output":
|
|
Connections.Add(new Connection(subElement, connectionIndex: Connections.Count, this, IdRemap.DiscardId, isItemSwap: false));
|
|
break;
|
|
}
|
|
}
|
|
|
|
base.IsActive = true;
|
|
InitProjSpecific();
|
|
}
|
|
|
|
partial void InitProjSpecific();
|
|
|
|
private bool linksInitialized;
|
|
public override void OnMapLoaded()
|
|
{
|
|
if (linksInitialized) { return; }
|
|
InitializeLinks();
|
|
}
|
|
|
|
public void InitializeLinks()
|
|
{
|
|
foreach (Connection c in Connections)
|
|
{
|
|
c.InitializeFromLoaded();
|
|
}
|
|
|
|
if (disconnectedWireIds != null)
|
|
{
|
|
foreach (ushort disconnectedWireId in disconnectedWireIds)
|
|
{
|
|
if (!(Entity.FindEntityByID(disconnectedWireId) is Item wireItem)) { continue; }
|
|
Wire wire = wireItem.GetComponent<Wire>();
|
|
if (wire != null)
|
|
{
|
|
if (Item.ItemList.Any(it => it != item && (it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false)))
|
|
{
|
|
if (wire.Item.body != null) { wire.Item.body.Enabled = false; }
|
|
wire.IsActive = false;
|
|
wire.UpdateSections();
|
|
}
|
|
DisconnectedWires.Add(wire);
|
|
base.IsActive = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
linksInitialized = true;
|
|
}
|
|
|
|
public override void OnItemLoaded()
|
|
{
|
|
if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic)
|
|
{
|
|
var holdable = item.GetComponent<Holdable>();
|
|
if (holdable == null || !holdable.Attachable)
|
|
{
|
|
DebugConsole.ThrowError("Item \"" + item.Name + "\" has a ConnectionPanel component," +
|
|
" but cannot be wired because it has an active physics body that cannot be attached to a wall." +
|
|
" Remove the physics body or add a Holdable component with the Attachable attribute set to true.");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void MoveConnectedWires(Vector2 amount)
|
|
{
|
|
Vector2 wireNodeOffset = item.Submarine == null ? Vector2.Zero : item.Submarine.HiddenSubPosition + amount;
|
|
foreach (Connection c in Connections)
|
|
{
|
|
// Use ToArray() snapshot for thread-safe iteration
|
|
foreach (Wire wire in c.Wires.ToArray())
|
|
{
|
|
if (wire == null) { continue; }
|
|
TryMoveWire(wire);
|
|
}
|
|
}
|
|
|
|
// Use ToList() snapshot for thread-safe iteration
|
|
foreach (var wire in DisconnectedWires.ToList())
|
|
{
|
|
TryMoveWire(wire);
|
|
}
|
|
|
|
void TryMoveWire(Wire wire)
|
|
{
|
|
#if CLIENT
|
|
if (wire.Item.IsSelected) { return; }
|
|
#endif
|
|
var wireNodes = wire.GetNodes();
|
|
if (wireNodes.Count == 0) { return; }
|
|
|
|
if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset))
|
|
{
|
|
wire.MoveNode(0, amount);
|
|
}
|
|
else if (Submarine.RectContains(item.Rect, wireNodes[wireNodes.Count - 1] + wireNodeOffset))
|
|
{
|
|
wire.MoveNode(wireNodes.Count - 1, amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
UpdateProjSpecific(deltaTime);
|
|
|
|
if (user == null || (user.SelectedItem != item && user.SelectedSecondaryItem != item))
|
|
{
|
|
#if SERVER
|
|
if (user != null) { item.CreateServerEvent(this); }
|
|
#endif
|
|
user = null;
|
|
if (DisconnectedWires.Count == 0) { base.IsActive = false; }
|
|
return;
|
|
}
|
|
|
|
if (!user.Enabled || !HasRequiredItems(user, addMessage: false))
|
|
{
|
|
user = null;
|
|
base.IsActive = false;
|
|
return;
|
|
}
|
|
|
|
user.AnimController.UpdateUseItem(!user.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * (((float)Timing.TotalTime / 10.0f) % 0.1f));
|
|
}
|
|
|
|
public override void UpdateBroken(float deltaTime, Camera cam)
|
|
{
|
|
Update(deltaTime, cam);
|
|
}
|
|
|
|
partial void UpdateProjSpecific(float deltaTime);
|
|
|
|
public bool CanRewire()
|
|
{
|
|
if (item.Container?.GetComponent<CircuitBox>() != null)
|
|
{
|
|
return true;
|
|
}
|
|
//attaching wires to items with a body is not allowed
|
|
//(signal items remove their bodies when attached to a wall)
|
|
if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public override bool Select(Character picker)
|
|
{
|
|
if (!CanRewire())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
user = picker;
|
|
#if SERVER
|
|
if (user != null) { item.CreateServerEvent(this); }
|
|
#endif
|
|
base.IsActive = true;
|
|
return true;
|
|
}
|
|
|
|
public override bool Use(float deltaTime, Character character = null)
|
|
{
|
|
if (character == null || character != user) { return false; }
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the character manages to succesfully rewire the panel, and if not, apply OnFailure effects
|
|
/// </summary>
|
|
public bool CheckCharacterSuccess(Character character)
|
|
{
|
|
if (character == null) { return false; }
|
|
//no electrocution in sub editor
|
|
if (Screen.Selected == GameMain.SubEditorScreen) { return true; }
|
|
|
|
var reactor = item.GetComponent<Reactor>();
|
|
if (reactor != null)
|
|
{
|
|
//reactors that arent generating power atm can be rewired without the risk of electrical shock
|
|
if (MathUtils.NearlyEqual(reactor.CurrPowerConsumption, 0.0f)) { return true; }
|
|
}
|
|
var powerContainer = item.GetComponent<PowerContainer>();
|
|
if (powerContainer != null)
|
|
{
|
|
//empty batteries/supercapacitors can be rewired without the risk of electrical shock
|
|
//non-empty ones always have a chance of zapping the user
|
|
if (powerContainer.Charge <= 0.0f) { return true; }
|
|
}
|
|
var powered = item.GetComponent<Powered>();
|
|
if (powered != null && powerContainer == null)
|
|
{
|
|
//unpowered panels can be rewired without the risk of electrical shock
|
|
if (powered.Voltage < 0.1f) { return true; }
|
|
}
|
|
|
|
float degreeOfSuccess = DegreeOfSuccess(character);
|
|
if (Rand.Range(0.0f, 0.5f) < degreeOfSuccess) { return true; }
|
|
|
|
ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
|
|
return false;
|
|
}
|
|
|
|
public override void Load(ContentXElement element, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
|
{
|
|
base.Load(element, usePrefabValues, idRemap, isItemSwap);
|
|
|
|
List<Connection> loadedConnections = new List<Connection>();
|
|
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
switch (subElement.Name.ToString())
|
|
{
|
|
case "input":
|
|
loadedConnections.Add(new Connection(subElement, connectionIndex: loadedConnections.Count, this, idRemap, isItemSwap));
|
|
break;
|
|
case "output":
|
|
loadedConnections.Add(new Connection(subElement, connectionIndex: loadedConnections.Count, this, idRemap, isItemSwap));
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++)
|
|
{
|
|
Connections[i].LoadedWires.Clear();
|
|
Connections[i].LoadedWires.AddRange(loadedConnections[i].LoadedWires);
|
|
}
|
|
|
|
disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty<ushort>()).ToList();
|
|
for (int i = 0; i < disconnectedWireIds.Count; i++)
|
|
{
|
|
disconnectedWireIds[i] = idRemap.GetOffsetId(disconnectedWireIds[i]);
|
|
}
|
|
}
|
|
|
|
public override XElement Save(XElement parentElement)
|
|
{
|
|
XElement componentElement = base.Save(parentElement);
|
|
|
|
foreach (Connection c in Connections)
|
|
{
|
|
c.Save(componentElement);
|
|
}
|
|
|
|
if (DisconnectedWires.Count > 0)
|
|
{
|
|
componentElement.Add(new XAttribute("disconnectedwires", string.Join(",", DisconnectedWires.Select(w => w.Item.ID))));
|
|
}
|
|
|
|
return componentElement;
|
|
}
|
|
|
|
protected override void ShallowRemoveComponentSpecific()
|
|
{
|
|
//do nothing
|
|
}
|
|
|
|
protected override void RemoveComponentSpecific()
|
|
{
|
|
base.RemoveComponentSpecific();
|
|
foreach (Wire wire in DisconnectedWires.ToList())
|
|
{
|
|
if (wire.OtherConnection(null) == null) //wire not connected to anything else
|
|
{
|
|
#if CLIENT
|
|
if (SubEditorScreen.IsSubEditor())
|
|
{
|
|
wire.Item.Remove();
|
|
}
|
|
else
|
|
{
|
|
wire.Item.Drop(null);
|
|
}
|
|
#else
|
|
wire.Item.Drop(null);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
DisconnectedWires.Clear();
|
|
foreach (Connection c in Connections)
|
|
{
|
|
foreach (Wire wire in c.Wires.ToArray())
|
|
{
|
|
if (wire.OtherConnection(c) == null) //wire not connected to anything else
|
|
{
|
|
#if CLIENT
|
|
if (SubEditorScreen.IsSubEditor())
|
|
{
|
|
wire.Item.Remove();
|
|
}
|
|
else
|
|
{
|
|
wire.Item.Drop(null);
|
|
}
|
|
#else
|
|
wire.Item.Drop(null);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
wire.RemoveConnection(item);
|
|
}
|
|
}
|
|
c.Grid = null;
|
|
}
|
|
foreach (var connection in Connections)
|
|
{
|
|
Powered.UnmarkConnectionChanged(connection);
|
|
connection.Recipients.Clear();
|
|
}
|
|
Connections.Clear();
|
|
|
|
#if CLIENT
|
|
rewireSoundChannel?.FadeOutAndDispose();
|
|
rewireSoundChannel = null;
|
|
#endif
|
|
}
|
|
|
|
public override void ReceiveSignal(Signal signal, Connection connection)
|
|
{
|
|
//do nothing
|
|
}
|
|
|
|
|
|
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
|
|
{
|
|
#if CLIENT
|
|
TriggerRewiringSound();
|
|
#endif
|
|
msg.WriteByte((byte)Connections.Count);
|
|
foreach (Connection connection in Connections)
|
|
{
|
|
// Use ToArray() snapshot for thread-safe iteration
|
|
var wiresSnapshot = connection.Wires.ToArray();
|
|
msg.WriteVariableUInt32((uint)wiresSnapshot.Length);
|
|
foreach (Wire wire in wiresSnapshot)
|
|
{
|
|
msg.WriteUInt16(wire?.Item == null ? (ushort)0 : wire.Item.ID);
|
|
}
|
|
}
|
|
|
|
// Use ToList() snapshot for thread-safe iteration
|
|
var disconnectedSnapshot = DisconnectedWires.ToList();
|
|
msg.WriteUInt16((ushort)disconnectedSnapshot.Count);
|
|
foreach (Wire disconnectedWire in disconnectedSnapshot)
|
|
{
|
|
msg.WriteUInt16(disconnectedWire.Item.ID);
|
|
}
|
|
}
|
|
}
|
|
}
|