Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs
1 using System;
2 using Microsoft.Xna.Framework;
3 using System.Collections.Generic;
4 using System.Linq;
5 #if CLIENT
6 using Barotrauma.Sounds;
7 #endif
8 
10 {
14  public enum PowerPriority
15  {
16  Default = 0, // Use for status effects and/or extraload
17  Reactor = 1,
18  Relay = 3,
19  Battery = 5
20  }
21 
22  readonly struct PowerRange
23  {
24  public readonly static PowerRange Zero = default;
25 
26  public readonly float Min;
27  public readonly float Max;
28 
32  public readonly float ReactorMaxOutput;
33 
34  public PowerRange(float min, float max) : this(min, max, 0.0f)
35  {
36  }
37 
38  public PowerRange(float min, float max, float reactorMaxOutput)
39  {
40  System.Diagnostics.Debug.Assert(max >= min);
41  System.Diagnostics.Debug.Assert(min >= 0);
42  System.Diagnostics.Debug.Assert(max >= 0);
43  Min = min;
44  Max = max;
45  ReactorMaxOutput = reactorMaxOutput;
46  }
47 
49  {
50  return new PowerRange(a.Min + b.Min, a.Max + b.Max, a.ReactorMaxOutput + b.ReactorMaxOutput);
51  }
53  {
54  return new PowerRange(a.Min - b.Min, a.Max - b.Max, a.ReactorMaxOutput - b.ReactorMaxOutput);
55  }
56  }
57 
58  partial class Powered : ItemComponent
59  {
60  //TODO: test sparser update intervals?
61  protected const float UpdateInterval = (float)Timing.Step;
62 
66  private static readonly List<Powered> poweredList = new List<Powered>();
67  public static IEnumerable<Powered> PoweredList
68  {
69  get { return poweredList; }
70  }
71 
72  public static readonly HashSet<Connection> ChangedConnections = new HashSet<Connection>();
73 
74  public readonly static Dictionary<int, GridInfo> Grids = new Dictionary<int, GridInfo>();
75 
79  protected float currPowerConsumption;
80 
84  private float voltage;
85 
89  private float minVoltage;
90 
94  protected float powerConsumption;
95 
96  protected Connection powerIn, powerOut;
97 
101  protected const float MaxOverVoltageFactor = 2.0f;
102 
103  protected virtual PowerPriority Priority { get { return PowerPriority.Default; } }
104 
105  [Header(localizedTextTag: "sp.powered.propertyheader")]
106  [Editable, Serialize(0.5f, IsPropertySaveable.Yes, description: "The minimum voltage required for the device to function. " +
107  "The voltage is calculated as power / powerconsumption, meaning that a device " +
108  "with a power consumption of 1000 kW would need at least 500 kW of power to work if the minimum voltage is set to 0.5.")]
109  public float MinVoltage
110  {
111  get { return powerConsumption <= 0.0f ? 0.0f : minVoltage; }
112  set { minVoltage = value; }
113  }
114 
115  [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How much power the device draws (or attempts to draw) from the electrical grid when active.")]
116  public float PowerConsumption
117  {
118  get { return powerConsumption; }
119  set { powerConsumption = value; }
120  }
121 
122  [Serialize(false, IsPropertySaveable.Yes, description: "Is the device currently active. Inactive devices don't consume power.")]
123  public override bool IsActive
124  {
125  get { return base.IsActive; }
126  set
127  {
128  base.IsActive = value;
129  if (!value)
130  {
131  currPowerConsumption = 0.0f;
132  }
133  }
134  }
135 
136  [Serialize(0.0f, IsPropertySaveable.Yes, description: "The current power consumption of the device. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
137  public float CurrPowerConsumption
138  {
139  get {return currPowerConsumption; }
140  set { currPowerConsumption = value; }
141  }
142 
143  [Serialize(0.0f, IsPropertySaveable.Yes, description: "The current voltage of the item (calculated as power consumption / available power). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
144  public float Voltage
145  {
146  get
147  {
148  if (PoweredByTinkering)
149  {
150  return 1.0f;
151  }
152  else if (powerIn != null)
153  {
154  if (powerIn?.Grid != null) { return powerIn.Grid.Voltage; }
155  }
156  else if (powerOut != null)
157  {
158  if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; }
159  }
160  return PowerConsumption <= 0.0f ? 1.0f : voltage;
161  }
162  set
163  {
164  voltage = Math.Max(0.0f, value);
165  }
166  }
167 
172  public float RelativeVoltage => minVoltage <= 0.0f ? 1.0f : MathHelper.Clamp(Voltage / minVoltage, 0.0f, 1.0f);
173 
174  public bool PoweredByTinkering { get; set; }
175 
176  [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the item be damaged by electomagnetic pulses.")]
177  public bool VulnerableToEMP
178  {
179  get;
180  set;
181  }
182 
183  public Powered(Item item, ContentXElement element)
184  : base(item, element)
185  {
186  poweredList.Add(this);
187  InitProjectSpecific(element);
188  }
189 
190  partial void InitProjectSpecific(ContentXElement element);
191 
192  protected void UpdateOnActiveEffects(float deltaTime)
193  {
194  if (currPowerConsumption <= 0.0f && PowerConsumption <= 0.0f)
195  {
196  //if the item consumes no power, ignore the voltage requirement and
197  //apply OnActive statuseffects as long as this component is active
198  ApplyStatusEffects(ActionType.OnActive, deltaTime);
199  return;
200  }
201 
202  if (Voltage > minVoltage)
203  {
204  ApplyStatusEffects(ActionType.OnActive, deltaTime);
205  }
206 #if CLIENT
207  if (Voltage > minVoltage)
208  {
209  if (!powerOnSoundPlayed && powerOnSound != null)
210  {
211  SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling, freqMult: powerOnSound.GetRandomFrequencyMultiplier());
212  powerOnSoundPlayed = true;
213  }
214  }
215  else if (Voltage < 0.1f)
216  {
217  powerOnSoundPlayed = false;
218  }
219 #endif
220  }
221 
222  public override void Update(float deltaTime, Camera cam)
223  {
224  UpdateOnActiveEffects(deltaTime);
225  }
226 
227  public override void OnItemLoaded()
228  {
229  if (item.Connections == null) { return; }
230  foreach (Connection c in item.Connections)
231  {
232  if (!c.IsPower) { continue; }
233  if (this is PowerTransfer pt)
234  {
235  if (c.Name == "power_in")
236  {
237  powerIn = c;
238  }
239  else if (c.Name == "power_out")
240  {
241  powerOut = c;
242  // Connection takes the lowest priority
243  if (Priority > powerOut.Priority)
244  {
245  powerOut.Priority = Priority;
246  }
247  }
248  else if (c.Name == "power")
249  {
250  powerIn = powerOut = c;
251  }
252  }
253  else
254  {
255  if (c.IsOutput)
256  {
257  if (c.Name == "power_in")
258  {
259 #if DEBUG
260  DebugConsole.ThrowError($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.");
261 #else
262  DebugConsole.NewMessage($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.", Color.Orange);
263 #endif
264  }
265  powerOut = c;
266  // Connection takes the lowest priority
267  if (Priority > powerOut.Priority)
268  {
269  powerOut.Priority = Priority;
270  }
271  }
272  else
273  {
274  if (c.Name == "power_out")
275  {
276 #if DEBUG
277  DebugConsole.ThrowError($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.");
278 #else
279  DebugConsole.NewMessage($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.", Color.Orange);
280 #endif
281  }
282  powerIn = c;
283  }
284  }
285  }
286  }
287 
288 
293  public static void UpdateGrids(bool useCache = true)
294  {
295  //don't use cache if there are no existing grids
296  if (Grids.Count > 0 && useCache)
297  {
298  //delete all grids that were affected
299  foreach (Connection c in ChangedConnections)
300  {
301  if (c.Grid != null)
302  {
303  Grids.Remove(c.Grid.ID);
304  c.Grid = null;
305  }
306  }
307 
308  foreach (Connection c in ChangedConnections)
309  {
310  //Make sure the connection grid hasn't been resolved by another connection update
311  //Ensure the connection has other connections
312  if (c.Grid == null && c.Recipients.Count > 0 && c.Item.Condition > 0.0f)
313  {
314  GridInfo grid = PropagateGrid(c);
315  Grids[grid.ID] = grid;
316  }
317  }
318  }
319  else
320  {
321  //Clear all grid IDs from connections
322  foreach (Powered powered in poweredList)
323  {
324  //Only check devices with connectors
325  if (powered.powerIn != null)
326  {
327  powered.powerIn.Grid = null;
328  }
329  if (powered.powerOut != null)
330  {
331  powered.powerOut.Grid = null;
332  }
333  }
334 
335  Grids.Clear();
336 
337  foreach (Powered powered in poweredList)
338  {
339  //Probe through all connections that don't have a gridID
340  if (powered.powerIn != null && powered.powerIn.Grid == null && powered.powerIn != powered.powerOut && powered.Item.Condition > 0.0f)
341  {
342  // Only create grids for networks with more than 1 device
343  if (powered.powerIn.Recipients.Count > 0)
344  {
345  GridInfo grid = PropagateGrid(powered.powerIn);
346  Grids[grid.ID] = grid;
347  }
348  }
349 
350  if (powered.powerOut != null && powered.powerOut.Grid == null && powered.Item.Condition > 0.0f)
351  {
352  //Only create grids for networks with more than 1 device
353  if (powered.powerOut.Recipients.Count > 0)
354  {
355  GridInfo grid = PropagateGrid(powered.powerOut);
356  Grids[grid.ID] = grid;
357  }
358  }
359  }
360  }
361 
362  //Clear changed connections after each update
363  ChangedConnections.Clear();
364  }
365 
366  private static GridInfo PropagateGrid(Connection conn)
367  {
368  //Generate unique Key
369  int id = Rand.Int(int.MaxValue, Rand.RandSync.Unsynced);
370  while (Grids.ContainsKey(id))
371  {
372  id = Rand.Int(int.MaxValue, Rand.RandSync.Unsynced);
373  }
374 
375  return PropagateGrid(conn, id);
376  }
377 
378  private static GridInfo PropagateGrid(Connection conn, int gridID)
379  {
380  Stack<Connection> probeStack = new Stack<Connection>();
381 
382  GridInfo grid = new GridInfo(gridID);
383 
384  probeStack.Push(conn);
385 
386  //Non recursive approach to traversing connection tree
387  while (probeStack.Count > 0)
388  {
389  Connection c = probeStack.Pop();
390  c.Grid = grid;
391  grid.AddConnection(c);
392 
393  //Add on recipients
394  foreach (Connection otherC in c.Recipients)
395  {
396  //Only add valid connections
397  if (otherC.Grid != grid && (otherC.Grid == null || !Grids.ContainsKey(otherC.Grid.ID)) && ValidPowerConnection(c, otherC))
398  {
399  otherC.Grid = grid; //Assigning ID early prevents unncessary adding to stack
400  probeStack.Push(otherC);
401  }
402  }
403  }
404 
405  return grid;
406  }
407 
423  public static void UpdatePower(float deltaTime)
424  {
425  //Don't update the power if the round is ending
427  {
428  return;
429  }
430 
431  //Only update the power at the given update interval
432  /*
433  //Not use currently as update interval of 1/60
434  if (updateTimer > 0.0f)
435  {
436  updateTimer -= deltaTime;
437  return;
438  }
439  updateTimer = UpdateInterval;
440  */
441 
442 #if CLIENT
443  System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
444  sw.Start();
445 #endif
446  //Ensure all grids are updated correctly and have the correct connections
447  UpdateGrids();
448 
449 #if CLIENT
450  sw.Stop();
451  GameMain.PerformanceCounter.AddElapsedTicks("Update:Power", sw.ElapsedTicks);
452  sw.Restart();
453 #endif
454 
455  //Reset all grids
456  foreach (GridInfo grid in Grids.Values)
457  {
458  //Wipe priority groups as connections can change to not be outputting -- Can be improved caching wise --
459  grid.PowerSourceGroups.Clear();
460  grid.Power = 0;
461  grid.Load = 0;
462  }
463 
464  //Determine if devices are adding a load or providing power, also resolve solo nodes
465  foreach (Powered powered in poweredList)
466  {
467  //Make voltage decay to ensure the device powers down.
468  //This only effects devices with no power input (whose voltage is set by other means, e.g. status effects from a contained battery)
469  //or devices that have been disconnected from the power grid - other devices use the voltage of the grid instead.
470  powered.Voltage -= deltaTime;
471 
472  //Handle the device if it's got a power connection
473  if (powered.powerIn != null && powered.powerOut != powered.powerIn)
474  {
475  //Get the new load for the connection
476  float currLoad = powered.GetCurrentPowerConsumption(powered.powerIn);
477 
478  //If its a load update its grid load
479  if (currLoad >= 0)
480  {
481  if (powered.PoweredByTinkering) { currLoad = 0.0f; }
482  powered.CurrPowerConsumption = currLoad;
483  if (powered.powerIn.Grid != null)
484  {
485  powered.powerIn.Grid.Load += currLoad;
486  }
487  }
488  else if (powered.powerIn.Grid != null)
489  {
490  //If connected to a grid add as a source to be processed
491  powered.powerIn.Grid.AddSrc(powered.powerIn);
492  }
493  else
494  {
495  powered.CurrPowerConsumption = -powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0);
496  powered.GridResolved(powered.powerIn);
497  }
498  }
499 
500  //Handle the device power depending on if its powerout
501  if (powered.powerOut != null)
502  {
503  //Get the connection's load
504  float currLoad = powered.GetCurrentPowerConsumption(powered.powerOut);
505 
506  //Update the device's output load to the correct variable
507  if (powered is PowerTransfer pt)
508  {
509  pt.PowerLoad = currLoad;
510  }
511  else if (powered is PowerContainer pc)
512  {
513  // PowerContainer handle its own output value
514  }
515  else
516  {
517  powered.CurrPowerConsumption = currLoad;
518  }
519 
520  if (currLoad >= 0)
521  {
522  //Add to the grid load if possible
523  if (powered.powerOut.Grid != null)
524  {
525  powered.powerOut.Grid.Load += currLoad;
526  }
527  }
528  else if (powered.powerOut.Grid != null)
529  {
530  //Add connection as a source to be processed
531  powered.powerOut.Grid.AddSrc(powered.powerOut);
532  }
533  else
534  {
535  //Perform power calculations for the singular connection
536  float loadOut = -powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0);
537  if (powered is PowerTransfer pt2)
538  {
539  pt2.PowerLoad = loadOut;
540  }
541  else if (powered is PowerContainer pc)
542  {
543  //PowerContainer handles its own output value
544  }
545  else
546  {
547  powered.CurrPowerConsumption = loadOut;
548  }
549 
550  //Indicate grid is resolved as it was the only device
551  powered.GridResolved(powered.powerOut);
552  }
553  }
554  }
555 
556  //Iterate through all grids to determine the power on the grid
557  foreach (GridInfo grid in Grids.Values)
558  {
559  //Iterate through the priority src groups lowest first
560  foreach (PowerSourceGroup scrGroup in grid.PowerSourceGroups.Values)
561  {
562  scrGroup.MinMaxPower = PowerRange.Zero;
563 
564  //Iterate through all connections in the group to get their minmax power and sum them
565  foreach (Connection c in scrGroup.Connections)
566  {
567  foreach (var device in c.Item.GetComponents<Powered>())
568  {
569  scrGroup.MinMaxPower += device.MinMaxPowerOut(c, grid.Load);
570  }
571  }
572 
573  //Iterate through all connections to get their final power out provided the min max information
574  float addedPower = 0;
575  foreach (Connection c in scrGroup.Connections)
576  {
577  foreach (var device in c.Item.GetComponents<Powered>())
578  {
579  addedPower += device.GetConnectionPowerOut(c, grid.Power, scrGroup.MinMaxPower, grid.Load);
580  }
581  }
582 
583  //Add the power to the grid
584  grid.Power += addedPower;
585  }
586 
587  //Calculate Grid voltage, limit between 0 - 1000
588  float newVoltage = MathHelper.Min(grid.Power / MathHelper.Max(grid.Load, 1E-10f), 1000);
589  if (float.IsNegative(newVoltage))
590  {
591  newVoltage = 0.0f;
592  }
593 
594  grid.Voltage = newVoltage;
595 
596  //Iterate through all connections on that grid and run their gridResolved function
597  foreach (Connection c in grid.Connections)
598  {
599  foreach (var device in c.Item.GetComponents<Powered>())
600  {
601  device?.GridResolved(c);
602  }
603  }
604  }
605 
606 #if CLIENT
607  sw.Stop();
608  GameMain.PerformanceCounter.AddElapsedTicks("Update:Power", sw.ElapsedTicks);
609 #endif
610  }
611 
616  public virtual float GetCurrentPowerConsumption(Connection connection = null)
617  {
618  // If a handheld device there is no consumption
619  if (powerIn == null && powerOut == null)
620  {
621  return 0;
622  }
623 
624  // Add extraload for PowerTransfer devices
625  if (this is PowerTransfer pt)
626  {
627  return PowerConsumption + pt.ExtraLoad;
628  }
629  else if (connection != this.powerIn || !IsActive)
630  {
631  //If not the power in connection or is inactive there is no draw
632  return 0;
633  }
634 
635  //Otherwise return the max powerconsumption of the device
636  return PowerConsumption;
637  }
638 
644  public virtual PowerRange MinMaxPowerOut(Connection conn, float load = 0)
645  {
646  return PowerRange.Zero;
647  }
648 
656  public virtual float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load)
657  {
658  return conn == powerOut ? MathHelper.Max(-CurrPowerConsumption, 0) : 0;
659  }
660 
664  public virtual void GridResolved(Connection conn) { }
665 
666  public static bool ValidPowerConnection(Connection conn1, Connection conn2)
667  {
668  return
669  conn1.IsPower && conn2.IsPower &&
670  conn1.Item.Condition > 0.0f && conn2.Item.Condition > 0.0f &&
671  (conn1.Item.HasTag(Tags.JunctionBox) || conn2.Item.HasTag(Tags.JunctionBox) || conn1.Item.HasTag(Tags.DockingPort) || conn2.Item.HasTag(Tags.DockingPort) || conn1.IsOutput != conn2.IsOutput);
672  }
673 
678  {
679  if (item.Connections == null || powerIn == null) { return 0.0f; }
680  float availablePower = 0.0f;
681  var recipients = powerIn.Recipients;
682  foreach (Connection recipient in recipients)
683  {
684  if (!recipient.IsPower || !recipient.IsOutput) { continue; }
685  var battery = recipient.Item?.GetComponent<PowerContainer>();
686  if (battery == null || battery.Item.Condition <= 0.0f) { continue; }
687  if (battery.OutputDisabled) { continue; }
688  float maxOutputPerFrame = battery.MaxOutPut / 60.0f;
689  float framesPerMinute = 3600.0f;
690  availablePower += Math.Min(battery.Charge * framesPerMinute, maxOutputPerFrame);
691  }
692  return availablePower;
693  }
694 
695  protected IEnumerable<PowerContainer> GetDirectlyConnectedBatteries()
696  {
697  if (item.Connections != null && powerIn != null)
698  {
699  foreach (Connection recipient in powerIn.Recipients)
700  {
701  if (!recipient.IsPower || !recipient.IsOutput) { continue; }
702  if (recipient.Item?.GetComponent<PowerContainer>() is PowerContainer battery)
703  {
704  yield return battery;
705  }
706  }
707  }
708  }
709 
710  protected override void RemoveComponentSpecific()
711  {
712  //Flag power connections to be updated
713  if (item.Connections != null)
714  {
715  foreach (Connection c in item.Connections)
716  {
717  if (c.IsPower && c.Grid != null)
718  {
719  ChangedConnections.Add(c);
720  }
721  }
722  }
723 
724  base.RemoveComponentSpecific();
725  poweredList.Remove(this);
726  }
727  }
728 
729  partial class GridInfo
730  {
731  public readonly int ID;
732  public float Voltage = 0;
733  public float Load = 0;
734  public float Power = 0;
735 
736  public readonly List<Connection> Connections = new List<Connection>();
737  public readonly SortedList<PowerPriority, PowerSourceGroup> PowerSourceGroups = new SortedList<PowerPriority, PowerSourceGroup>();
738 
739  public GridInfo(int id)
740  {
741  ID = id;
742  }
743 
745  {
746  Connections.Remove(c);
747 
748  //Remove the grid if it has no devices
749  if (Connections.Count == 0 && Powered.Grids.ContainsKey(ID))
750  {
751  Powered.Grids.Remove(ID);
752  }
753  }
754 
755  public void AddConnection(Connection c)
756  {
757  Connections.Add(c);
758  }
759 
760  public void AddSrc(Connection c)
761  {
762  if (PowerSourceGroups.ContainsKey(c.Priority))
763  {
764  PowerSourceGroups[c.Priority].Connections.Add(c);
765  }
766  else
767  {
768  PowerSourceGroup group = new PowerSourceGroup();
769  group.Connections.Add(c);
770  PowerSourceGroups[c.Priority] = group;
771  }
772  }
773  }
774 
775  partial class PowerSourceGroup
776  {
778  public readonly List<Connection> Connections = new List<Connection>();
779 
781  {
782  }
783  }
784 }
static GameSession?? GameSession
Definition: GameMain.cs:88
static PerformanceCounter PerformanceCounter
Definition: GameMain.cs:37
readonly SortedList< PowerPriority, PowerSourceGroup > PowerSourceGroups
The base class for components holding the different functionalities of the item
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb targetLimb=null, Entity useTarget=null, Character user=null, Vector2? worldPosition=null, float afflictionMultiplier=1.0f)
const float MaxOverVoltageFactor
Maximum voltage factor when the device is being overvolted. I.e. how many times more effectively the ...
float RelativeVoltage
Essentially Voltage / MinVoltage (= how much of the minimum required voltage has been satisfied),...
virtual float GetCurrentPowerConsumption(Connection connection=null)
Current power consumption of the device (or amount of generated power if negative)
virtual PowerRange MinMaxPowerOut(Connection conn, float load=0)
Minimum and maximum power the connection can provide
float powerConsumption
The maximum amount of power the item can draw from connected items
virtual void GridResolved(Connection conn)
Can be overridden to perform updates for the device after the connected grid has resolved its power c...
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
static void UpdateGrids(bool useCache=true)
Allocate electrical devices into their grids based on connections
float currPowerConsumption
The amount of power currently consumed by the item. Negative values mean that the item is providing p...
float GetAvailableInstantaneousBatteryPower()
Returns the amount of power that can be supplied by batteries directly connected to the item
virtual float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load)
Finalize how much power the device will be outputting to the connection
static void UpdatePower(float deltaTime)
Update the power calculations of all devices and grids Updates grids in the order of ConnCurrConsumpt...
void AddElapsedTicks(string identifier, long ticks)
readonly float Range
Definition: RoundSound.cs:14
float GetRandomFrequencyMultiplier()
Definition: RoundSound.cs:54
readonly float Volume
Definition: RoundSound.cs:13
readonly bool IgnoreMuffling
Definition: RoundSound.cs:17
PowerPriority
Order in which power sources will provide to a grid, lower number is higher priority
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
readonly float ReactorMaxOutput
Used by reactors to communicate their maximum output to each other so they can divide the grid load b...