using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class PowerTransfer : Powered { private static float fullPower; private static float fullLoad; private int updateCount; //affects how fast changes in power/load are carried over the grid static float inertia = 5.0f; private static HashSet connectedList = new HashSet(); private List powerConnections; public List PowerConnections { get { return powerConnections; } } private Dictionary connectionDirty = new Dictionary(); //a list of connections a given connection is connected to, either directly or via other power transfer components private Dictionary> connectedRecipients = new Dictionary>(); private float powerLoad; private bool isBroken; public float PowerLoad { get { return powerLoad; } } [Serialize(true, true), Editable(ToolTip = "Can the item be damaged if too much power is supplied to the power grid.")] public bool CanBeOverloaded { get; set; } [Serialize(2.0f, true), Editable(MinValueFloat = 1.0f, ToolTip = "How much power has to be supplied to the grid relative to the load before item starts taking damage. " +"E.g. a value of 2 means that the grid has to be receiving twice as much power as the devices in the grid are consuming.")] public float OverloadVoltage { get; set; } [Serialize(0.15f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, ToolTip = "The probability for a fire to start when the item breaks.")] public float FireProbability { get; set; } //can the component transfer power private bool canTransfer; public bool CanTransfer { get { return canTransfer; } set { if (canTransfer == value) return; canTransfer = value; SetAllConnectionsDirty(); } } public override bool IsActive { get { return base.IsActive; } set { if (base.IsActive == value) return; base.IsActive = value; powerLoad = 0.0f; currPowerConsumption = 0.0f; SetAllConnectionsDirty(); if (!base.IsActive) { //we need to refresh the connections here because Update won't be called on inactive components RefreshConnections(); } } } public PowerTransfer(Item item, XElement element) : base(item, element) { IsActive = true; canTransfer = true; InitProjectSpecific(element); } partial void InitProjectSpecific(XElement element); public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); if (!isBroken) { powerLoad = 0.0f; currPowerConsumption = 0.0f; SetAllConnectionsDirty(); RefreshConnections(); isBroken = true; } } public override void Update(float deltaTime, Camera cam) { RefreshConnections(); if (!CanTransfer) return; if (isBroken) { SetAllConnectionsDirty(); isBroken = false; } if (updateCount > 0) { //this junction box has already been updated this frame updateCount--; return; } //reset and recalculate the power generated/consumed //by the constructions connected to the grid fullPower = 0.0f; fullLoad = 0.0f; connectedList.Clear(); updateCount = 0; CheckJunctions(deltaTime); foreach (Powered p in connectedList) { PowerTransfer pt = p as PowerTransfer; if (pt == null || pt.updateCount == 0) continue; if (pt is RelayComponent != this is RelayComponent) continue; pt.powerLoad += (fullLoad - pt.powerLoad) / inertia; pt.currPowerConsumption += (-fullPower - pt.currPowerConsumption) / inertia; float voltage = fullPower / Math.Max(fullLoad, 1.0f); if (this is RelayComponent) { pt.currPowerConsumption = Math.Max(-fullLoad, pt.currPowerConsumption); voltage = Math.Min(voltage, 1.0f); } pt.Item.SendSignal(0, "", "power", null, voltage); pt.Item.SendSignal(0, "", "power_out", null, voltage); #if CLIENT //damage the item if voltage is too high //(except if running as a client) if (GameMain.Client != null) continue; #endif //items in a bad condition are more sensitive to overvoltage float maxOverVoltage = MathHelper.Lerp(OverloadVoltage * 0.75f, OverloadVoltage, item.Condition / item.MaxCondition); maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); //if the item can't be fixed, don't allow it to break if (!item.Repairables.Any() || !CanBeOverloaded) continue; //relays don't blow up if the power is higher than load, only if the output is high enough //(i.e. enough power passing through the relay) if (this is RelayComponent) continue; if (-pt.currPowerConsumption < Math.Max(pt.powerLoad, 200.0f) * maxOverVoltage) continue; float prevCondition = pt.item.Condition; pt.item.Condition -= deltaTime * 10.0f; if (pt.item.Condition <= 0.0f && prevCondition > 0.0f) { #if CLIENT if (sparkSounds.Count > 0) { var sparkSound = sparkSounds[Rand.Int(sparkSounds.Count)]; SoundPlayer.PlaySound(sparkSound.Sound, sparkSound.Volume, sparkSound.Range, pt.item.WorldPosition, pt.item.CurrentHull); } Vector2 baseVel = Rand.Vector(300.0f); for (int i = 0; i < 10; i++) { var particle = GameMain.ParticleManager.CreateParticle("spark", pt.item.WorldPosition, baseVel + Rand.Vector(100.0f), 0.0f, item.CurrentHull); if (particle != null) particle.Size *= Rand.Range(0.5f, 1.0f); } #endif float currentIntensity = GameMain.GameSession?.EventManager != null ? GameMain.GameSession.EventManager.CurrentIntensity : 0.5f; //higher probability for fires if the current intensity is low if (FireProbability > 0.0f && Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(FireProbability, FireProbability * 0.1f, currentIntensity)) { new FireSource(pt.item.WorldPosition); } } } updateCount = 0; } public override bool Pick(Character picker) { return picker != null; } private void RefreshConnections() { var connections = item.Connections; foreach (Connection c in connections) { if (!connectionDirty.ContainsKey(c)) { connectionDirty[c] = true; } else if (!connectionDirty[c]) { continue; } HashSet connected = new HashSet(); if (!connectedRecipients.ContainsKey(c)) { connectedRecipients.Add(c, connected); } else { //mark all previous recipients as dirty foreach (Connection recipient in connectedRecipients[c]) { var pt = recipient.Item.GetComponent(); if (pt != null) pt.connectionDirty[recipient] = true; } } //find all connections that are connected to this one (directly or via another PowerTransfer) connected.Add(c); GetConnected(c, connected); connectedRecipients[c] = connected; //go through all the PowerTransfers and we're connected to and set their connections to match the ones we just calculated //(no need to go through the recursive GetConnected method again) foreach (Connection recipient in connected) { var recipientPowerTransfer = recipient.Item.GetComponent(); if (recipientPowerTransfer == null) continue; if (!connectedRecipients.ContainsKey(recipient)) { connectedRecipients.Add(recipient, connected); } recipientPowerTransfer.connectedRecipients[recipient] = connected; recipientPowerTransfer.connectionDirty[recipient] = false; } } } //Finds all the connections that can receive a signal sent into the given connection and stores them in the hashset. private void GetConnected(Connection c, HashSet connected) { var recipients = c.Recipients; foreach (Connection recipient in recipients) { if (recipient == null || connected.Contains(recipient)) continue; Item it = recipient.Item; if (it == null || it.Condition <= 0.0f) continue; connected.Add(recipient); var powerTransfer = it.GetComponent(); if (powerTransfer != null && powerTransfer.CanTransfer && powerTransfer.IsActive) { GetConnected(recipient, connected); } } } //a recursive function that goes through all the junctions and adds up //all the generated/consumed power of the constructions connected to the grid private void CheckJunctions(float deltaTime, bool increaseUpdateCount = true, float clampPower = float.MaxValue, float clampLoad = float.MaxValue) { if (increaseUpdateCount) { updateCount = 1; } connectedList.Add(this); ApplyStatusEffects(ActionType.OnActive, deltaTime, null); //float maxPower = this is RelayComponent relayComponent ? relayComponent.MaxPower : float.PositiveInfinity; RelayComponent thisRelayComponent = this as RelayComponent; if (thisRelayComponent != null) { clampPower = Math.Min(Math.Min(clampPower, thisRelayComponent.MaxPower), powerLoad); clampLoad = Math.Min(clampLoad, thisRelayComponent.MaxPower); } foreach (Connection c in PowerConnections) { var recipients = c.Recipients; foreach (Connection recipient in recipients) { if (recipient?.Item == null) continue; Item it = recipient.Item; if (it.Condition <= 0.0f) continue; foreach (ItemComponent ic in it.Components) { if (!(ic is Powered powered) || !powered.IsActive) continue; if (connectedList.Contains(powered)) continue; if (powered is PowerTransfer powerTransfer) { RelayComponent otherRelayComponent = powerTransfer as RelayComponent; if ((thisRelayComponent == null) == (otherRelayComponent == null)) { if (!powerTransfer.CanTransfer) continue; powerTransfer.CheckJunctions(deltaTime, increaseUpdateCount, clampPower, clampLoad); } else { if (!powerTransfer.CanTransfer) continue; powerTransfer.CheckJunctions( deltaTime, false, (thisRelayComponent != null && c.IsOutput) ? 0.0f : clampPower, (thisRelayComponent != null && !c.IsOutput) ? 0.0f : clampLoad); } continue; } float addLoad = 0.0f; float addPower = 0.0f; if (powered is PowerContainer powerContainer) { if (recipient.Name == "power_in") { addLoad = powerContainer.CurrPowerConsumption; } else { addPower = powerContainer.CurrPowerOutput; } } else { connectedList.Add(powered); //positive power consumption = the construction requires power -> increase load if (powered.CurrPowerConsumption > 0.0f) { addLoad = powered.CurrPowerConsumption; } else if (powered.CurrPowerConsumption < 0.0f) //negative power consumption = the construction is a //generator/battery or another junction box { addPower -= powered.CurrPowerConsumption; } } if (addPower + fullPower > clampPower) { addPower -= (addPower + fullPower) - clampPower; }; if (addPower > 0) { fullPower += addPower; } if (addLoad + fullLoad > clampLoad) { addLoad -= (addLoad + fullLoad) - clampLoad; }; if (addLoad > 0) { fullLoad += addLoad; } } } } } public void SetAllConnectionsDirty() { if (item.Connections == null) return; foreach (Connection c in item.Connections) { connectionDirty[c] = true; } } public void SetConnectionDirty(Connection connection) { var connections = item.Connections; if (connections == null || !connections.Contains(connection)) return; connectionDirty[connection] = true; } public override void OnItemLoaded() { var connections = Item.Connections; powerConnections = connections == null ? new List() : connections.FindAll(c => c.IsPower); if (connections == null) { IsActive = false; return; } SetAllConnectionsDirty(); } public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f) { if (connection.IsPower) return; base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power); if (!connectedRecipients.ContainsKey(connection)) return; if (connection.Name.Length > 5 && connection.Name.Substring(0, 6) == "signal") { foreach (Connection recipient in connectedRecipients[connection]) { if (recipient.Item == item || recipient.Item == source) continue; foreach (ItemComponent ic in recipient.Item.Components) { //powertransfer components don't need to receive the signal in the pass-through signal connections //because we relay it straight to the connected items without going through the whole chain of junction boxes if (ic is PowerTransfer && connection.Name.Contains("signal")) continue; ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, 0.0f, signalStrength); } bool broken = recipient.Item.Condition <= 0.0f; foreach (StatusEffect effect in recipient.effects) { if (broken && effect.type != ActionType.OnBroken) continue; recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, null, null, false, false); } } } } } }