using System; using System.Collections.Generic; using Content.Shared.GameObjects.EntitySystems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Power { /// /// Master class for group of , takes in and distributes power via nodes /// public class Powernet { public Powernet() { var EntitySystemManager = IoCManager.Resolve(); var powerSystem = EntitySystemManager.GetEntitySystem(); powerSystem.Powernets.Add(this); Uid = powerSystem.NewUid(); } /// /// Unique identifier per powernet, used for debugging mostly. /// [ViewVariables] public int Uid { get; } /// /// The entities that make up the powernet's physical location and allow powernet connection /// public readonly List WireList = new List(); /// /// Entities that connect directly to the powernet through above to add power or add power load /// public readonly List NodeList = new List(); /// /// Subset of nodelist that adds a continuous power supply to the network /// private readonly Dictionary GeneratorList = new Dictionary(); [ViewVariables] public int GeneratorCount => GeneratorList.Count; /// /// Subset of nodelist that draw power, stores information on current continuous powernet load /// private readonly SortedSet DeviceLoadList = new SortedSet(new DevicePriorityCompare()); [ViewVariables] public int DeviceCount => DeviceLoadList.Count; /// /// All the devices that have been depowered by this powernet or depowered prior to being absorted into this powernet /// private readonly List DepoweredDevices = new List(); /// /// A list of the energy storage components that will feed the powernet if necessary, and if there is enough power feed itself /// private readonly List PowerStorageSupplierList = new List(); [ViewVariables] public int PowerStorageSupplierCount => PowerStorageSupplierList.Count; /// /// A list of energy storage components that will never feed the powernet, will try to draw energy to feed themselves if possible /// private readonly List PowerStorageConsumerList = new List(); [ViewVariables] public int PowerStorageConsumerCount => PowerStorageConsumerList.Count; /// /// Static counter of all continuous load placed from devices on this power network. /// In Watts. /// [ViewVariables] public float Load { get; private set; } = 0; /// /// Static counter of all continuous supply from generators on this power network. /// In Watts. /// [ViewVariables] public float Supply { get; private set; } = 0; /// /// Variable that causes powernet to be regenerated from its wires during the next update cycle. /// [ViewVariables] public bool Dirty { get; set; } = false; // These are stats for power monitoring equipment such as APCs. /// /// The total supply that was available to us last tick. /// This does not mean it was used. /// [ViewVariables] public float LastTotalAvailable { get; private set; } /// /// The total power drawn last tick. /// This is how much power was actually, in practice, drawn. /// Not how much SHOULD have been drawn. /// If avail < demand, this will be just <= than the actual avail /// (e.g. if all machines need 100 W but there's 20 W excess, the 20 W will be avail but not drawn.) /// [ViewVariables] public float LastTotalDraw { get; private set; } /// /// The amount of power that was demanded last tick. /// This does not mean it was full filled in practice. /// This does not include the demand from storage suppliers until the suppliers are actually capable of drawing power. /// As such, this will quite abruptly shoot up if available rises to cover supplier charge demand too. /// /// [ViewVariables] public float LastTotalDemand { get; private set; } /// /// The amount of power that was demanded last tick, ALWAYS including storage supplier draw. /// This does not mean it was full filled in practice. /// See for the difference. /// [ViewVariables] public float LastTotalDemandWithSuppliers { get; private set; } /// /// The amount of power that we are lacking to properly power everything (excluding storage supplier charging). /// [ViewVariables] public float Lack => Math.Max(0, LastTotalDemand - LastTotalAvailable); /// /// The total amount of power that wasn't used last tick. /// This does not necessarily mean it went to waste, unused supply from storage is also counted. /// It is ALSO not implied that if this is > 0, that we have sufficient power for everything. /// See the doc comment on . /// [ViewVariables] public float Excess => Math.Max(0, LastTotalAvailable - LastTotalDraw); public void Update(float frameTime) { // The amount of energy that is supplied from generators that do not care if it's used or not. var activeSupply = Supply * frameTime; // The total load we need to fill for machines. var activeLoad = Load * frameTime; // The total load from storage consumers (batteries that do not supply like an SMES) float storageConsumerDemand = 0; foreach (var supply in PowerStorageConsumerList) { storageConsumerDemand += supply.RequestCharge(frameTime); } // The total supply from storage suppliers. float storageSupply = 0; // The total load from storage suppliers (batteries that DO supply like an SMES) float storageSupplierDemand = 0; foreach (var supply in PowerStorageSupplierList) { storageSupply += supply.AvailableCharge(frameTime); storageSupplierDemand += supply.RequestCharge(frameTime); } LastTotalAvailable = (storageSupply + activeSupply) / frameTime; LastTotalDemandWithSuppliers = (activeLoad + storageConsumerDemand + storageSupplierDemand) / frameTime; // The happy case. // If we have enough power to feed all load and storage demand, then feed everything if (activeSupply > activeLoad + storageConsumerDemand + storageSupplierDemand) { PowerAllDevices(); ChargeStorageConsumers(frameTime); ChargeStorageSuppliers(frameTime); LastTotalDraw = LastTotalDemand = LastTotalDemandWithSuppliers; return; } LastTotalDemand = (activeLoad + storageConsumerDemand) / frameTime; // We don't have enough power for the storage powernet suppliers, ignore powering them // TODO: This is technically incorrect, it's totally possible to power *some* suppliers here, // just not all. if (activeSupply > activeLoad + storageConsumerDemand) { PowerAllDevices(); ChargeStorageConsumers(frameTime); LastTotalDraw = LastTotalDemand; return; } // The complex case: There is too little power to power everything without using storage suppliers (SMES). // We have to keep track of power draw as to not incorrectly detract too much from storage suppliers. // Calculate the total potential supply, then go through every normal load and detract. var totalRemaining = activeSupply + storageSupply; foreach (var device in DeviceLoadList) { var deviceLoad = device.Load * frameTime; if (deviceLoad > totalRemaining) { device.ExternalPowered = false; DepoweredDevices.Add(device); } else { totalRemaining -= deviceLoad; if (!device.ExternalPowered) { DepoweredDevices.Remove(device); device.ExternalPowered = true; } } } if (totalRemaining > 0) { // What we have left (if any) goes into storage consumers. foreach (var consumer in PowerStorageConsumerList) { if (totalRemaining < 0) { break; } var demand = consumer.RequestCharge(frameTime); if (demand == 0) { continue; } var taken = Math.Min(demand, totalRemaining); totalRemaining -= taken; consumer.AddCharge(taken); } } LastTotalDraw = (activeSupply + storageSupply - totalRemaining) / frameTime; // activeSupply is free to use, but storageSupply is not. // Calculate how much of storageSupply, and deduct it from the storage suppliers. var supplierUsed = storageSupply - totalRemaining; // And deduct! foreach (var supplier in PowerStorageSupplierList) { var load = supplier.AvailableCharge(frameTime); if (load == 0) { continue; } var added = Math.Min(load, supplierUsed); supplierUsed -= added; supplier.DeductCharge(added); if (supplierUsed <= 0) { return; } } } private void PowerAllDevices() { foreach (var device in DepoweredDevices) { device.ExternalPowered = true; } DepoweredDevices.Clear(); } private void ChargeStorageConsumers(float frametime) { foreach (var storage in PowerStorageConsumerList) { storage.ChargePowerTick(frametime); } } private void ChargeStorageSuppliers(float frametime) { foreach (var storage in PowerStorageSupplierList) { storage.ChargePowerTick(frametime); } } /// /// Kills a powernet after it is marked dirty and its component have already been regenerated by the powernet system /// public void DirtyKill() { WireList.Clear(); while (NodeList.Count != 0) { NodeList[0].DisconnectFromPowernet(); } GeneratorList.Clear(); DeviceLoadList.Clear(); DepoweredDevices.Clear(); PowerStorageSupplierList.Clear(); PowerStorageConsumerList.Clear(); RemoveFromSystem(); } /// /// Combines two powernets when they connect via powertransfer components /// public void MergePowernets(Powernet toMerge) { //TODO: load balance reconciliation between powernets on merge tick here foreach (var wire in toMerge.WireList) { wire.Parent = this; } WireList.AddRange(toMerge.WireList); toMerge.WireList.Clear(); foreach (var node in toMerge.NodeList) { node.Parent = this; } NodeList.AddRange(toMerge.NodeList); toMerge.NodeList.Clear(); foreach (var generator in toMerge.GeneratorList) { GeneratorList.Add(generator.Key, generator.Value); } Supply += toMerge.Supply; toMerge.Supply = 0; toMerge.GeneratorList.Clear(); foreach (var device in toMerge.DeviceLoadList) { DeviceLoadList.Add(device); } Load += toMerge.Load; toMerge.Load = 0; toMerge.DeviceLoadList.Clear(); DepoweredDevices.AddRange(toMerge.DepoweredDevices); toMerge.DepoweredDevices.Clear(); PowerStorageSupplierList.AddRange(toMerge.PowerStorageSupplierList); toMerge.PowerStorageSupplierList.Clear(); PowerStorageConsumerList.AddRange(toMerge.PowerStorageConsumerList); toMerge.PowerStorageConsumerList.Clear(); toMerge.RemoveFromSystem(); } /// /// Removes reference from the powernets list on the powernet system /// private void RemoveFromSystem() { var EntitySystemManager = IoCManager.Resolve(); EntitySystemManager.GetEntitySystem().Powernets.Remove(this); } #region Registration /// /// Register a continuous load from a device connected to the powernet /// public void AddDevice(PowerDeviceComponent device) { DeviceLoadList.Add(device); Load += device.Load; if (!device.Powered) DepoweredDevices.Add(device); } /// /// Update one of the loads from a deviceconnected to the powernet /// public void UpdateDevice(PowerDeviceComponent device, float oldLoad) { if (DeviceLoadList.Contains(device)) { Load -= oldLoad; Load += device.Load; } } /// /// Returns whether or not a power device is in this powernet's load list. /// /// The device to check for. /// True if the device is in the load list, false otherwise. public bool HasDevice(PowerDeviceComponent device) { return DeviceLoadList.Contains(device); } /// /// Remove a continuous load from a device connected to the powernet /// public void RemoveDevice(PowerDeviceComponent device) { if (DeviceLoadList.Contains(device)) { Load -= device.Load; DeviceLoadList.Remove(device); if (DepoweredDevices.Contains(device)) DepoweredDevices.Remove(device); } else { Logger.WarningS("power", "We tried to remove device {0} twice from {1}, somehow.", device.Owner, this); } } /// /// Register a power supply from a generator connected to the powernet /// public void AddGenerator(PowerGeneratorComponent generator) { GeneratorList.Add(generator, generator.Supply); Supply += generator.Supply; } /// /// Update the value supplied from a generator connected to the powernet /// public void UpdateGenerator(PowerGeneratorComponent generator) { if (GeneratorList.ContainsKey(generator)) { Supply -= GeneratorList[generator]; GeneratorList[generator] = generator.Supply; Supply += generator.Supply; } } /// /// Remove a power supply from a generator connected to the powernet /// public void RemoveGenerator(PowerGeneratorComponent generator) { if (GeneratorList.ContainsKey(generator)) { Supply -= GeneratorList[generator]; GeneratorList.Remove(generator); } else { Logger.WarningS("power", "We tried to remove generator {0} twice from {1}, somehow.", generator.Owner, this); } } /// /// Register a power supply from a generator connected to the powernet /// public void AddPowerStorage(PowerStorageNetComponent storage) { if (storage.ChargePowernet) PowerStorageSupplierList.Add(storage); else PowerStorageConsumerList.Add(storage); } //How do I even call this? TODO: fix public void UpdateStorageType(PowerStorageNetComponent storage) { //If our chargepowernet settings change we need to tell the powernet of this new setting and remove traces of our old setting if (PowerStorageSupplierList.Contains(storage)) PowerStorageSupplierList.Remove(storage); if (PowerStorageConsumerList.Contains(storage)) PowerStorageConsumerList.Remove(storage); //Apply new setting if (storage.ChargePowernet) PowerStorageSupplierList.Add(storage); else PowerStorageConsumerList.Add(storage); } /// /// Remove a power supply from a generator connected to the powernet /// public void RemovePowerStorage(PowerStorageNetComponent storage) { if (PowerStorageSupplierList.Contains(storage)) { PowerStorageSupplierList.Remove(storage); } if (PowerStorageConsumerList.Contains(storage)) { PowerStorageSupplierList.Remove(storage); } } #endregion Registration public override string ToString() { return $"Powernet {Uid}"; } /// /// Priority that a device will receive power if powernet cannot supply every device /// public enum Priority { Necessary = 0, High = 1, Medium = 2, Low = 3, Provider = 4, Unnecessary = 5 } /// /// Comparer that keeps the device dictionary sorted by powernet priority /// public class DevicePriorityCompare : IComparer { public int Compare(PowerDeviceComponent x, PowerDeviceComponent y) { int compare = y.Priority.CompareTo(x.Priority); //If the comparer returns 0 sortedset will believe it is a duplicate and return 0, so return 1 instead if (compare == 0) { return y.Owner.Uid.CompareTo(x.Owner.Uid); } return compare; } } } }