diff --git a/Content.Client/Content.Client.csproj.DotSettings b/Content.Client/Content.Client.csproj.DotSettings
new file mode 100644
index 0000000000..a558236f55
--- /dev/null
+++ b/Content.Client/Content.Client.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ True
\ No newline at end of file
diff --git a/Content.Client/Doors/AirlockVisualizer.cs b/Content.Client/Doors/AirlockVisualizer.cs
index 4f6832b7b6..8f6c0651b1 100644
--- a/Content.Client/Doors/AirlockVisualizer.cs
+++ b/Content.Client/Doors/AirlockVisualizer.cs
@@ -1,5 +1,6 @@
using System;
using Content.Client.Wires;
+using Content.Client.Wires.Visualizers;
using Content.Shared.Audio;
using Content.Shared.Doors;
using JetBrains.Annotations;
diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 9b19afe561..b3a76fb612 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -55,7 +55,7 @@ namespace Content.Client.Entry
"AccessReader",
"IdCardConsole",
"Airlock",
- "WirePlacer",
+ "CablePlacer",
"Drink",
"Food",
"FoodContainer",
@@ -73,6 +73,7 @@ namespace Content.Client.Entry
"StorageFill",
"Mop",
"Bucket",
+ "CableVis",
"Puddle",
"CanSpill",
"SpeedLoader",
@@ -105,12 +106,11 @@ namespace Content.Client.Entry
"PowerSupplier",
"PowerConsumer",
"Battery",
- "BatteryStorage",
"BatteryDischarger",
"Apc",
"PowerProvider",
- "PowerReceiver",
- "Wire",
+ "ApcPowerReceiver",
+ "Cable",
"StressTestMovement",
"Toys",
"SurgeryTool",
@@ -273,6 +273,8 @@ namespace Content.Client.Entry
"ExplosionLaunched",
"BeingCloned",
"Advertise",
+ "PowerNetworkBattery",
+ "BatteryCharger",
};
}
}
diff --git a/Content.Client/Light/Visualizers/EmergencyLightVisualizer.cs b/Content.Client/Light/Visualizers/EmergencyLightVisualizer.cs
new file mode 100644
index 0000000000..4c7ed5db08
--- /dev/null
+++ b/Content.Client/Light/Visualizers/EmergencyLightVisualizer.cs
@@ -0,0 +1,23 @@
+using Content.Shared.Light.Component;
+using Robust.Client.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Client.Light.Visualizers
+{
+ [DataDefinition]
+ public sealed class EmergencyLightVisualizer : AppearanceVisualizer
+ {
+ public override void OnChangeData(AppearanceComponent component)
+ {
+ base.OnChangeData(component);
+
+ if (!component.Owner.TryGetComponent(out SpriteComponent? sprite))
+ return;
+
+ if (!component.TryGetData(EmergencyLightVisuals.On, out bool on))
+ on = false;
+
+ sprite.LayerSetState(0, on ? "emergency_light_on" : "emergency_light_off");
+ }
+ }
+}
diff --git a/Content.Client/NodeContainer/NodeGroupSystem.cs b/Content.Client/NodeContainer/NodeGroupSystem.cs
new file mode 100644
index 0000000000..4cb7d678c4
--- /dev/null
+++ b/Content.Client/NodeContainer/NodeGroupSystem.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Linq;
+using Content.Shared.NodeContainer;
+using JetBrains.Annotations;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+
+namespace Content.Client.NodeContainer
+{
+ [UsedImplicitly]
+ public sealed class NodeGroupSystem : EntitySystem
+ {
+ [Dependency] private readonly IOverlayManager _overlayManager = default!;
+ [Dependency] private readonly IEntityLookup _entityLookup = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+
+ public bool VisEnabled { get; private set; }
+
+ public Dictionary Groups { get; } = new();
+ public HashSet Filtered { get; } = new();
+
+ public Dictionary
+ Entities { get; private set; } = new();
+
+ public Dictionary<(int group, int node), NodeVis.NodeDatum> NodeLookup { get; private set; } = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeNetworkEvent(DataMsgHandler);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+
+ _overlayManager.RemoveOverlay();
+ }
+
+ private void DataMsgHandler(NodeVis.MsgData ev)
+ {
+ if (!VisEnabled)
+ return;
+
+ foreach (var deletion in ev.GroupDeletions)
+ {
+ Groups.Remove(deletion);
+ }
+
+ foreach (var group in ev.Groups)
+ {
+ Groups.Add(group.NetId, group);
+ }
+
+ Entities = Groups.Values
+ .SelectMany(g => g.Nodes, (data, nodeData) => (data, nodeData))
+ .GroupBy(n => n.nodeData.Entity)
+ .ToDictionary(g => g.Key, g => g.ToArray());
+
+ NodeLookup = Groups.Values
+ .SelectMany(g => g.Nodes, (data, nodeData) => (data, nodeData))
+ .ToDictionary(n => (n.data.NetId, n.nodeData.NetId), n => n.nodeData);
+ }
+
+ public void SetVisEnabled(bool enabled)
+ {
+ VisEnabled = enabled;
+
+ RaiseNetworkEvent(new NodeVis.MsgEnable(enabled));
+
+ if (enabled)
+ {
+ var overlay = new NodeVisualizationOverlay(
+ this,
+ _entityLookup,
+ _mapManager,
+ _inputManager,
+ _eyeManager,
+ _resourceCache,
+ EntityManager);
+
+ _overlayManager.AddOverlay(overlay);
+ }
+ else
+ {
+ Groups.Clear();
+ Entities.Clear();
+ }
+ }
+ }
+}
diff --git a/Content.Client/NodeContainer/NodeVisCommand.cs b/Content.Client/NodeContainer/NodeVisCommand.cs
new file mode 100644
index 0000000000..c6a95fce5b
--- /dev/null
+++ b/Content.Client/NodeContainer/NodeVisCommand.cs
@@ -0,0 +1,56 @@
+using Content.Client.Administration.Managers;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Client.NodeContainer
+{
+ public sealed class NodeVisCommand : IConsoleCommand
+ {
+ public string Command => "nodevis";
+ public string Description => "Toggles node group visualization";
+ public string Help => "";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var adminMan = IoCManager.Resolve();
+ if (!adminMan.HasFlag(AdminFlags.Debug))
+ {
+ shell.WriteError("You need +DEBUG for this command");
+ return;
+ }
+
+ var sys = EntitySystem.Get();
+ sys.SetVisEnabled(!sys.VisEnabled);
+ }
+ }
+
+ public sealed class NodeVisFilterCommand : IConsoleCommand
+ {
+ public string Command => "nodevisfilter";
+ public string Description => "Toggles showing a specific group on nodevis";
+ public string Help => "Usage: nodevis [filter]\nOmit filter to list currently masked-off";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var sys = EntitySystem.Get();
+
+ if (args.Length == 0)
+ {
+ foreach (var filtered in sys.Filtered)
+ {
+ shell.WriteLine(filtered);
+ }
+ }
+ else
+ {
+ var filter = args[0];
+ if (!sys.Filtered.Add(filter))
+ {
+ sys.Filtered.Remove(filter);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Client/NodeContainer/NodeVisualizationOverlay.cs b/Content.Client/NodeContainer/NodeVisualizationOverlay.cs
new file mode 100644
index 0000000000..989780f3c8
--- /dev/null
+++ b/Content.Client/NodeContainer/NodeVisualizationOverlay.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Content.Client.Resources;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Enums;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using static Content.Shared.NodeContainer.NodeVis;
+
+namespace Content.Client.NodeContainer
+{
+ public sealed class NodeVisualizationOverlay : Overlay
+ {
+ private readonly NodeGroupSystem _system;
+ private readonly IEntityLookup _lookup;
+ private readonly IMapManager _mapManager;
+ private readonly IInputManager _inputManager;
+ private readonly IEyeManager _eyeManager;
+ private readonly IEntityManager _entityManager;
+
+ private readonly Dictionary<(int, int), NodeRenderData> _nodeIndex = new();
+ private readonly Dictionary>> _gridIndex = new ();
+
+ private readonly Font _font;
+
+ private (int group, int node)? _hovered;
+ private float _time;
+
+ public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
+
+ public NodeVisualizationOverlay(
+ NodeGroupSystem system,
+ IEntityLookup lookup,
+ IMapManager mapManager,
+ IInputManager inputManager,
+ IEyeManager eyeManager,
+ IResourceCache cache,
+ IEntityManager entityManager)
+ {
+ _system = system;
+ _lookup = lookup;
+ _mapManager = mapManager;
+ _inputManager = inputManager;
+ _eyeManager = eyeManager;
+ _entityManager = entityManager;
+
+ _font = cache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12);
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if ((args.Space & OverlaySpace.WorldSpace) != 0)
+ {
+ DrawWorld(args);
+ }
+ else if ((args.Space & OverlaySpace.ScreenSpace) != 0)
+ {
+ DrawScreen(args);
+ }
+ }
+
+ private void DrawScreen(in OverlayDrawArgs args)
+ {
+ if (_hovered == null)
+ return;
+
+ var (groupId, nodeId) = _hovered.Value;
+
+ var group = _system.Groups[groupId];
+ var node = _system.NodeLookup[(groupId, nodeId)];
+
+ var mousePos = _inputManager.MouseScreenPosition.Position;
+
+ var entity = _entityManager.GetEntity(node.Entity);
+
+ var gridId = entity.Transform.GridID;
+ var grid = _mapManager.GetGrid(gridId);
+ var gridTile = grid.TileIndicesFor(entity.Transform.Coordinates);
+
+ var sb = new StringBuilder();
+ sb.Append($"entity: {entity}\n");
+ sb.Append($"group id: {group.GroupId}\n");
+ sb.Append($"node: {node.Name}\n");
+ sb.Append($"type: {node.Type}\n");
+ sb.Append($"grid pos: {gridTile}\n");
+
+ args.ScreenHandle.DrawString(_font, mousePos + (20, -20), sb.ToString());
+ }
+
+ private void DrawWorld(in OverlayDrawArgs overlayDrawArgs)
+ {
+ const float nodeSize = 8f / 32;
+ const float nodeOffset = 6f / 32;
+
+ var handle = overlayDrawArgs.WorldHandle;
+
+ var map = overlayDrawArgs.Viewport.Eye?.Position.MapId ?? default;
+ if (map == MapId.Nullspace)
+ return;
+
+ var mouseScreenPos = _inputManager.MouseScreenPosition;
+ var mouseWorldPos = _eyeManager.ScreenToMap(mouseScreenPos).Position;
+
+ _hovered = default;
+
+ var cursorBox = Box2.CenteredAround(mouseWorldPos, (nodeSize, nodeSize));
+
+ // Group visible nodes by grid tiles.
+ var worldBounds = overlayDrawArgs.WorldBounds;
+ _lookup.FastEntitiesIntersecting(map, ref worldBounds, entity =>
+ {
+ if (!_system.Entities.TryGetValue(entity.Uid, out var nodeData))
+ return;
+
+ var gridId = entity.Transform.GridID;
+ var grid = _mapManager.GetGrid(gridId);
+ var gridDict = _gridIndex.GetOrNew(gridId);
+ var coords = entity.Transform.Coordinates;
+
+ // TODO: This probably shouldn't be capable of returning NaN...
+ if (float.IsNaN(coords.Position.X) || float.IsNaN(coords.Position.Y))
+ return;
+
+ var tile = gridDict.GetOrNew(grid.TileIndicesFor(coords));
+
+ foreach (var (group, nodeDatum) in nodeData)
+ {
+ if (!_system.Filtered.Contains(group.GroupId))
+ {
+ tile.Add((group, nodeDatum));
+ }
+ }
+ });
+
+ foreach (var (gridId, gridDict) in _gridIndex)
+ {
+ var grid = _mapManager.GetGrid(gridId);
+ foreach (var (pos, list) in gridDict)
+ {
+ var centerPos = grid.GridTileToWorld(pos).Position;
+ list.Sort(NodeDisplayComparer.Instance);
+
+ var offset = -(list.Count - 1) * nodeOffset / 2;
+
+ foreach (var (group, node) in list)
+ {
+ var nodePos = centerPos + (offset, offset);
+ if (cursorBox.Contains(nodePos))
+ _hovered = (group.NetId, node.NetId);
+
+ _nodeIndex[(group.NetId, node.NetId)] = new NodeRenderData(group, node, nodePos);
+ offset += nodeOffset;
+ }
+ }
+ }
+
+ foreach (var nodeRenderData in _nodeIndex.Values)
+ {
+ var pos = nodeRenderData.WorldPos;
+ var bounds = Box2.CenteredAround(pos, (nodeSize, nodeSize));
+
+ var groupData = nodeRenderData.GroupData;
+ var color = groupData.Color;
+
+ if (!_hovered.HasValue)
+ color.A = 0.5f;
+ else if (_hovered.Value.group != groupData.NetId)
+ color.A = 0.2f;
+ else
+ color.A = 0.75f + MathF.Sin(_time * 4) * 0.25f;
+
+ handle.DrawRect(bounds, color);
+
+ foreach (var reachable in nodeRenderData.NodeDatum.Reachable)
+ {
+ if (_nodeIndex.TryGetValue((groupData.NetId, reachable), out var reachDat))
+ {
+ handle.DrawLine(pos, reachDat.WorldPos, color);
+ }
+ }
+ }
+
+ _nodeIndex.Clear();
+ _gridIndex.Clear();
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ _time += args.DeltaSeconds;
+ }
+
+ private sealed class NodeDisplayComparer : IComparer<(GroupData, NodeDatum)>
+ {
+ public static readonly NodeDisplayComparer Instance = new();
+
+ public int Compare((GroupData, NodeDatum) x, (GroupData, NodeDatum) y)
+ {
+ var (groupX, nodeX) = x;
+ var (groupY, nodeY) = y;
+
+ var cmp = groupX.NetId.CompareTo(groupY.NetId);
+ if (cmp != 0)
+ return cmp;
+
+ return nodeX.NetId.CompareTo(nodeY.NetId);
+ }
+ }
+
+ private sealed class NodeRenderData
+ {
+ public GroupData GroupData;
+ public NodeDatum NodeDatum;
+ public Vector2 WorldPos;
+
+ public NodeRenderData(GroupData groupData, NodeDatum nodeDatum, Vector2 worldPos)
+ {
+ GroupData = groupData;
+ NodeDatum = nodeDatum;
+ WorldPos = worldPos;
+ }
+ }
+ }
+}
diff --git a/Content.Client/APC/ApcBoundUserInterface.cs b/Content.Client/Power/APC/ApcBoundUserInterface.cs
similarity index 99%
rename from Content.Client/APC/ApcBoundUserInterface.cs
rename to Content.Client/Power/APC/ApcBoundUserInterface.cs
index 567bb842b8..0df4379d78 100644
--- a/Content.Client/APC/ApcBoundUserInterface.cs
+++ b/Content.Client/Power/APC/ApcBoundUserInterface.cs
@@ -9,7 +9,7 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
-namespace Content.Client.APC
+namespace Content.Client.Power.APC
{
[UsedImplicitly]
public class ApcBoundUserInterface : BoundUserInterface
diff --git a/Content.Client/APC/ApcVisualizer.cs b/Content.Client/Power/APC/ApcVisualizer.cs
similarity index 98%
rename from Content.Client/APC/ApcVisualizer.cs
rename to Content.Client/Power/APC/ApcVisualizer.cs
index a112f55b42..85ed0e5d25 100644
--- a/Content.Client/APC/ApcVisualizer.cs
+++ b/Content.Client/Power/APC/ApcVisualizer.cs
@@ -3,7 +3,7 @@ using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
-namespace Content.Client.APC
+namespace Content.Client.Power.APC
{
public class ApcVisualizer : AppearanceVisualizer
{
diff --git a/Content.Client/SMES/SmesVisualizer.cs b/Content.Client/Power/SMES/SmesVisualizer.cs
similarity index 98%
rename from Content.Client/SMES/SmesVisualizer.cs
rename to Content.Client/Power/SMES/SmesVisualizer.cs
index 0937cfb3ad..11fcbd41d5 100644
--- a/Content.Client/SMES/SmesVisualizer.cs
+++ b/Content.Client/Power/SMES/SmesVisualizer.cs
@@ -4,7 +4,7 @@ using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
-namespace Content.Client.SMES
+namespace Content.Client.Power.SMES
{
[UsedImplicitly]
public class SmesVisualizer : AppearanceVisualizer
diff --git a/Content.Client/Power/Visualizers/CableVisualizer.cs b/Content.Client/Power/Visualizers/CableVisualizer.cs
new file mode 100644
index 0000000000..b5c09c7ebc
--- /dev/null
+++ b/Content.Client/Power/Visualizers/CableVisualizer.cs
@@ -0,0 +1,26 @@
+using Content.Shared.Wires;
+using Robust.Client.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Client.Power
+{
+ [DataDefinition]
+ public sealed class CableVisualizer : AppearanceVisualizer
+ {
+ [DataField("base")]
+ public string? StateBase;
+
+ public override void OnChangeData(AppearanceComponent component)
+ {
+ base.OnChangeData(component);
+
+ if (!component.Owner.TryGetComponent(out SpriteComponent? sprite))
+ return;
+
+ if (!component.TryGetData(WireVisVisuals.ConnectedMask, out WireVisDirFlags mask))
+ mask = WireVisDirFlags.None;
+
+ sprite.LayerSetState(0, $"{StateBase}{(int) mask}");
+ }
+ }
+}
diff --git a/Content.Client/Power/PowerDeviceVisualizer.cs b/Content.Client/Power/Visualizers/PowerDeviceVisualizer.cs
similarity index 100%
rename from Content.Client/Power/PowerDeviceVisualizer.cs
rename to Content.Client/Power/Visualizers/PowerDeviceVisualizer.cs
diff --git a/Content.Client/Stack/StackVisualizer.cs b/Content.Client/Stack/StackVisualizer.cs
index 3b0197ef93..7324bd1b8a 100644
--- a/Content.Client/Stack/StackVisualizer.cs
+++ b/Content.Client/Stack/StackVisualizer.cs
@@ -66,7 +66,7 @@ namespace Content.Client.Stack
///
///
/// -
- /// false: they are opaque and mutually exclusive (e.g. sprites in a wire coil). Default value
+ /// false: they are opaque and mutually exclusive (e.g. sprites in a cable coil). Default value
///
/// -
/// true: they are transparent and thus layered one over another in ascending order first
diff --git a/Content.Client/Wires/WiresVisualizer.cs b/Content.Client/Wires/Visualizers/WiresVisualizer.cs
similarity index 95%
rename from Content.Client/Wires/WiresVisualizer.cs
rename to Content.Client/Wires/Visualizers/WiresVisualizer.cs
index e20fd1cfa4..2ad300e436 100644
--- a/Content.Client/Wires/WiresVisualizer.cs
+++ b/Content.Client/Wires/Visualizers/WiresVisualizer.cs
@@ -1,7 +1,7 @@
using Robust.Client.GameObjects;
using static Content.Shared.Wires.SharedWiresComponent;
-namespace Content.Client.Wires
+namespace Content.Client.Wires.Visualizers
{
public class WiresVisualizer : AppearanceVisualizer
{
diff --git a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs
index 6f4b5f5555..a75d936594 100644
--- a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs
+++ b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs
@@ -85,7 +85,7 @@ namespace Content.IntegrationTests.Tests.Disposal
components:
- type: DisposalUnit
- type: Anchorable
- - type: PowerReceiver
+ - type: ApcPowerReceiver
- type: Physics
bodyType: Static
@@ -155,7 +155,7 @@ namespace Content.IntegrationTests.Tests.Disposal
Flush(unit, false, human, wrench);
// Remove power need
- Assert.True(disposalUnit.TryGetComponent(out PowerReceiverComponent? power));
+ Assert.True(disposalUnit.TryGetComponent(out ApcPowerReceiverComponent? power));
power!.NeedsPower = false;
Assert.True(unit.Powered);
diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs
index b2812c5478..6e5bbb5469 100644
--- a/Content.IntegrationTests/Tests/EntityTest.cs
+++ b/Content.IntegrationTests/Tests/EntityTest.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using Content.Server.Battery.Components;
+using Content.Server.Power.Components;
using Content.Server.PowerCell.Components;
using Content.Shared.CCVar;
using Content.Shared.Coordinates;
diff --git a/Content.IntegrationTests/Tests/GravityGridTest.cs b/Content.IntegrationTests/Tests/GravityGridTest.cs
index 078d69762a..69d8c63581 100644
--- a/Content.IntegrationTests/Tests/GravityGridTest.cs
+++ b/Content.IntegrationTests/Tests/GravityGridTest.cs
@@ -22,7 +22,7 @@ namespace Content.IntegrationTests.Tests
id: GravityGeneratorDummy
components:
- type: GravityGenerator
- - type: PowerReceiver
+ - type: ApcPowerReceiver
";
[Test]
public async Task Test()
@@ -48,9 +48,9 @@ namespace Content.IntegrationTests.Tests
generator = entityMan.SpawnEntity("GravityGeneratorDummy", grid2.ToCoordinates());
Assert.That(generator.HasComponent());
- Assert.That(generator.HasComponent());
+ Assert.That(generator.HasComponent());
var generatorComponent = generator.GetComponent();
- var powerComponent = generator.GetComponent();
+ var powerComponent = generator.GetComponent();
Assert.That(generatorComponent.Status, Is.EqualTo(GravityGeneratorStatus.Unpowered));
powerComponent.NeedsPower = false;
});
diff --git a/Content.IntegrationTests/Tests/Power/PowerTest.cs b/Content.IntegrationTests/Tests/Power/PowerTest.cs
new file mode 100644
index 0000000000..b8453821f3
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Power/PowerTest.cs
@@ -0,0 +1,996 @@
+#nullable enable
+using System.Threading.Tasks;
+using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
+using Content.Server.Power.Components;
+using Content.Server.Power.Nodes;
+using Content.Shared.Coordinates;
+using NUnit.Framework;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Timing;
+
+namespace Content.IntegrationTests.Tests.Power
+{
+ [Parallelizable(ParallelScope.Fixtures)]
+ [TestFixture]
+ public class PowerTest : ContentIntegrationTest
+ {
+ private const string Prototypes = @"
+- type: entity
+ id: GeneratorDummy
+ components:
+ - type: NodeContainer
+ nodes:
+ output:
+ !type:CableDeviceNode
+ nodeGroupID: HVPower
+ - type: PowerSupplier
+ - type: Transform
+ anchored: true
+
+- type: entity
+ id: ConsumerDummy
+ components:
+ - type: Transform
+ anchored: true
+ - type: NodeContainer
+ nodes:
+ input:
+ !type:CableDeviceNode
+ nodeGroupID: HVPower
+ - type: PowerConsumer
+
+- type: entity
+ id: ChargingBatteryDummy
+ components:
+ - type: Transform
+ anchored: true
+ - type: NodeContainer
+ nodes:
+ output:
+ !type:CableDeviceNode
+ nodeGroupID: HVPower
+ - type: PowerNetworkBattery
+ - type: Battery
+ - type: BatteryCharger
+
+- type: entity
+ id: DischargingBatteryDummy
+ components:
+ - type: Transform
+ anchored: true
+ - type: NodeContainer
+ nodes:
+ output:
+ !type:CableDeviceNode
+ nodeGroupID: HVPower
+ - type: PowerNetworkBattery
+ - type: Battery
+ - type: BatteryDischarger
+
+- type: entity
+ id: FullBatteryDummy
+ components:
+ - type: Transform
+ anchored: true
+ - type: NodeContainer
+ nodes:
+ output:
+ !type:CableDeviceNode
+ nodeGroupID: HVPower
+ input:
+ !type:CableTerminalPortNode
+ nodeGroupID: HVPower
+ - type: PowerNetworkBattery
+ - type: Battery
+ - type: BatteryDischarger
+ node: output
+ - type: BatteryCharger
+ node: input
+
+- type: entity
+ id: SubstationDummy
+ components:
+ - type: NodeContainer
+ nodes:
+ input:
+ !type:CableDeviceNode
+ nodeGroupID: HVPower
+ output:
+ !type:CableDeviceNode
+ nodeGroupID: MVPower
+ - type: BatteryCharger
+ voltage: High
+ - type: BatteryDischarger
+ voltage: Medium
+ - type: PowerNetworkBattery
+ maxChargeRate: 1000
+ maxSupply: 1000
+ supplyRampTolerance: 1000
+ - type: Battery
+ maxCharge: 1000
+ startingCharge: 1000
+ - type: Transform
+ anchored: true
+
+- type: entity
+ id: ApcDummy
+ components:
+ - type: Battery
+ maxCharge: 10000
+ startingCharge: 10000
+ - type: PowerNetworkBattery
+ maxChargeRate: 1000
+ maxSupply: 1000
+ supplyRampTolerance: 1000
+ - type: BatteryCharger
+ voltage: Medium
+ - type: BatteryDischarger
+ voltage: Apc
+ - type: Apc
+ voltage: Apc
+ - type: NodeContainer
+ nodes:
+ input:
+ !type:CableDeviceNode
+ nodeGroupID: MVPower
+ output:
+ !type:CableDeviceNode
+ nodeGroupID: Apc
+ - type: Transform
+ anchored: true
+ - type: UserInterface
+ interfaces:
+ - key: enum.ApcUiKey.Key
+ type: ApcBoundUserInterface
+ - type: AccessReader
+ access: [['Engineering']]
+
+- type: entity
+ id: ApcPowerReceiverDummy
+ components:
+ - type: ApcPowerReceiver
+ - type: Transform
+ anchored: true
+";
+
+ private ServerIntegrationInstance _server = default!;
+ private IMapManager _mapManager = default!;
+ private IEntityManager _entityManager = default!;
+ private IGameTiming _gameTiming = default!;
+
+ [OneTimeSetUp]
+ public async Task Setup()
+ {
+ var options = new ServerIntegrationOptions {ExtraPrototypes = Prototypes};
+ _server = StartServerDummyTicker(options);
+
+ await _server.WaitIdleAsync();
+ _mapManager = _server.ResolveDependency();
+ _entityManager = _server.ResolveDependency();
+ _gameTiming = _server.ResolveDependency();
+ }
+
+ ///
+ /// Test small power net with a simple surplus of power over the loads.
+ ///
+ [Test]
+ public async Task TestSimpleSurplus()
+ {
+ const float loadPower = 200;
+ PowerSupplierComponent supplier = default!;
+ PowerConsumerComponent consumer1 = default!;
+ PowerConsumerComponent consumer2 = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 3; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
+ }
+
+ var generatorEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
+ var consumerEnt1 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 1));
+ var consumerEnt2 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2));
+
+ supplier = generatorEnt.GetComponent();
+ consumer1 = consumerEnt1.GetComponent();
+ consumer2 = consumerEnt2.GetComponent();
+
+ // Plenty of surplus and tolerance
+ supplier.MaxSupply = loadPower * 4;
+ supplier.SupplyRampTolerance = loadPower * 4;
+ consumer1.DrawRate = loadPower;
+ consumer2.DrawRate = loadPower;
+ });
+
+ _server.RunTicks(1); //let run a tick for PowerNet to process power
+
+ _server.Assert(() =>
+ {
+ // Assert both consumers fully powered
+ Assert.That(consumer1.ReceivedPower, Is.EqualTo(consumer1.DrawRate).Within(0.1));
+ Assert.That(consumer2.ReceivedPower, Is.EqualTo(consumer2.DrawRate).Within(0.1));
+
+ // Assert that load adds up on supply.
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(loadPower * 2).Within(0.1));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+
+ ///
+ /// Test small power net with a simple deficit of power over the loads.
+ ///
+ [Test]
+ public async Task TestSimpleDeficit()
+ {
+ const float loadPower = 200;
+ PowerSupplierComponent supplier = default!;
+ PowerConsumerComponent consumer1 = default!;
+ PowerConsumerComponent consumer2 = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 3; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
+ }
+
+ var generatorEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
+ var consumerEnt1 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 1));
+ var consumerEnt2 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2));
+
+ supplier = generatorEnt.GetComponent();
+ consumer1 = consumerEnt1.GetComponent();
+ consumer2 = consumerEnt2.GetComponent();
+
+ // Too little supply, both consumers should get 33% power.
+ supplier.MaxSupply = loadPower;
+ supplier.SupplyRampTolerance = loadPower;
+ consumer1.DrawRate = loadPower;
+ consumer2.DrawRate = loadPower * 2;
+ });
+
+ _server.RunTicks(1); //let run a tick for PowerNet to process power
+
+ _server.Assert(() =>
+ {
+ // Assert both consumers get 33% power.
+ Assert.That(consumer1.ReceivedPower, Is.EqualTo(consumer1.DrawRate / 3).Within(0.1));
+ Assert.That(consumer2.ReceivedPower, Is.EqualTo(consumer2.DrawRate / 3).Within(0.1));
+
+ // Supply should be maxed out
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task TestSupplyRamp()
+ {
+ PowerSupplierComponent supplier = default!;
+ PowerConsumerComponent consumer = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 3; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
+ }
+
+ var generatorEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
+ var consumerEnt = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2));
+
+ supplier = generatorEnt.GetComponent();
+ consumer = consumerEnt.GetComponent();
+
+ // Supply has enough total power but needs to ramp up to match.
+ supplier.MaxSupply = 400;
+ supplier.SupplyRampRate = 400;
+ supplier.SupplyRampTolerance = 100;
+ consumer.DrawRate = 400;
+ });
+
+ // Exact values can/will be off by a tick, add tolerance for that.
+ var tickRate = (float) _gameTiming.TickPeriod.TotalSeconds;
+ var tickDev = 400 * tickRate * 1.1f;
+
+ _server.RunTicks(1);
+
+ _server.Assert(() =>
+ {
+ // First tick, supply should be delivering 100 W (max tolerance) and start ramping up.
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(100).Within(0.1));
+ Assert.That(consumer.ReceivedPower, Is.EqualTo(100).Within(0.1));
+ });
+
+ _server.RunTicks(14);
+
+ _server.Assert(() =>
+ {
+ // After 15 ticks (0.25 seconds), supply ramp pos should be at 100 W and supply at 100, approx.
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(200).Within(tickDev));
+ Assert.That(supplier.SupplyRampPosition, Is.EqualTo(100).Within(tickDev));
+ Assert.That(consumer.ReceivedPower, Is.EqualTo(200).Within(tickDev));
+ });
+
+ _server.RunTicks(45);
+
+ _server.Assert(() =>
+ {
+ // After 1 second total, ramp should be at 400 and supply should be at 400, everybody happy.
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(400).Within(tickDev));
+ Assert.That(supplier.SupplyRampPosition, Is.EqualTo(400).Within(tickDev));
+ Assert.That(consumer.ReceivedPower, Is.EqualTo(400).Within(tickDev));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task TestBatteryRamp()
+ {
+ const float startingCharge = 100_000;
+
+ PowerNetworkBatteryComponent netBattery = default!;
+ BatteryComponent battery = default!;
+ PowerConsumerComponent consumer = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 3; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
+ }
+
+ var generatorEnt = _entityManager.SpawnEntity("DischargingBatteryDummy", grid.ToCoordinates());
+ var consumerEnt = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2));
+
+ netBattery = generatorEnt.GetComponent();
+ battery = generatorEnt.GetComponent();
+ consumer = consumerEnt.GetComponent();
+
+ battery.MaxCharge = startingCharge;
+ battery.CurrentCharge = startingCharge;
+ netBattery.MaxSupply = 400;
+ netBattery.SupplyRampRate = 400;
+ netBattery.SupplyRampTolerance = 100;
+ consumer.DrawRate = 400;
+ });
+
+ // Exact values can/will be off by a tick, add tolerance for that.
+ var tickRate = (float) _gameTiming.TickPeriod.TotalSeconds;
+ var tickDev = 400 * tickRate * 1.1f;
+
+ _server.RunTicks(1);
+
+ _server.Assert(() =>
+ {
+ // First tick, supply should be delivering 100 W (max tolerance) and start ramping up.
+ Assert.That(netBattery.CurrentSupply, Is.EqualTo(100).Within(0.1));
+ Assert.That(consumer.ReceivedPower, Is.EqualTo(100).Within(0.1));
+ });
+
+ _server.RunTicks(14);
+
+ _server.Assert(() =>
+ {
+ // After 15 ticks (0.25 seconds), supply ramp pos should be at 100 W and supply at 100, approx.
+ Assert.That(netBattery.CurrentSupply, Is.EqualTo(200).Within(tickDev));
+ Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(100).Within(tickDev));
+ Assert.That(consumer.ReceivedPower, Is.EqualTo(200).Within(tickDev));
+
+ // Trivial integral to calculate expected power spent.
+ const double spentExpected = (200 + 100) / 2.0 * 0.25;
+ Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
+ });
+
+ _server.RunTicks(45);
+
+ _server.Assert(() =>
+ {
+ // After 1 second total, ramp should be at 400 and supply should be at 400, everybody happy.
+ Assert.That(netBattery.CurrentSupply, Is.EqualTo(400).Within(tickDev));
+ Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(tickDev));
+ Assert.That(consumer.ReceivedPower, Is.EqualTo(400).Within(tickDev));
+
+ // Trivial integral to calculate expected power spent.
+ const double spentExpected = (400 + 100) / 2.0 * 0.75 + 400 * 0.25;
+ Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+
+ [Test]
+ public async Task TestSimpleBatteryChargeDeficit()
+ {
+ PowerSupplierComponent supplier = default!;
+ BatteryComponent battery = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 3; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
+ }
+
+ var generatorEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
+ var batteryEnt = _entityManager.SpawnEntity("ChargingBatteryDummy", grid.ToCoordinates(0, 2));
+
+ supplier = generatorEnt.GetComponent();
+ var netBattery = batteryEnt.GetComponent();
+ battery = batteryEnt.GetComponent();
+
+ supplier.MaxSupply = 500;
+ supplier.SupplyRampTolerance = 500;
+ battery.MaxCharge = 100000;
+ netBattery.MaxChargeRate = 1000;
+ netBattery.Efficiency = 0.5f;
+ });
+
+ _server.RunTicks(30); // 60 TPS, 0.5 seconds
+
+ _server.Assert(() =>
+ {
+ // half a second @ 500 W = 250
+ // 50% efficiency, so 125 J stored total.
+ Assert.That(battery.CurrentCharge, Is.EqualTo(125).Within(0.1));
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(500).Within(0.1));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task TestFullBattery()
+ {
+ PowerConsumerComponent consumer = default!;
+ PowerSupplierComponent supplier = default!;
+ PowerNetworkBatteryComponent netBattery = default!;
+ BatteryComponent battery = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 4; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
+ }
+
+ var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1));
+ terminal.Transform.LocalRotation = Angle.FromDegrees(180);
+
+ var batteryEnt = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2));
+ var supplyEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0));
+ var consumerEnt = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 3));
+
+ consumer = consumerEnt.GetComponent();
+ supplier = supplyEnt.GetComponent();
+ netBattery = batteryEnt.GetComponent();
+ battery = batteryEnt.GetComponent();
+
+ // Consumer needs 1000 W, supplier can only provide 800, battery fills in the remaining 200.
+ consumer.DrawRate = 1000;
+ supplier.MaxSupply = 800;
+ supplier.SupplyRampTolerance = 800;
+
+ netBattery.MaxSupply = 400;
+ netBattery.SupplyRampTolerance = 400;
+ netBattery.SupplyRampRate = 100_000;
+ battery.MaxCharge = 100_000;
+ battery.CurrentCharge = 100_000;
+ });
+
+ // Run some ticks so everything is stable.
+ _server.RunTicks(60);
+
+ // Exact values can/will be off by a tick, add tolerance for that.
+ var tickRate = (float) _gameTiming.TickPeriod.TotalSeconds;
+ var tickDev = 400 * tickRate * 1.1f;
+
+ _server.Assert(() =>
+ {
+ Assert.That(consumer.ReceivedPower, Is.EqualTo(consumer.DrawRate).Within(0.1));
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1));
+
+ // Battery's current supply includes passed-through power from the supply.
+ // Assert ramp position is correct to make sure it's only supplying 200 W for real.
+ Assert.That(netBattery.CurrentSupply, Is.EqualTo(1000).Within(0.1));
+ Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(200).Within(0.1));
+
+ const int expectedSpent = 200;
+ Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task TestFullBatteryEfficiencyPassThrough()
+ {
+ PowerConsumerComponent consumer = default!;
+ PowerSupplierComponent supplier = default!;
+ PowerNetworkBatteryComponent netBattery = default!;
+ BatteryComponent battery = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 4; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
+ }
+
+ var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1));
+ terminal.Transform.LocalRotation = Angle.FromDegrees(180);
+
+ var batteryEnt = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2));
+ var supplyEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0));
+ var consumerEnt = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 3));
+
+ consumer = consumerEnt.GetComponent();
+ supplier = supplyEnt.GetComponent();
+ netBattery = batteryEnt.GetComponent();
+ battery = batteryEnt.GetComponent();
+
+ // Consumer needs 1000 W, supply and battery can only provide 400 each.
+ // BUT the battery has 50% input efficiency, so 50% of the power of the supply gets lost.
+ consumer.DrawRate = 1000;
+ supplier.MaxSupply = 400;
+ supplier.SupplyRampTolerance = 400;
+
+ netBattery.MaxSupply = 400;
+ netBattery.SupplyRampTolerance = 400;
+ netBattery.SupplyRampRate = 100_000;
+ netBattery.Efficiency = 0.5f;
+ battery.MaxCharge = 1_000_000;
+ battery.CurrentCharge = 1_000_000;
+ });
+
+ // Run some ticks so everything is stable.
+ _server.RunTicks(60);
+
+ // Exact values can/will be off by a tick, add tolerance for that.
+ var tickRate = (float) _gameTiming.TickPeriod.TotalSeconds;
+ var tickDev = 400 * tickRate * 1.1f;
+
+ _server.Assert(() =>
+ {
+ Assert.That(consumer.ReceivedPower, Is.EqualTo(600).Within(0.1));
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1));
+
+ Assert.That(netBattery.CurrentSupply, Is.EqualTo(600).Within(0.1));
+ Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(0.1));
+
+ const int expectedSpent = 400;
+ Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task TestFullBatteryEfficiencyDemandPassThrough()
+ {
+ PowerConsumerComponent consumer1 = default!;
+ PowerConsumerComponent consumer2 = default!;
+ PowerSupplierComponent supplier = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Map layout here is
+ // C - consumer
+ // B - battery
+ // G - generator
+ // B - battery
+ // C - consumer
+ // Connected in the only way that makes sense.
+
+ // Power only works when anchored
+ for (var i = 0; i < 5; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
+ }
+
+ _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2));
+ var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2));
+ terminal.Transform.LocalRotation = Angle.FromDegrees(180);
+
+ var batteryEnt1 = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 1));
+ var batteryEnt2 = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 3));
+ var supplyEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 2));
+ var consumerEnt1 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 0));
+ var consumerEnt2 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 4));
+
+ consumer1 = consumerEnt1.GetComponent();
+ consumer2 = consumerEnt2.GetComponent();
+ supplier = supplyEnt.GetComponent();
+ var netBattery1 = batteryEnt1.GetComponent();
+ var netBattery2 = batteryEnt2.GetComponent();
+ var battery1 = batteryEnt1.GetComponent();
+ var battery2 = batteryEnt2.GetComponent();
+
+ // There are two loads, 500 W and 1000 W respectively.
+ // The 500 W load is behind a 50% efficient battery,
+ // so *effectively* it needs 2x as much power from the supply to run.
+ // Assert that both are getting 50% power.
+ // Batteries are empty and only a bridge.
+
+ consumer1.DrawRate = 500;
+ consumer2.DrawRate = 1000;
+ supplier.MaxSupply = 1000;
+ supplier.SupplyRampTolerance = 1000;
+
+ battery1.MaxCharge = 1_000_000;
+ battery2.MaxCharge = 1_000_000;
+
+ netBattery1.MaxChargeRate = 1_000;
+ netBattery2.MaxChargeRate = 1_000;
+
+ netBattery1.Efficiency = 0.5f;
+
+ netBattery1.MaxSupply = 1_000_000;
+ netBattery2.MaxSupply = 1_000_000;
+
+ netBattery1.SupplyRampTolerance = 1_000_000;
+ netBattery2.SupplyRampTolerance = 1_000_000;
+ });
+
+ // Run some ticks so everything is stable.
+ _server.RunTicks(10);
+
+ _server.Assert(() =>
+ {
+ Assert.That(consumer1.ReceivedPower, Is.EqualTo(250).Within(0.1));
+ Assert.That(consumer2.ReceivedPower, Is.EqualTo(500).Within(0.1));
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+ ///
+ /// Test that power is distributed proportionally, even through batteries.
+ ///
+ [Test]
+ public async Task TestBatteriesProportional()
+ {
+ PowerConsumerComponent consumer1 = default!;
+ PowerConsumerComponent consumer2 = default!;
+ PowerSupplierComponent supplier = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Map layout here is
+ // C - consumer
+ // B - battery
+ // G - generator
+ // B - battery
+ // C - consumer
+ // Connected in the only way that makes sense.
+
+ // Power only works when anchored
+ for (var i = 0; i < 5; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
+ }
+
+ _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2));
+ var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2));
+ terminal.Transform.LocalRotation = Angle.FromDegrees(180);
+
+ var batteryEnt1 = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 1));
+ var batteryEnt2 = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 3));
+ var supplyEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 2));
+ var consumerEnt1 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 0));
+ var consumerEnt2 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 4));
+
+ consumer1 = consumerEnt1.GetComponent();
+ consumer2 = consumerEnt2.GetComponent();
+ supplier = supplyEnt.GetComponent();
+ var netBattery1 = batteryEnt1.GetComponent();
+ var netBattery2 = batteryEnt2.GetComponent();
+ var battery1 = batteryEnt1.GetComponent();
+ var battery2 = batteryEnt2.GetComponent();
+
+ consumer1.DrawRate = 500;
+ consumer2.DrawRate = 1000;
+ supplier.MaxSupply = 1000;
+ supplier.SupplyRampTolerance = 1000;
+
+ battery1.MaxCharge = 1_000_000;
+ battery2.MaxCharge = 1_000_000;
+
+ netBattery1.MaxChargeRate = 20;
+ netBattery2.MaxChargeRate = 20;
+
+ netBattery1.MaxSupply = 1_000_000;
+ netBattery2.MaxSupply = 1_000_000;
+
+ netBattery1.SupplyRampTolerance = 1_000_000;
+ netBattery2.SupplyRampTolerance = 1_000_000;
+ });
+
+ // Run some ticks so everything is stable.
+ _server.RunTicks(60);
+
+ _server.Assert(() =>
+ {
+ // NOTE: MaxChargeRate on batteries actually skews the demand.
+ // So that's why the tolerance is so high, the charge rate is so *low*,
+ // and we run so many ticks to stabilize.
+ Assert.That(consumer1.ReceivedPower, Is.EqualTo(333.333).Within(10));
+ Assert.That(consumer2.ReceivedPower, Is.EqualTo(666.666).Within(10));
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task TestBatteryEngineCut()
+ {
+ PowerConsumerComponent consumer = default!;
+ PowerSupplierComponent supplier = default!;
+ PowerNetworkBatteryComponent netBattery = default!;
+
+ _server.Post(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 4; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
+ }
+
+ var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1));
+ terminal.Transform.LocalRotation = Angle.FromDegrees(180);
+
+ var batteryEnt = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2));
+ var supplyEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0));
+ var consumerEnt = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 3));
+
+ consumer = consumerEnt.GetComponent();
+ supplier = supplyEnt.GetComponent();
+ netBattery = batteryEnt.GetComponent();
+ var battery = batteryEnt.GetComponent();
+
+ // Consumer needs 1000 W, supplier can only provide 800, battery fills in the remaining 200.
+ consumer.DrawRate = 1000;
+ supplier.MaxSupply = 1000;
+ supplier.SupplyRampTolerance = 1000;
+
+ netBattery.MaxSupply = 1000;
+ netBattery.SupplyRampTolerance = 200;
+ netBattery.SupplyRampRate = 10;
+ battery.MaxCharge = 100_000;
+ battery.CurrentCharge = 100_000;
+ });
+
+ // Run some ticks so everything is stable.
+ _server.RunTicks(5);
+
+ _server.Assert(() =>
+ {
+ // Supply and consumer are fully loaded/supplied.
+ Assert.That(consumer.ReceivedPower, Is.EqualTo(consumer.DrawRate).Within(0.5));
+ Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.5));
+
+ // Cut off the supplier
+ supplier.Enabled = false;
+ // Remove tolerance on battery too.
+ netBattery.SupplyRampTolerance = 5;
+ });
+
+ _server.RunTicks(3);
+
+ _server.Assert(() =>
+ {
+ // Assert that network drops to 0 power and starts ramping up
+ Assert.That(consumer.ReceivedPower, Is.LessThan(50).And.GreaterThan(0));
+ Assert.That(netBattery.CurrentReceiving, Is.EqualTo(0));
+ Assert.That(netBattery.CurrentSupply, Is.GreaterThan(0));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+ ///
+ /// Test that correctly isolates two networks.
+ ///
+ [Test]
+ public async Task TestTerminalNodeGroups()
+ {
+ CableNode leftNode = default!;
+ CableNode rightNode = default!;
+ Node batteryInput = default!;
+ Node batteryOutput = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 4; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ }
+
+ var leftEnt = _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 0));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 1));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 2));
+ var rightEnt = _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 3));
+
+ var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1));
+ terminal.Transform.LocalRotation = Angle.FromDegrees(180);
+
+ var battery = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2));
+ var batteryNodeContainer = battery.GetComponent();
+
+ leftNode = leftEnt.GetComponent().GetNode("power");
+ rightNode = rightEnt.GetComponent().GetNode("power");
+
+ batteryInput = batteryNodeContainer.GetNode("input");
+ batteryOutput = batteryNodeContainer.GetNode("output");
+ });
+
+ // Run ticks to allow node groups to update.
+ _server.RunTicks(1);
+
+ _server.Assert(() =>
+ {
+ Assert.That(batteryInput.NodeGroup, Is.EqualTo(leftNode.NodeGroup));
+ Assert.That(batteryOutput.NodeGroup, Is.EqualTo(rightNode.NodeGroup));
+
+ Assert.That(leftNode.NodeGroup, Is.Not.EqualTo(rightNode.NodeGroup));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task ApcChargingTest()
+ {
+ PowerNetworkBatteryComponent substationNetBattery = default!;
+ BatteryComponent apcBattery = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 3; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ }
+
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 0));
+ _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 1));
+ _entityManager.SpawnEntity("CableMV", grid.ToCoordinates(0, 1));
+ _entityManager.SpawnEntity("CableMV", grid.ToCoordinates(0, 2));
+
+ var generatorEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0));
+ var substationEnt = _entityManager.SpawnEntity("SubstationDummy", grid.ToCoordinates(0, 1));
+ var apcEnt = _entityManager.SpawnEntity("ApcDummy", grid.ToCoordinates(0, 2));
+
+ var generatorSupplier = generatorEnt.GetComponent();
+ substationNetBattery = substationEnt.GetComponent();
+ apcBattery = apcEnt.GetComponent();
+
+ generatorSupplier.MaxSupply = 1000;
+ generatorSupplier.SupplyRampTolerance = 1000;
+
+ apcBattery.CurrentCharge = 0;
+ });
+
+ _server.RunTicks(5); //let run a few ticks for PowerNets to reevaluate and start charging apc
+
+ _server.Assert(() =>
+ {
+ Assert.That(substationNetBattery.CurrentSupply, Is.GreaterThan(0)); //substation should be providing power
+ Assert.That(apcBattery.CurrentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
+ });
+
+ await _server.WaitIdleAsync();
+ }
+
+ [Test]
+ public async Task ApcNetTest()
+ {
+ PowerNetworkBatteryComponent apcNetBattery = default!;
+ ApcPowerReceiverComponent receiver = default!;
+
+ _server.Assert(() =>
+ {
+ var map = _mapManager.CreateMap();
+ var grid = _mapManager.CreateGrid(map);
+
+ // Power only works when anchored
+ for (var i = 0; i < 3; i++)
+ {
+ grid.SetTile(new Vector2i(0, i), new Tile(1));
+ }
+
+ var apcEnt = _entityManager.SpawnEntity("ApcDummy", grid.ToCoordinates(0, 0));
+ var apcExtensionEnt = _entityManager.SpawnEntity("CableApcExtension", grid.ToCoordinates(0, 0));
+ var powerReceiverEnt = _entityManager.SpawnEntity("ApcPowerReceiverDummy", grid.ToCoordinates(0, 2));
+
+ var provider = apcExtensionEnt.GetComponent();
+ receiver = powerReceiverEnt.GetComponent();
+ var battery = apcEnt.GetComponent();
+ apcNetBattery = apcEnt.GetComponent();
+
+ provider.PowerTransferRange = 5; //arbitrary range to reach receiver
+ receiver.PowerReceptionRange = 5; //arbitrary range to reach provider
+
+ battery.MaxCharge = 10000; //arbitrary nonzero amount of charge
+ battery.CurrentCharge = battery.MaxCharge; //fill battery
+
+ receiver.Load = 1; //arbitrary small amount of power
+ });
+
+ _server.RunTicks(1); //let run a tick for ApcNet to process power
+
+ _server.Assert(() =>
+ {
+ Assert.That(receiver.Powered);
+ Assert.That(apcNetBattery.CurrentSupply, Is.EqualTo(1).Within(0.1));
+ });
+
+ await _server.WaitIdleAsync();
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/PowerTest.cs b/Content.IntegrationTests/Tests/PowerTest.cs
deleted file mode 100644
index 191288b16a..0000000000
--- a/Content.IntegrationTests/Tests/PowerTest.cs
+++ /dev/null
@@ -1,292 +0,0 @@
-#nullable enable
-using System.Threading.Tasks;
-using Content.Server.APC.Components;
-using Content.Server.Battery.Components;
-using Content.Server.GameObjects.Components;
-using Content.Server.Power.Components;
-using Content.Shared.Coordinates;
-using NUnit.Framework;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Map;
-using Robust.Shared.Maths;
-using Robust.Shared.Physics;
-
-namespace Content.IntegrationTests.Tests
-{
- [TestFixture]
- public class PowerTest : ContentIntegrationTest
- {
- private const string Prototypes = @"
-- type: entity
- name: GeneratorDummy
- id: GeneratorDummy
- components:
- - type: NodeContainer
- nodes:
- output:
- !type:AdjacentNode
- nodeGroupID: HVPower
- - type: PowerSupplier
- supplyRate: 3000
- - type: Anchorable
- - type: Transform
- anchored: true
-
-- type: entity
- name: ConsumerDummy
- id: ConsumerDummy
- components:
- - type: Transform
- anchored: true
- - type: NodeContainer
- nodes:
- input:
- !type:AdjacentNode
- nodeGroupID: HVPower
- - type: PowerConsumer
- drawRate: 50
-
-- type: entity
- name: SubstationDummy
- id: SubstationDummy
- components:
- - type: Battery
- maxCharge: 1000
- startingCharge: 1000
- - type: NodeContainer
- nodes:
- input:
- !type:AdjacentNode
- nodeGroupID: HVPower
- output:
- !type:AdjacentNode
- nodeGroupID: MVPower
- - type: PowerConsumer
- - type: BatteryStorage
- activeDrawRate: 1500
- - type: PowerSupplier
- voltage: Medium
- - type: BatteryDischarger
- activeSupplyRate: 1000
- - type: Transform
- anchored: true
-
-- type: entity
- name: ApcDummy
- id: ApcDummy
- components:
- - type: Battery
- maxCharge: 10000
- startingCharge: 10000
- - type: BatteryStorage
- activeDrawRate: 1000
- - type: PowerProvider
- voltage: Apc
- - type: Apc
- voltage: Apc
- - type: PowerConsumer
- voltage: Medium
- - type: NodeContainer
- nodes:
- input:
- !type:AdjacentNode
- nodeGroupID: MVPower
- output:
- !type:AdjacentNode
- nodeGroupID: Apc
- - type: Transform
- anchored: true
- - type: UserInterface
- interfaces:
- - key: enum.ApcUiKey.Key
- type: ApcBoundUserInterface
- - type: AccessReader
- access: [['Engineering']]
-
-- type: entity
- name: ApcExtensionCableDummy
- id: ApcExtensionCableDummy
- components:
- - type: NodeContainer
- nodes:
- apc:
- !type:AdjacentNode
- nodeGroupID: Apc
- wire:
- !type:AdjacentNode
- nodeGroupID: WireNet
- - type: PowerProvider
- voltage: Apc
- - type: Wire
- wireType: Apc
- - type: Transform
- anchored: true
-
-- type: entity
- name: PowerReceiverDummy
- id: PowerReceiverDummy
- components:
- - type: PowerReceiver
- - type: Transform
- anchored: true
-";
- [Test]
- public async Task PowerNetTest()
- {
- var options = new ServerIntegrationOptions{ExtraPrototypes = Prototypes};
- var server = StartServerDummyTicker(options);
-
- PowerSupplierComponent supplier = default!;
- PowerConsumerComponent consumer1 = default!;
- PowerConsumerComponent consumer2 = default!;
-
- server.Assert(() =>
- {
- var mapMan = IoCManager.Resolve();
- var entityMan = IoCManager.Resolve();
- mapMan.CreateMap(new MapId(1));
- var grid = mapMan.CreateGrid(new MapId(1));
-
- // Power only works when anchored
- grid.SetTile(new Vector2i(0, 0), new Tile(1));
- grid.SetTile(new Vector2i(0, 1), new Tile(1));
- grid.SetTile(new Vector2i(0, 2), new Tile(1));
-
- var generatorEnt = entityMan.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
- var consumerEnt1 = entityMan.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 1));
- var consumerEnt2 = entityMan.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2));
-
- if (generatorEnt.TryGetComponent(out PhysicsComponent? physics))
- {
- physics.BodyType = BodyType.Static;
- }
-
- supplier = generatorEnt.GetComponent();
- consumer1 = consumerEnt1.GetComponent();
- consumer2 = consumerEnt2.GetComponent();
-
- var supplyRate = 1000; //arbitrary amount of power supply
-
- supplier.SupplyRate = supplyRate;
- consumer1.DrawRate = supplyRate / 2; //arbitrary draw less than supply
- consumer2.DrawRate = supplyRate * 2; //arbitrary draw greater than supply
-
- consumer1.Priority = Priority.First; //power goes to this consumer first
- consumer2.Priority = Priority.Last; //any excess power should go to low priority consumer
- });
-
- server.RunTicks(1); //let run a tick for PowerNet to process power
-
- server.Assert(() =>
- {
- Assert.That(consumer1.DrawRate, Is.EqualTo(consumer1.ReceivedPower)); //first should be fully powered
- Assert.That(consumer2.ReceivedPower, Is.EqualTo(supplier.SupplyRate - consumer1.ReceivedPower)); //second should get remaining power
- });
-
- await server.WaitIdleAsync();
- }
-
- [Test]
- public async Task ApcChargingTest()
- {
- var options = new ServerIntegrationOptions{ExtraPrototypes = Prototypes};
- var server = StartServerDummyTicker(options);
-
- BatteryComponent apcBattery = default!;
- PowerSupplierComponent substationSupplier = default!;
-
- server.Assert(() =>
- {
- var mapMan = IoCManager.Resolve();
- var entityMan = IoCManager.Resolve();
- mapMan.CreateMap(new MapId(1));
- var grid = mapMan.CreateGrid(new MapId(1));
-
- // Power only works when anchored
- grid.SetTile(new Vector2i(0, 0), new Tile(1));
- grid.SetTile(new Vector2i(0, 1), new Tile(1));
- grid.SetTile(new Vector2i(0, 2), new Tile(1));
-
- var generatorEnt = entityMan.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
- var substationEnt = entityMan.SpawnEntity("SubstationDummy", grid.ToCoordinates(0, 1));
- var apcEnt = entityMan.SpawnEntity("ApcDummy", grid.ToCoordinates(0, 2));
-
- var generatorSupplier = generatorEnt.GetComponent();
-
- substationSupplier = substationEnt.GetComponent();
- var substationStorage = substationEnt.GetComponent();
- var substationDischarger = substationEnt.GetComponent();
-
- apcBattery = apcEnt.GetComponent();
- var apcStorage = apcEnt.GetComponent();
-
- generatorSupplier.SupplyRate = 1000; //arbitrary nonzero amount of power
- substationStorage.ActiveDrawRate = 1000; //arbitrary nonzero power draw
- substationDischarger.ActiveSupplyRate = 500; //arbitirary nonzero power supply less than substation storage draw
- apcStorage.ActiveDrawRate = 500; //arbitrary nonzero power draw
- apcBattery.MaxCharge = 100; //abbitrary nonzero amount of charge
- apcBattery.CurrentCharge = 0; //no charge
- });
-
- server.RunTicks(5); //let run a few ticks for PowerNets to reevaluate and start charging apc
-
- server.Assert(() =>
- {
- Assert.That(substationSupplier.SupplyRate, Is.Not.EqualTo(0)); //substation should be providing power
- Assert.That(apcBattery.CurrentCharge, Is.Not.EqualTo(0)); //apc battery should have gained charge
- });
-
- await server.WaitIdleAsync();
- }
-
- [Test]
- public async Task ApcNetTest()
- {
- var options = new ServerIntegrationOptions{ExtraPrototypes = Prototypes};
- var server = StartServerDummyTicker(options);
-
- PowerReceiverComponent receiver = default!;
-
- server.Assert(() =>
- {
- var mapMan = IoCManager.Resolve();
- var entityMan = IoCManager.Resolve();
- var mapId = new MapId(1);
- mapMan.CreateMap(mapId);
- var grid = mapMan.CreateGrid(mapId);
-
- // Power only works when anchored
- grid.SetTile(new Vector2i(0, 0), new Tile(1));
- grid.SetTile(new Vector2i(0, 1), new Tile(1));
- grid.SetTile(new Vector2i(0, 2), new Tile(1));
-
- var apcEnt = entityMan.SpawnEntity("ApcDummy", grid.ToCoordinates(0, 0));
- var apcExtensionEnt = entityMan.SpawnEntity("ApcExtensionCableDummy", grid.ToCoordinates(0, 1));
- var powerReceiverEnt = entityMan.SpawnEntity("PowerReceiverDummy", grid.ToCoordinates(0, 2));
-
- var apc = apcEnt.GetComponent();
- var provider = apcExtensionEnt.GetComponent();
- receiver = powerReceiverEnt.GetComponent();
- var battery = apcEnt.GetComponent();
-
- provider.PowerTransferRange = 5; //arbitrary range to reach receiver
- receiver.PowerReceptionRange = 5; //arbitrary range to reach provider
-
- battery.MaxCharge = 10000; //arbitrary nonzero amount of charge
- battery.CurrentCharge = battery.MaxCharge; //fill battery
-
- receiver.Load = 1; //arbitrary small amount of power
- });
-
- server.RunTicks(1); //let run a tick for ApcNet to process power
-
- server.Assert(() =>
- {
- Assert.That(receiver.Powered);
- });
-
- await server.WaitIdleAsync();
- }
- }
-}
diff --git a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs
index 9bf40017c0..e77981375b 100644
--- a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs
+++ b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs
@@ -122,7 +122,24 @@ namespace Content.IntegrationTests.Tests
two = reader.ReadToEnd();
}
- Assert.That(one, Is.EqualTo(two));
+ Assert.Multiple(() => {
+ Assert.That(two, Is.EqualTo(one));
+ var failed = TestContext.CurrentContext.Result.Assertions.FirstOrDefault();
+ if (failed != null)
+ {
+ var oneTmp = Path.GetTempFileName();
+ var twoTmp = Path.GetTempFileName();
+
+ File.WriteAllText(oneTmp, one);
+ File.WriteAllText(twoTmp, two);
+
+ TestContext.AddTestAttachment(oneTmp, "First save file");
+ TestContext.AddTestAttachment(twoTmp, "Second save file");
+ TestContext.Error.WriteLine("Complete output:");
+ TestContext.Error.WriteLine(oneTmp);
+ TestContext.Error.WriteLine(twoTmp);
+ }
+ });
}
}
}
diff --git a/Content.Server/AME/AMENodeGroup.cs b/Content.Server/AME/AMENodeGroup.cs
index 920cff987c..c632f39d91 100644
--- a/Content.Server/AME/AMENodeGroup.cs
+++ b/Content.Server/AME/AMENodeGroup.cs
@@ -8,6 +8,7 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.IoC;
using Robust.Shared.Map;
+using Robust.Shared.Maths;
using Robust.Shared.Random;
using Robust.Shared.ViewVariables;
@@ -36,19 +37,23 @@ namespace Content.Server.AME
public int CoreCount => _cores.Count;
- protected override void OnAddNode(Node node)
+ public override void LoadNodes(List groupNodes)
{
- base.OnAddNode(node);
- if (_masterController == null)
+ base.LoadNodes(groupNodes);
+
+ foreach (var node in groupNodes)
{
- node.Owner.TryGetComponent(out var controller);
- _masterController = controller;
+ if (node.Owner.TryGetComponent(out AMEControllerComponent? controller))
+ {
+ _masterController = controller;
+ }
}
}
- protected override void OnRemoveNode(Node node)
+ public override void RemoveNode(Node node)
{
- base.OnRemoveNode(node);
+ base.RemoveNode(node);
+
RefreshAMENodes(_masterController);
if (_masterController != null && _masterController?.Owner == node.Owner) { _masterController = null; }
}
@@ -119,7 +124,7 @@ namespace Content.Server.AME
// fuel > safeFuelLimit: Slow damage. Can safely run at this level for burst periods if the engine is small and someone is keeping an eye on it.
if (_random.Prob(0.5f))
instability = 1;
- // overloadVsSizeResult > 5:
+ // overloadVsSizeResult > 5:
if (overloadVsSizeResult > 5)
instability = 5;
// overloadVsSizeResult > 10: This will explode in at most 5 injections.
diff --git a/Content.Server/AME/Components/AMEControllerComponent.cs b/Content.Server/AME/Components/AMEControllerComponent.cs
index b6e71fd1c2..ea65e81f94 100644
--- a/Content.Server/AME/Components/AMEControllerComponent.cs
+++ b/Content.Server/AME/Components/AMEControllerComponent.cs
@@ -33,7 +33,7 @@ namespace Content.Server.AME.Components
private AppearanceComponent? _appearance;
private PowerSupplierComponent? _powerSupplier;
- private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables]
private int _stability = 100;
@@ -92,7 +92,7 @@ namespace Content.Server.AME.Components
if(fuelJar != null && _powerSupplier != null)
{
var availableInject = fuelJar.FuelAmount >= InjectionAmount ? InjectionAmount : fuelJar.FuelAmount;
- _powerSupplier.SupplyRate = group.InjectFuel(availableInject, out var overloading);
+ _powerSupplier.MaxSupply = group.InjectFuel(availableInject, out var overloading);
fuelJar.FuelAmount -= availableInject;
InjectSound(overloading);
UpdateUserInterface();
@@ -252,7 +252,7 @@ namespace Content.Server.AME.Components
_appearance?.SetData(AMEControllerVisuals.DisplayState, "off");
if (_powerSupplier != null)
{
- _powerSupplier.SupplyRate = 0;
+ _powerSupplier.MaxSupply = 0;
}
}
_injecting = !_injecting;
diff --git a/Content.Server/APC/ApcNetNodeGroup.cs b/Content.Server/APC/ApcNetNodeGroup.cs
deleted file mode 100644
index 8b2bae38b8..0000000000
--- a/Content.Server/APC/ApcNetNodeGroup.cs
+++ /dev/null
@@ -1,223 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using System.Linq;
-using Content.Server.APC.Components;
-using Content.Server.Battery.Components;
-using Content.Server.NodeContainer.NodeGroups;
-using Content.Server.NodeContainer.Nodes;
-using Content.Server.Power.Components;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.Utility;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Server.APC
-{
- public interface IApcNet
- {
- bool Powered { get; }
-
- void AddApc(ApcComponent apc);
-
- void RemoveApc(ApcComponent apc);
-
- void AddPowerProvider(PowerProviderComponent provider);
-
- void RemovePowerProvider(PowerProviderComponent provider);
-
- void UpdatePowerProviderReceivers(PowerProviderComponent provider, int oldLoad, int newLoad);
-
- void Update(float frameTime);
-
- GridId? GridId { get; }
- }
-
- [NodeGroup(NodeGroupID.Apc)]
- public class ApcNetNodeGroup : BaseNetConnectorNodeGroup, IApcNet
- {
- [ViewVariables]
- private readonly Dictionary _apcBatteries = new();
-
- [ViewVariables]
- private readonly List _providers = new();
-
- [ViewVariables]
- public bool Powered { get => _powered; private set => SetPowered(value); }
- private bool _powered = false;
-
- //Debug property
- [ViewVariables]
- private int TotalReceivers => _providers.SelectMany(provider => provider.LinkedReceivers).Count();
-
- [ViewVariables]
- private int TotalPowerReceiverLoad { get => _totalPowerReceiverLoad; set => SetTotalPowerReceiverLoad(value); }
-
- GridId? IApcNet.GridId => GridId;
-
- private int _totalPowerReceiverLoad = 0;
-
- public static readonly IApcNet NullNet = new NullApcNet();
-
- public override void Initialize(Node sourceNode)
- {
- base.Initialize(sourceNode);
-
- EntitySystem.Get().AddApcNet(this);
- }
-
- protected override void AfterRemake(IEnumerable newGroups)
- {
- base.AfterRemake(newGroups);
-
- foreach (var group in newGroups)
- {
- if (group is not ApcNetNodeGroup apcNet)
- continue;
-
- apcNet.Powered = Powered;
- }
-
- StopUpdates();
- }
-
- protected override void OnGivingNodesForCombine(INodeGroup newGroup)
- {
- base.OnGivingNodesForCombine(newGroup);
-
- if (newGroup is ApcNetNodeGroup apcNet)
- {
- apcNet.Powered = Powered;
- }
-
- StopUpdates();
- }
-
- private void StopUpdates()
- {
- EntitySystem.Get().RemoveApcNet(this);
- }
-
- #region IApcNet Methods
-
- protected override void SetNetConnectorNet(BaseApcNetComponent netConnectorComponent)
- {
- netConnectorComponent.Net = this;
- }
-
- public void AddApc(ApcComponent apc)
- {
- if (!apc.Owner.TryGetComponent(out BatteryComponent? battery))
- {
- return;
- }
-
- _apcBatteries.Add(apc, battery);
- }
-
- public void RemoveApc(ApcComponent apc)
- {
- _apcBatteries.Remove(apc);
- }
-
- public void AddPowerProvider(PowerProviderComponent provider)
- {
- _providers.Add(provider);
-
- foreach (var receiver in provider.LinkedReceivers)
- {
- TotalPowerReceiverLoad += receiver.Load;
- }
- }
-
- public void RemovePowerProvider(PowerProviderComponent provider)
- {
- _providers.Remove(provider);
-
- foreach (var receiver in provider.LinkedReceivers)
- {
- TotalPowerReceiverLoad -= receiver.Load;
- }
- }
-
- public void UpdatePowerProviderReceivers(PowerProviderComponent provider, int oldLoad, int newLoad)
- {
- DebugTools.Assert(_providers.Contains(provider));
- TotalPowerReceiverLoad -= oldLoad;
- TotalPowerReceiverLoad += newLoad;
- }
-
- public void Update(float frameTime)
- {
- var remainingPowerNeeded = TotalPowerReceiverLoad * frameTime;
-
- foreach (var apcBatteryPair in _apcBatteries)
- {
- var apc = apcBatteryPair.Key;
-
- if (!apc.MainBreakerEnabled)
- continue;
-
- var battery = apcBatteryPair.Value;
-
- if (battery.CurrentCharge < remainingPowerNeeded)
- {
- remainingPowerNeeded -= battery.CurrentCharge;
- battery.CurrentCharge = 0;
- }
- else
- {
- battery.UseCharge(remainingPowerNeeded);
- remainingPowerNeeded = 0;
- }
-
- if (remainingPowerNeeded == 0)
- break;
- }
-
- Powered = remainingPowerNeeded == 0;
- }
-
- private void SetPowered(bool powered)
- {
- if (powered != Powered)
- {
- _powered = powered;
- PoweredChanged();
- }
- }
-
- private void PoweredChanged()
- {
- foreach (var provider in _providers)
- {
- foreach (var receiver in provider.LinkedReceivers)
- {
- receiver.ApcPowerChanged();
- }
- }
- }
-
- private void SetTotalPowerReceiverLoad(int totalPowerReceiverLoad)
- {
- DebugTools.Assert(totalPowerReceiverLoad >= 0, $"Expected load equal to or greater than 0, was {totalPowerReceiverLoad}");
- _totalPowerReceiverLoad = totalPowerReceiverLoad;
- }
-
- #endregion
-
- private class NullApcNet : IApcNet
- {
- ///
- /// It is important that this returns false, so s with a have no power.
- ///
- public bool Powered => false;
- public GridId? GridId => default;
- public void AddApc(ApcComponent apc) { }
- public void AddPowerProvider(PowerProviderComponent provider) { }
- public void RemoveApc(ApcComponent apc) { }
- public void RemovePowerProvider(PowerProviderComponent provider) { }
- public void UpdatePowerProviderReceivers(PowerProviderComponent provider, int oldLoad, int newLoad) { }
- public void Update(float frameTime) { }
- }
- }
-}
diff --git a/Content.Server/APC/ApcNetSystem.cs b/Content.Server/APC/ApcNetSystem.cs
deleted file mode 100644
index ad19216340..0000000000
--- a/Content.Server/APC/ApcNetSystem.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using Content.Shared.GameTicking;
-using JetBrains.Annotations;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Timing;
-
-namespace Content.Server.APC
-{
- [UsedImplicitly]
- internal sealed class ApcNetSystem : EntitySystem
- {
- [Dependency] private readonly IPauseManager _pauseManager = default!;
-
- private HashSet _apcNets = new();
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(Reset);
- }
-
- public override void Update(float frameTime)
- {
- foreach (var apcNet in _apcNets)
- {
- var gridId = apcNet.GridId;
- if (gridId != null && !_pauseManager.IsGridPaused(gridId.Value))
- apcNet.Update(frameTime);
- }
- }
-
- public void AddApcNet(ApcNetNodeGroup apcNet)
- {
- _apcNets.Add(apcNet);
- }
-
- public void RemoveApcNet(ApcNetNodeGroup apcNet)
- {
- _apcNets.Remove(apcNet);
- }
-
- public void Reset(RoundRestartCleanupEvent ev)
- {
- // NodeGroupSystem does not remake ApcNets affected during restarting until a frame later,
- // when their grid is invalid. So, we are clearing them on round restart.
- _apcNets.Clear();
- }
- }
-}
diff --git a/Content.Server/APC/Components/BaseApcNetComponent.cs b/Content.Server/APC/Components/BaseApcNetComponent.cs
deleted file mode 100644
index 51fa8b3c43..0000000000
--- a/Content.Server/APC/Components/BaseApcNetComponent.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-#nullable enable
-using Content.Server.Power.Components;
-
-namespace Content.Server.APC.Components
-{
- public abstract class BaseApcNetComponent : BaseNetConnectorComponent
- {
- protected override IApcNet NullNet => ApcNetNodeGroup.NullNet;
- }
-}
diff --git a/Content.Server/Access/Components/IdCardConsoleComponent.cs b/Content.Server/Access/Components/IdCardConsoleComponent.cs
index 5debc68a1a..f9de78f2ac 100644
--- a/Content.Server/Access/Components/IdCardConsoleComponent.cs
+++ b/Content.Server/Access/Components/IdCardConsoleComponent.cs
@@ -34,7 +34,7 @@ namespace Content.Server.Access.Components
private ContainerSlot _targetIdContainer = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(IdCardConsoleUiKey.Key);
- [ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ [ViewVariables] private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private bool PrivilegedIDEmpty => _privilegedIdContainer.ContainedEntities.Count < 1;
private bool TargetIDEmpty => _targetIdContainer.ContainedEntities.Count < 1;
diff --git a/Content.Server/Arcade/Components/BlockGameArcadeComponent.cs b/Content.Server/Arcade/Components/BlockGameArcadeComponent.cs
index 5dce5324bf..6634ae5d9a 100644
--- a/Content.Server/Arcade/Components/BlockGameArcadeComponent.cs
+++ b/Content.Server/Arcade/Components/BlockGameArcadeComponent.cs
@@ -27,7 +27,7 @@ namespace Content.Server.Arcade.Components
public override string Name => "BlockGameArcade";
public override uint? NetID => ContentNetIDs.BLOCKGAME_ARCADE;
- [ComponentDependency] private readonly PowerReceiverComponent? _powerReceiverComponent = default!;
+ [ComponentDependency] private readonly ApcPowerReceiverComponent? _powerReceiverComponent = default!;
private bool Powered => _powerReceiverComponent?.Powered ?? false;
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BlockGameUiKey.Key);
diff --git a/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs b/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs
index 40f337896a..d2aae64b6a 100644
--- a/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs
+++ b/Content.Server/Arcade/Components/SpaceVillainArcadeComponent.cs
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Server.VendingMachines;
-using Content.Server.Wires.Components;
+using Content.Server.WireHacking;
using Content.Shared.ActionBlocker;
using Content.Shared.Arcade;
using Content.Shared.Interaction;
@@ -28,7 +28,7 @@ namespace Content.Server.Arcade.Components
{
[Dependency] private readonly IRobustRandom _random = null!;
- [ComponentDependency] private readonly PowerReceiverComponent? _powerReceiverComponent = default!;
+ [ComponentDependency] private readonly ApcPowerReceiverComponent? _powerReceiverComponent = default!;
[ComponentDependency] private readonly WiresComponent? _wiresComponent = default!;
private bool Powered => _powerReceiverComponent != null && _powerReceiverComponent.Powered;
diff --git a/Content.Server/Atmos/Components/BaseComputerUserInterfaceComponent.cs b/Content.Server/Atmos/Components/BaseComputerUserInterfaceComponent.cs
index 195b1c2623..8023127069 100644
--- a/Content.Server/Atmos/Components/BaseComputerUserInterfaceComponent.cs
+++ b/Content.Server/Atmos/Components/BaseComputerUserInterfaceComponent.cs
@@ -20,7 +20,7 @@ namespace Content.Server.GameObjects.Components
protected readonly object UserInterfaceKey;
[ViewVariables] protected BoundUserInterface? UserInterface => Owner.GetUIOrNull(UserInterfaceKey);
- [ViewVariables] public bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ [ViewVariables] public bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
public BaseComputerUserInterfaceComponent(object key)
{
diff --git a/Content.Server/Atmos/Components/GasTankComponent.cs b/Content.Server/Atmos/Components/GasTankComponent.cs
index add95c5905..d30494b082 100644
--- a/Content.Server/Atmos/Components/GasTankComponent.cs
+++ b/Content.Server/Atmos/Components/GasTankComponent.cs
@@ -4,9 +4,9 @@ using System;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Respiratory;
using Content.Server.Explosion;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.Interfaces;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
index f7bdf1839b..2094d2a52e 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.Components;
+using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Maps;
using JetBrains.Annotations;
@@ -33,6 +34,8 @@ namespace Content.Server.Atmos.EntitySystems
{
base.Initialize();
+ UpdatesAfter.Add(typeof(NodeGroupSystem));
+
InitializeGases();
InitializeCVars();
diff --git a/Content.Server/Atmos/Piping/Binary/Components/GasValveComponent.cs b/Content.Server/Atmos/Piping/Binary/Components/GasValveComponent.cs
index 6a239c0700..7a20b63509 100644
--- a/Content.Server/Atmos/Piping/Binary/Components/GasValveComponent.cs
+++ b/Content.Server/Atmos/Piping/Binary/Components/GasValveComponent.cs
@@ -1,5 +1,5 @@
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPassiveGateSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPassiveGateSystem.cs
index dc825a48d6..9d27211ad2 100644
--- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPassiveGateSystem.cs
+++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPassiveGateSystem.cs
@@ -1,8 +1,8 @@
using System;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs
index 5e616ea68a..dc38b4da76 100644
--- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs
+++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs
@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs
index a157094c88..48e253167b 100644
--- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs
+++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs
@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
diff --git a/Content.Server/Atmos/Piping/EntitySystems/AtmosUnsafeUnanchorSystem.cs b/Content.Server/Atmos/Piping/EntitySystems/AtmosUnsafeUnanchorSystem.cs
index b94441e5b4..2f63c28b59 100644
--- a/Content.Server/Atmos/Piping/EntitySystems/AtmosUnsafeUnanchorSystem.cs
+++ b/Content.Server/Atmos/Piping/EntitySystems/AtmosUnsafeUnanchorSystem.cs
@@ -1,8 +1,8 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Construction.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Notification.Managers;
using JetBrains.Annotations;
diff --git a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
index 2b543a6a82..a2db87f7bc 100644
--- a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
+++ b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Trinary.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
diff --git a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs
index f2b829854f..3e886d6773 100644
--- a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs
+++ b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs
@@ -1,8 +1,8 @@
using System;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Trinary.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
index 2cfabfeb1f..d12a363475 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
@@ -4,9 +4,9 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.Hands.Components;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Atmos;
@@ -62,7 +62,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
return;
// Create a pipenet if we don't have one already.
- portNode.TryAssignGroupIfNeeded();
+ portNode.CreateSingleNetImmediate();
Get().Merge(portNode.Air, canister.InitialMixture);
portNode.Air.Temperature = canister.InitialMixture.Temperature;
portNode.Volume = canister.InitialMixture.Volume;
@@ -90,7 +90,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
}
ui.SetState(new GasCanisterBoundUserInterfaceState(metadata.EntityName, portNode.Air.Pressure,
- portNode.NodeGroup.Nodes.Count > 1, tankLabel, tankPressure,
+ portNode.NodeGroup!.Nodes.Count > 1, tankLabel, tankPressure,
canister.ReleasePressure, canister.ReleaseValve,
canister.MinReleasePressure, canister.MaxReleasePressure));
}
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasOutletInjectorSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasOutletInjectorSystem.cs
index 187cee7b93..1d275837ea 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasOutletInjectorSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasOutletInjectorSystem.cs
@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPassiveVentSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPassiveVentSystem.cs
index c04d3c44ad..e984bd3515 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPassiveVentSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPassiveVentSystem.cs
@@ -1,8 +1,8 @@
using System;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs
index d675ad21c6..4ee5c723fa 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs
@@ -2,8 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.Construction.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping.Unary.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs
index 43f001d186..748700d13e 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs
@@ -1,8 +1,8 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs
index a310e4a225..16046fd61e 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs
@@ -1,8 +1,8 @@
using System;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Visuals;
using JetBrains.Annotations;
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs
index 17559dbb4b..262924c2b6 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs
@@ -2,8 +2,8 @@ using System;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Unary.Visuals;
using JetBrains.Annotations;
diff --git a/Content.Server/BarSign/BarSignComponent.cs b/Content.Server/BarSign/BarSignComponent.cs
index 1a673d1186..a0b953dc22 100644
--- a/Content.Server/BarSign/BarSignComponent.cs
+++ b/Content.Server/BarSign/BarSignComponent.cs
@@ -35,7 +35,7 @@ namespace Content.Server.BarSign
}
}
- private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private void UpdateSignInfo()
{
diff --git a/Content.Server/Battery/EntitySystems/BatteryDischargerSystem.cs b/Content.Server/Battery/EntitySystems/BatteryDischargerSystem.cs
deleted file mode 100644
index 37873eaa61..0000000000
--- a/Content.Server/Battery/EntitySystems/BatteryDischargerSystem.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#nullable enable
-using Content.Server.Power.Components;
-using JetBrains.Annotations;
-using Robust.Shared.GameObjects;
-
-namespace Content.Server.Battery.EntitySystems
-{
- [UsedImplicitly]
- internal sealed class BatteryDischargerSystem : EntitySystem
- {
- public override void Update(float frameTime)
- {
- foreach (var comp in ComponentManager.EntityQuery(false))
- {
- comp.Update(frameTime);
- }
- }
- }
-}
diff --git a/Content.Server/Battery/EntitySystems/BatteryStorageSystem.cs b/Content.Server/Battery/EntitySystems/BatteryStorageSystem.cs
deleted file mode 100644
index c32b58e725..0000000000
--- a/Content.Server/Battery/EntitySystems/BatteryStorageSystem.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#nullable enable
-using Content.Server.Power.Components;
-using JetBrains.Annotations;
-using Robust.Shared.GameObjects;
-
-namespace Content.Server.Battery.EntitySystems
-{
- [UsedImplicitly]
- internal sealed class BatteryStorageSystem : EntitySystem
- {
- public override void Update(float frameTime)
- {
- foreach (var comp in ComponentManager.EntityQuery(false))
- {
- comp.Update(frameTime);
- }
- }
- }
-}
diff --git a/Content.Server/Battery/EntitySystems/BatterySystem.cs b/Content.Server/Battery/EntitySystems/BatterySystem.cs
deleted file mode 100644
index ace6084dce..0000000000
--- a/Content.Server/Battery/EntitySystems/BatterySystem.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#nullable enable
-using Content.Server.Battery.Components;
-using JetBrains.Annotations;
-using Robust.Shared.GameObjects;
-
-namespace Content.Server.Battery.EntitySystems
-{
- [UsedImplicitly]
- public class BatterySystem : EntitySystem
- {
- public override void Update(float frameTime)
- {
- foreach (var comp in ComponentManager.EntityQuery(true))
- {
- comp.OnUpdate(frameTime);
- }
- }
- }
-}
diff --git a/Content.Server/Botany/Components/SeedExtractorComponent.cs b/Content.Server/Botany/Components/SeedExtractorComponent.cs
index 160fdad86a..c195c4f869 100644
--- a/Content.Server/Botany/Components/SeedExtractorComponent.cs
+++ b/Content.Server/Botany/Components/SeedExtractorComponent.cs
@@ -14,7 +14,7 @@ namespace Content.Server.Botany.Components
[RegisterComponent]
public class SeedExtractorComponent : Component, IInteractUsing
{
- [ComponentDependency] private readonly PowerReceiverComponent? _powerReceiver = default!;
+ [ComponentDependency] private readonly ApcPowerReceiverComponent? _powerReceiver = default!;
[Dependency] private readonly IRobustRandom _random = default!;
diff --git a/Content.Server/Cargo/Components/CargoConsoleComponent.cs b/Content.Server/Cargo/Components/CargoConsoleComponent.cs
index 3adcf4f9bb..4a53d52b48 100644
--- a/Content.Server/Cargo/Components/CargoConsoleComponent.cs
+++ b/Content.Server/Cargo/Components/CargoConsoleComponent.cs
@@ -59,7 +59,7 @@ namespace Content.Server.Cargo.Components
[DataField("requestOnly")]
private bool _requestOnly = false;
- private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private CargoConsoleSystem _cargoConsoleSystem = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CargoConsoleUiKey.Key);
@@ -171,7 +171,7 @@ namespace Content.Server.Cargo.Components
{
foreach (IEntity entity in enumerator)
{
- if (entity.HasComponent() && entity.TryGetComponent(out var powerReceiver) && powerReceiver.Powered)
+ if (entity.HasComponent() && entity.TryGetComponent(out var powerReceiver) && powerReceiver.Powered)
{
cargoTelepad = entity;
break;
diff --git a/Content.Server/Cargo/Components/CargoTelepadComponent.cs b/Content.Server/Cargo/Components/CargoTelepadComponent.cs
index 1d87a7cf2b..ba31eb3d13 100644
--- a/Content.Server/Cargo/Components/CargoTelepadComponent.cs
+++ b/Content.Server/Cargo/Components/CargoTelepadComponent.cs
@@ -43,14 +43,14 @@ namespace Content.Server.Cargo.Components
{
if (args.Powered && _currentState == CargoTelepadState.Unpowered) {
_currentState = CargoTelepadState.Idle;
- if(Owner.TryGetComponent(out var spriteComponent))
+ if(Owner.TryGetComponent(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "idle");
TeleportLoop();
}
else if (!args.Powered)
{
_currentState = CargoTelepadState.Unpowered;
- if (Owner.TryGetComponent(out var spriteComponent))
+ if (Owner.TryGetComponent(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "offline");
}
}
@@ -59,14 +59,14 @@ namespace Content.Server.Cargo.Components
if (_currentState == CargoTelepadState.Idle && _teleportQueue.Count > 0)
{
_currentState = CargoTelepadState.Charging;
- if (Owner.TryGetComponent(out var spriteComponent))
+ if (Owner.TryGetComponent(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "idle");
Owner.SpawnTimer((int) (TeleportDelay * 1000), () =>
{
if (!Deleted && !Owner.Deleted && _currentState == CargoTelepadState.Charging && _teleportQueue.Count > 0)
{
_currentState = CargoTelepadState.Teleporting;
- if (Owner.TryGetComponent(out var spriteComponent))
+ if (Owner.TryGetComponent(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "beam");
Owner.SpawnTimer((int) (TeleportDuration * 1000), () =>
{
@@ -75,7 +75,7 @@ namespace Content.Server.Cargo.Components
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/phasein.ogg", Owner, AudioParams.Default.WithVolume(-8f));
Owner.EntityManager.SpawnEntity(_teleportQueue[0].Product, Owner.Transform.Coordinates);
_teleportQueue.RemoveAt(0);
- if (Owner.TryGetComponent(out var spriteComponent))
+ if (Owner.TryGetComponent(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "idle");
_currentState = CargoTelepadState.Idle;
TeleportLoop();
diff --git a/Content.Server/Chemistry/Components/ChemMasterComponent.cs b/Content.Server/Chemistry/Components/ChemMasterComponent.cs
index b06c5c9479..7ae9b4193f 100644
--- a/Content.Server/Chemistry/Components/ChemMasterComponent.cs
+++ b/Content.Server/Chemistry/Components/ChemMasterComponent.cs
@@ -40,7 +40,7 @@ namespace Content.Server.Chemistry.Components
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private bool _bufferModeTransfer = true;
- [ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ [ViewVariables] private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private readonly Solution BufferSolution = new();
diff --git a/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs b/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs
index 66c31a9357..938f009b8e 100644
--- a/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs
+++ b/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs
@@ -51,7 +51,7 @@ namespace Content.Server.Chemistry.Components
[ViewVariables] private ReagentUnit _dispenseAmount = ReagentUnit.New(10);
[UsedImplicitly] [ViewVariables] private SolutionContainerComponent? Solution => _beakerContainer.ContainedEntity?.GetComponent();
- [ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ [ViewVariables] private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ReagentDispenserUiKey.Key);
diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs
index 0e11d02f38..4622ae660b 100644
--- a/Content.Server/Cloning/CloningSystem.cs
+++ b/Content.Server/Cloning/CloningSystem.cs
@@ -74,7 +74,7 @@ namespace Content.Server.Cloning
public override void Update(float frameTime)
{
- foreach (var (cloning, power) in ComponentManager.EntityQuery(true))
+ foreach (var (cloning, power) in ComponentManager.EntityQuery(true))
{
if (cloning.UiKnownPowerState != power.Powered)
{
diff --git a/Content.Server/Cloning/Components/CloningPodComponent.cs b/Content.Server/Cloning/Components/CloningPodComponent.cs
index 3b7f81e28c..fba96c8860 100644
--- a/Content.Server/Cloning/Components/CloningPodComponent.cs
+++ b/Content.Server/Cloning/Components/CloningPodComponent.cs
@@ -26,7 +26,7 @@ namespace Content.Server.Cloning.Components
[Dependency] private readonly EuiManager _euiManager = null!;
[ViewVariables]
- public bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ public bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables]
public BoundUserInterface? UserInterface =>
diff --git a/Content.Server/Communications/CommunicationsConsoleComponent.cs b/Content.Server/Communications/CommunicationsConsoleComponent.cs
index 4ed0b34b7b..cf0b768e78 100644
--- a/Content.Server/Communications/CommunicationsConsoleComponent.cs
+++ b/Content.Server/Communications/CommunicationsConsoleComponent.cs
@@ -25,7 +25,7 @@ namespace Content.Server.Communications
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
- private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private RoundEndSystem RoundEndSystem => EntitySystem.Get();
diff --git a/Content.Server/Computer/ComputerComponent.cs b/Content.Server/Computer/ComputerComponent.cs
index 22527a5c7f..9e22500007 100644
--- a/Content.Server/Computer/ComputerComponent.cs
+++ b/Content.Server/Computer/ComputerComponent.cs
@@ -24,7 +24,7 @@ namespace Content.Server.Computer
// Let's ensure the container manager and container are here.
Owner.EnsureContainer("board", out var _);
- if (Owner.TryGetComponent(out PowerReceiverComponent? powerReceiver) &&
+ if (Owner.TryGetComponent(out ApcPowerReceiverComponent? powerReceiver) &&
Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(ComputerVisuals.Powered, powerReceiver.Powered);
diff --git a/Content.Server/Construction/Conditions/AllWiresCut.cs b/Content.Server/Construction/Conditions/AllWiresCut.cs
index 0370978f64..0137c057fc 100644
--- a/Content.Server/Construction/Conditions/AllWiresCut.cs
+++ b/Content.Server/Construction/Conditions/AllWiresCut.cs
@@ -1,6 +1,6 @@
using System.Threading.Tasks;
using Content.Server.GameObjects.Components;
-using Content.Server.Wires.Components;
+using Content.Server.WireHacking;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
diff --git a/Content.Server/Construction/Conditions/WirePanel.cs b/Content.Server/Construction/Conditions/WirePanel.cs
index a966a3c845..c443506a2e 100644
--- a/Content.Server/Construction/Conditions/WirePanel.cs
+++ b/Content.Server/Construction/Conditions/WirePanel.cs
@@ -1,6 +1,6 @@
using System.Threading.Tasks;
using Content.Server.GameObjects.Components;
-using Content.Server.Wires.Components;
+using Content.Server.WireHacking;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
diff --git a/Content.Server/Conveyor/ConveyorComponent.cs b/Content.Server/Conveyor/ConveyorComponent.cs
index 55e8465154..31983f36ef 100644
--- a/Content.Server/Conveyor/ConveyorComponent.cs
+++ b/Content.Server/Conveyor/ConveyorComponent.cs
@@ -19,7 +19,7 @@ namespace Content.Server.Conveyor
{
public override string Name => "Conveyor";
- [ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ [ViewVariables] private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
///
/// The angle to move entities by in relation to the owner's rotation.
@@ -105,7 +105,7 @@ namespace Content.Server.Conveyor
return false;
}
- if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) &&
+ if (Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) &&
!receiver.Powered)
{
return false;
diff --git a/Content.Server/DeviceNetwork/Connections/WiredNetworkConnection.cs b/Content.Server/DeviceNetwork/Connections/WiredNetworkConnection.cs
index c49abd3d61..710de4ea34 100644
--- a/Content.Server/DeviceNetwork/Connections/WiredNetworkConnection.cs
+++ b/Content.Server/DeviceNetwork/Connections/WiredNetworkConnection.cs
@@ -26,7 +26,7 @@ namespace Content.Server.DeviceNetwork.Connections
return false;
}
- if (_owner.TryGetComponent(out var powerReceiver)
+ if (_owner.TryGetComponent(out var powerReceiver)
&& TryGetWireNet(powerReceiver, out var ownNet)
&& metadata.TryParseMetadata(WIRENET, out var senderNet))
{
@@ -44,7 +44,7 @@ namespace Content.Server.DeviceNetwork.Connections
return new Metadata();
}
- if (_owner.TryGetComponent(out var powerReceiver)
+ if (_owner.TryGetComponent(out var powerReceiver)
&& TryGetWireNet(powerReceiver, out var net))
{
var metadata = new Metadata
@@ -63,16 +63,16 @@ namespace Content.Server.DeviceNetwork.Connections
return payload;
}
- private bool TryGetWireNet(PowerReceiverComponent powerReceiver, [NotNullWhen(true)] out INodeGroup? net)
+ private bool TryGetWireNet(ApcPowerReceiverComponent apcPowerReceiver, [NotNullWhen(true)] out INodeGroup? net)
{
- if (powerReceiver.Provider is PowerProviderComponent provider &&
- provider.ProviderOwner.TryGetComponent(out var nodeContainer))
+ var provider = apcPowerReceiver.Provider;
+ if (provider != null && provider.ProviderOwner.TryGetComponent(out var nodeContainer))
{
var nodes = nodeContainer.Nodes;
foreach (var node in nodes.Values)
{
- if (node.NodeGroupID == NodeGroupID.WireNet)
+ if (node.NodeGroupID == NodeGroupID.WireNet && node.NodeGroup != null)
{
net = node.NodeGroup;
return true;
diff --git a/Content.Server/Disposal/Mailing/DisposalMailingUnitComponent.cs b/Content.Server/Disposal/Mailing/DisposalMailingUnitComponent.cs
index 806ca63dec..bc202065f2 100644
--- a/Content.Server/Disposal/Mailing/DisposalMailingUnitComponent.cs
+++ b/Content.Server/Disposal/Mailing/DisposalMailingUnitComponent.cs
@@ -117,7 +117,7 @@ namespace Content.Server.Disposal.Mailing
[ViewVariables]
public bool Powered =>
- !Owner.TryGetComponent(out PowerReceiverComponent? receiver) ||
+ !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) ||
receiver.Powered;
[ViewVariables]
@@ -372,7 +372,7 @@ namespace Content.Server.Disposal.Mailing
private void TogglePower()
{
- if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver))
+ if (!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{
return;
}
diff --git a/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs b/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs
index 7d483e4399..c040169240 100644
--- a/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs
+++ b/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs
@@ -107,7 +107,7 @@ namespace Content.Server.Disposal.Unit.Components
[ViewVariables]
public bool Powered =>
- !Owner.TryGetComponent(out PowerReceiverComponent? receiver) ||
+ !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) ||
receiver.Powered;
[ViewVariables]
@@ -314,7 +314,7 @@ namespace Content.Server.Disposal.Unit.Components
private void TogglePower()
{
- if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver))
+ if (!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{
return;
}
diff --git a/Content.Server/Doors/Components/AirlockComponent.cs b/Content.Server/Doors/Components/AirlockComponent.cs
index d810e71c0b..ae59d4a65c 100644
--- a/Content.Server/Doors/Components/AirlockComponent.cs
+++ b/Content.Server/Doors/Components/AirlockComponent.cs
@@ -3,7 +3,7 @@ using System;
using System.Threading;
using Content.Server.Power.Components;
using Content.Server.VendingMachines;
-using Content.Server.Wires.Components;
+using Content.Server.WireHacking;
using Content.Shared.Doors;
using Content.Shared.Interaction;
using Content.Shared.Notification;
@@ -36,7 +36,7 @@ namespace Content.Server.Doors.Components
private readonly SharedAppearanceComponent? _appearanceComponent = null;
[ComponentDependency]
- private readonly PowerReceiverComponent? _receiverComponent = null;
+ private readonly ApcPowerReceiverComponent? _receiverComponent = null;
[ComponentDependency]
private readonly WiresComponent? _wiresComponent = null;
diff --git a/Content.Server/Gravity/GravityGeneratorComponent.cs b/Content.Server/Gravity/GravityGeneratorComponent.cs
index 207f032697..b148562c72 100644
--- a/Content.Server/Gravity/GravityGeneratorComponent.cs
+++ b/Content.Server/Gravity/GravityGeneratorComponent.cs
@@ -26,7 +26,7 @@ namespace Content.Server.Gravity
private GravityGeneratorStatus _status;
- public bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ public bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
public bool SwitchedOn => _switchedOn;
diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs
index 3a9682d0b3..e6f5d5238a 100644
--- a/Content.Server/IoC/ServerContentIoC.cs
+++ b/Content.Server/IoC/ServerContentIoC.cs
@@ -50,7 +50,6 @@ namespace Content.Server.IoC
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
- IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
diff --git a/Content.Server/Kitchen/Components/MicrowaveComponent.cs b/Content.Server/Kitchen/Components/MicrowaveComponent.cs
index 26ff81bc50..a6453558c5 100644
--- a/Content.Server/Kitchen/Components/MicrowaveComponent.cs
+++ b/Content.Server/Kitchen/Components/MicrowaveComponent.cs
@@ -67,7 +67,7 @@ namespace Content.Server.Kitchen.Components
[ViewVariables]
private uint _currentCookTimerTime = 1;
- private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private bool _hasContents => Owner.TryGetComponent(out SolutionContainerComponent? solution) && (solution.ReagentList.Count > 0 || _storage.ContainedEntities.Count > 0);
private bool _uiDirty = true;
private bool _lostPower = false;
diff --git a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs b/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs
index 2ea4a7a955..4a9ff99166 100644
--- a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs
+++ b/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs
@@ -53,7 +53,7 @@ namespace Content.Server.Kitchen.Components
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ReagentGrinderUiKey.Key);
- private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
///
/// Should the BoundUI be told to update?
diff --git a/Content.Server/Lathe/Components/LatheComponent.cs b/Content.Server/Lathe/Components/LatheComponent.cs
index 0bd32992d2..f5494064b8 100644
--- a/Content.Server/Lathe/Components/LatheComponent.cs
+++ b/Content.Server/Lathe/Components/LatheComponent.cs
@@ -42,7 +42,7 @@ namespace Content.Server.Lathe.Components
[ViewVariables]
private LatheRecipePrototype? _producingRecipe;
[ViewVariables]
- private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private static readonly TimeSpan InsertionTime = TimeSpan.FromSeconds(0.9f);
diff --git a/Content.Server/Light/Components/EmergencyLightComponent.cs b/Content.Server/Light/Components/EmergencyLightComponent.cs
index 667ba5c925..f775c18e52 100644
--- a/Content.Server/Light/Components/EmergencyLightComponent.cs
+++ b/Content.Server/Light/Components/EmergencyLightComponent.cs
@@ -1,9 +1,9 @@
#nullable enable
using System;
using System.Collections.Generic;
-using Content.Server.Battery.Components;
using Content.Server.Power.Components;
using Content.Shared.Examine;
+using Content.Shared.Light.Component;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
@@ -60,7 +60,7 @@ namespace Content.Server.Light.Components
///
public void UpdateState()
{
- if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver))
+ if (!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{
return;
}
@@ -80,7 +80,7 @@ namespace Content.Server.Light.Components
public void OnUpdate(float frameTime)
{
- if (Owner.Deleted || !Owner.TryGetComponent(out BatteryComponent? battery))
+ if (Owner.Deleted || !Owner.TryGetComponent(out BatteryComponent? battery) || Owner.Paused)
{
return;
}
@@ -96,9 +96,9 @@ namespace Content.Server.Light.Components
else
{
battery.CurrentCharge += _chargingWattage * frameTime * _chargingEfficiency;
- if (battery.BatteryState == BatteryState.Full)
+ if (battery.IsFullyCharged)
{
- if (Owner.TryGetComponent(out PowerReceiverComponent? receiver))
+ if (Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{
receiver.Load = 1;
}
@@ -110,28 +110,24 @@ namespace Content.Server.Light.Components
private void TurnOff()
{
- if (Owner.TryGetComponent(out SpriteComponent? sprite))
- {
- sprite.LayerSetState(0, "emergency_light_off");
- }
-
if (Owner.TryGetComponent(out PointLightComponent? light))
{
light.Enabled = false;
}
+
+ if (Owner.TryGetComponent(out AppearanceComponent? appearance))
+ appearance.SetData(EmergencyLightVisuals.On, false);
}
private void TurnOn()
{
- if (Owner.TryGetComponent(out SpriteComponent? sprite))
- {
- sprite.LayerSetState(0, "emergency_light_on");
- }
-
if (Owner.TryGetComponent(out PointLightComponent? light))
{
light.Enabled = true;
}
+
+ if (Owner.TryGetComponent(out AppearanceComponent? appearance))
+ appearance.SetData(EmergencyLightVisuals.On, true);
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
diff --git a/Content.Server/Light/Components/PoweredLightComponent.cs b/Content.Server/Light/Components/PoweredLightComponent.cs
index 2bc11a73f4..3e5ecd5b19 100644
--- a/Content.Server/Light/Components/PoweredLightComponent.cs
+++ b/Content.Server/Light/Components/PoweredLightComponent.cs
@@ -198,7 +198,7 @@ namespace Content.Server.Light.Components
///
public void UpdateLight()
{
- var powerReceiver = Owner.GetComponent();
+ var powerReceiver = Owner.GetComponent();
if (LightBulb == null) // No light bulb.
{
diff --git a/Content.Server/Medical/Components/MedicalScannerComponent.cs b/Content.Server/Medical/Components/MedicalScannerComponent.cs
index bc677a2503..0ee7e60964 100644
--- a/Content.Server/Medical/Components/MedicalScannerComponent.cs
+++ b/Content.Server/Medical/Components/MedicalScannerComponent.cs
@@ -48,7 +48,7 @@ namespace Content.Server.Medical.Components
private readonly Vector2 _ejectOffset = new(0f, 0f);
[ViewVariables]
- private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+ private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables]
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(MedicalScannerUiKey.Key);
diff --git a/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs b/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs
index 519ddee323..19b9de4415 100644
--- a/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs
+++ b/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs
@@ -11,16 +11,51 @@ namespace Content.Server.NodeContainer.EntitySystems
{
base.Initialize();
+ SubscribeLocalEvent(OnInitEvent);
+ SubscribeLocalEvent(OnStartupEvent);
+ SubscribeLocalEvent(OnShutdownEvent);
SubscribeLocalEvent(OnAnchorStateChanged);
SubscribeLocalEvent(OnRotateEvent);
}
- private void OnAnchorStateChanged(EntityUid uid, NodeContainerComponent component, AnchorStateChangedEvent args)
+ private static void OnInitEvent(EntityUid uid, NodeContainerComponent component, ComponentInit args)
{
- component.AnchorUpdate();
+ foreach (var (key, node) in component.Nodes)
+ {
+ node.Name = key;
+ node.Initialize(component.Owner);
+ }
}
- private void OnRotateEvent(EntityUid uid, NodeContainerComponent container, RotateEvent ev)
+ private static void OnStartupEvent(EntityUid uid, NodeContainerComponent component, ComponentStartup args)
+ {
+ foreach (var node in component.Nodes.Values)
+ {
+ node.OnContainerStartup();
+ }
+ }
+
+ private static void OnShutdownEvent(EntityUid uid, NodeContainerComponent component, ComponentShutdown args)
+ {
+ foreach (var node in component.Nodes.Values)
+ {
+ node.OnContainerShutdown();
+ }
+ }
+
+ private static void OnAnchorStateChanged(
+ EntityUid uid,
+ NodeContainerComponent component,
+ AnchorStateChangedEvent args)
+ {
+ foreach (var node in component.Nodes.Values)
+ {
+ node.AnchorUpdate();
+ node.AnchorStateChanged();
+ }
+ }
+
+ private static void OnRotateEvent(EntityUid uid, NodeContainerComponent container, RotateEvent ev)
{
if (ev.NewRotation == ev.OldRotation)
{
diff --git a/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs b/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs
index c4a0a5e0a0..ca24e0f8fb 100644
--- a/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs
+++ b/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs
@@ -1,30 +1,385 @@
using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Content.Server.Administration.Managers;
using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.NodeContainer.Nodes;
+using Content.Shared.Administration;
+using Content.Shared.NodeContainer;
using JetBrains.Annotations;
+using Robust.Server.Player;
+using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Maths;
+using Robust.Shared.Utility;
namespace Content.Server.NodeContainer.EntitySystems
{
[UsedImplicitly]
public class NodeGroupSystem : EntitySystem
{
- private readonly HashSet _dirtyNodeGroups = new();
+ [Dependency] private readonly IEntityManager _entityManager = default!;
- public void AddDirtyNodeGroup(INodeGroup nodeGroup)
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IAdminManager _adminManager = default!;
+ [Dependency] private readonly INodeGroupFactory _nodeGroupFactory = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
+
+ private readonly List _visDeletes = new();
+ private readonly List _visSends = new();
+
+ private readonly HashSet _visPlayers = new();
+ private readonly HashSet _toRemake = new();
+ private readonly HashSet _toRemove = new();
+ private readonly List _toReflood = new();
+
+ private ISawmill _sawmill = default!;
+
+ public bool VisEnabled => _visPlayers.Count != 0;
+
+ private int _gen = 1;
+ private int _groupNetIdCounter = 1;
+
+ public override void Initialize()
{
- _dirtyNodeGroups.Add(nodeGroup);
+ base.Initialize();
+
+ _sawmill = _logManager.GetSawmill("nodegroup");
+
+ _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
+
+ SubscribeNetworkEvent(HandleEnableMsg);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+
+ _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
+ }
+
+ private void HandleEnableMsg(NodeVis.MsgEnable msg, EntitySessionEventArgs args)
+ {
+ var session = (IPlayerSession) args.SenderSession;
+ if (!_adminManager.HasAdminFlag(session, AdminFlags.Debug))
+ return;
+
+ if (msg.Enabled)
+ {
+ _visPlayers.Add(session);
+ VisSendFullStateImmediate(session);
+ }
+ else
+ {
+ _visPlayers.Remove(session);
+ }
+ }
+
+ private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
+ {
+ if (e.NewStatus == SessionStatus.Disconnected)
+ _visPlayers.Remove(e.Session);
+ }
+
+ public void QueueRemakeGroup(BaseNodeGroup group)
+ {
+ if (group.Remaking)
+ return;
+
+ _toRemake.Add(group);
+ group.Remaking = true;
+
+ foreach (var node in group.Nodes)
+ {
+ QueueReflood(node);
+ }
+ }
+
+ public void QueueReflood(Node node)
+ {
+ if (node.FlaggedForFlood)
+ return;
+
+ _toReflood.Add(node);
+ node.FlaggedForFlood = true;
+ }
+
+ public void QueueNodeRemove(Node node)
+ {
+ _toRemove.Add(node);
+ }
+
+ public void CreateSingleNetImmediate(Node node)
+ {
+ if (node.NodeGroup != null)
+ return;
+
+ QueueReflood(node);
+
+ InitGroup(node, new List {node});
}
public override void Update(float frameTime)
{
base.Update(frameTime);
- foreach (var group in _dirtyNodeGroups)
+ DoGroupUpdates();
+ VisDoUpdate();
+ }
+
+ private void DoGroupUpdates()
+ {
+ // "Why is there a separate queue for group remakes and node refloods when they both cause eachother"
+ // Future planning for the potential ability to do more intelligent group updating.
+
+ if (_toRemake.Count == 0 && _toReflood.Count == 0 && _toRemove.Count == 0)
+ return;
+
+ var sw = Stopwatch.StartNew();
+
+ foreach (var toRemove in _toRemove)
{
- group.RemakeGroup();
+ if (toRemove.NodeGroup == null)
+ continue;
+
+ var group = (BaseNodeGroup) toRemove.NodeGroup;
+
+ group.RemoveNode(toRemove);
+ toRemove.NodeGroup = null;
+
+ QueueRemakeGroup(group);
}
- _dirtyNodeGroups.Clear();
+ // Break up all remaking groups.
+ // Don't clear the list yet, we'll come back to these later.
+ foreach (var toRemake in _toRemake)
+ {
+ QueueRemakeGroup(toRemake);
+ }
+
+ _gen += 1;
+
+ // Go over all nodes to calculate reachable nodes and make an undirected graph out of them.
+ // Node.GetReachableNodes() may return results asymmetrically,
+ // i.e. node A may return B, but B may not return A.
+ //
+ // Must be for loop to allow concurrent modification from RemakeGroupImmediate.
+ for (var i = 0; i < _toReflood.Count; i++)
+ {
+ var node = _toReflood[i];
+
+ if (node.Deleting)
+ continue;
+
+ ClearReachableIfNecessary(node);
+
+ if (node.NodeGroup?.Remaking == false)
+ {
+ QueueRemakeGroup((BaseNodeGroup) node.NodeGroup);
+ }
+
+ foreach (var compatible in GetCompatibleNodes(node))
+ {
+ ClearReachableIfNecessary(compatible);
+
+ if (compatible.NodeGroup?.Remaking == false)
+ {
+ // We are expanding into an existing group,
+ // remake it so that we can treat it uniformly.
+ var group = (BaseNodeGroup) compatible.NodeGroup;
+ QueueRemakeGroup(group);
+ }
+
+ node.ReachableNodes.Add(compatible);
+ compatible.ReachableNodes.Add(node);
+ }
+ }
+
+ var newGroups = new List();
+
+ // Flood fill over nodes. Every node will only be flood filled once.
+ foreach (var node in _toReflood)
+ {
+ node.FlaggedForFlood = false;
+
+ // Check if already flood filled.
+ if (node.FloodGen == _gen || node.Deleting)
+ continue;
+
+ // Flood fill
+ var groupNodes = FloodFillNode(node);
+
+ var newGroup = InitGroup(node, groupNodes);
+ newGroups.Add(newGroup);
+ }
+
+ // Go over dead groups that need to be cleaned up.
+ // Tell them to push their data to new groups too.
+ foreach (var oldGroup in _toRemake)
+ {
+ // Group by the NEW group.
+ var newGrouped = oldGroup.Nodes.GroupBy(n => n.NodeGroup);
+
+ oldGroup.Removed = true;
+ oldGroup.AfterRemake(newGrouped);
+ if (VisEnabled)
+ _visDeletes.Add(oldGroup.NetId);
+ }
+
+ var refloodCount = _toReflood.Count;
+
+ _toReflood.Clear();
+ _toRemake.Clear();
+ _toRemove.Clear();
+
+ foreach (var group in newGroups)
+ {
+ foreach (var node in group.Nodes)
+ {
+ node.OnPostRebuild();
+ }
+ }
+
+ _sawmill.Debug($"Updated node groups in {sw.Elapsed.TotalMilliseconds}ms. {newGroups.Count} new groups, {refloodCount} nodes processed.");
+ }
+
+ private void ClearReachableIfNecessary(Node node)
+ {
+ if (node.UndirectGen != _gen)
+ {
+ node.ReachableNodes.Clear();
+ node.UndirectGen = _gen;
+ }
+ }
+
+ private BaseNodeGroup InitGroup(Node node, List groupNodes)
+ {
+ var newGroup = (BaseNodeGroup) _nodeGroupFactory.MakeNodeGroup(node.NodeGroupID);
+ newGroup.Initialize(node);
+ newGroup.NetId = _groupNetIdCounter++;
+
+ var netIdCounter = 0;
+ foreach (var groupNode in groupNodes)
+ {
+ groupNode.NodeGroup = newGroup;
+ groupNode.NetId = ++netIdCounter;
+ }
+
+ newGroup.LoadNodes(groupNodes);
+
+ if (VisEnabled)
+ _visSends.Add(newGroup);
+
+ return newGroup;
+ }
+
+ private List FloodFillNode(Node rootNode)
+ {
+ // All nodes we're filling into that currently have NO network.
+ var allNodes = new List();
+
+ var stack = new Stack();
+ stack.Push(rootNode);
+ rootNode.FloodGen = _gen;
+
+ while (stack.TryPop(out var node))
+ {
+ allNodes.Add(node);
+
+ foreach (var reachable in node.ReachableNodes)
+ {
+ if (reachable.FloodGen == _gen)
+ continue;
+
+ reachable.FloodGen = _gen;
+ stack.Push(reachable);
+ }
+ }
+
+ return allNodes;
+ }
+
+ private static IEnumerable GetCompatibleNodes(Node node)
+ {
+ foreach (var reachable in node.GetReachableNodes())
+ {
+ DebugTools.Assert(reachable != node, "GetReachableNodes() should not include self.");
+
+ if (reachable.Connectable && reachable.NodeGroupID == node.NodeGroupID)
+ yield return reachable;
+ }
+ }
+
+ private void VisDoUpdate()
+ {
+ if (_visSends.Count == 0 && _visDeletes.Count == 0)
+ return;
+
+ var msg = new NodeVis.MsgData();
+
+ msg.GroupDeletions.AddRange(_visDeletes);
+ msg.Groups.AddRange(_visSends.Select(VisMakeGroupState));
+
+ _visSends.Clear();
+ _visDeletes.Clear();
+
+ foreach (var player in _visPlayers)
+ {
+ RaiseNetworkEvent(msg, player.ConnectedClient);
+ }
+ }
+
+ private void VisSendFullStateImmediate(IPlayerSession player)
+ {
+ var msg = new NodeVis.MsgData();
+
+ var allNetworks = ComponentManager
+ .EntityQuery()
+ .SelectMany(nc => nc.Nodes.Values)
+ .Select(n => (BaseNodeGroup?) n.NodeGroup)
+ .Where(n => n != null)
+ .Distinct();
+
+ foreach (var network in allNetworks)
+ {
+ msg.Groups.Add(VisMakeGroupState(network!));
+ }
+
+ RaiseNetworkEvent(msg, player.ConnectedClient);
+ }
+
+ private static NodeVis.GroupData VisMakeGroupState(BaseNodeGroup group)
+ {
+ return new()
+ {
+ NetId = group.NetId,
+ GroupId = group.GroupId.ToString(),
+ Color = CalcNodeGroupColor(group),
+ Nodes = group.Nodes.Select(n => new NodeVis.NodeDatum
+ {
+ Name = n.Name,
+ NetId = n.NetId,
+ Reachable = n.ReachableNodes.Select(r => r.NetId).ToArray(),
+ Entity = n.Owner.Uid,
+ Type = n.GetType().Name
+ }).ToArray()
+ };
+ }
+
+ private static Color CalcNodeGroupColor(BaseNodeGroup group)
+ {
+ return group.GroupId switch
+ {
+ NodeGroupID.HVPower => Color.Orange,
+ NodeGroupID.MVPower => Color.Yellow,
+ NodeGroupID.Apc => Color.LimeGreen,
+ NodeGroupID.AMEngine => Color.Purple,
+ NodeGroupID.Pipe => Color.Blue,
+ NodeGroupID.WireNet => Color.DarkMagenta,
+ _ => Color.White
+ };
}
}
}
diff --git a/Content.Server/NodeContainer/NodeContainerComponent.cs b/Content.Server/NodeContainer/NodeContainerComponent.cs
index 2c1e4841bf..b0943f7cb6 100644
--- a/Content.Server/NodeContainer/NodeContainerComponent.cs
+++ b/Content.Server/NodeContainer/NodeContainerComponent.cs
@@ -20,60 +20,18 @@ namespace Content.Server.NodeContainer
{
public override string Name => "NodeContainer";
- [ViewVariables]
- public IReadOnlyDictionary Nodes => _nodes;
+ [DataField("nodes")] [ViewVariables] public Dictionary Nodes { get; } = new();
- [DataField("nodes")]
- private readonly Dictionary _nodes = new();
-
- [DataField("examinable")]
- private bool _examinable = false;
-
- protected override void Initialize()
- {
- base.Initialize();
- foreach (var node in _nodes.Values)
- {
- node.Initialize(Owner);
- }
- }
-
- protected override void Startup()
- {
- base.Startup();
- foreach (var node in _nodes.Values)
- {
- node.OnContainerStartup();
- }
- }
-
- protected override void Shutdown()
- {
- base.Shutdown();
-
- foreach (var node in _nodes.Values)
- {
- node.OnContainerShutdown();
- }
- }
-
- public void AnchorUpdate()
- {
- foreach (var node in Nodes.Values)
- {
- node.AnchorUpdate();
- node.AnchorStateChanged();
- }
- }
+ [DataField("examinable")] private bool _examinable = false;
public T GetNode(string identifier) where T : Node
{
- return (T)_nodes[identifier];
+ return (T) Nodes[identifier];
}
public bool TryGetNode(string identifier, [NotNullWhen(true)] out T? node) where T : Node
{
- if (_nodes.TryGetValue(identifier, out var n) && n is T t)
+ if (Nodes.TryGetValue(identifier, out var n) && n is T t)
{
node = t;
return true;
diff --git a/Content.Server/NodeContainer/NodeGroups/BaseNetConnectorNodeGroup.cs b/Content.Server/NodeContainer/NodeGroups/BaseNetConnectorNodeGroup.cs
deleted file mode 100644
index bee595ac19..0000000000
--- a/Content.Server/NodeContainer/NodeGroups/BaseNetConnectorNodeGroup.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using System.Linq;
-using Content.Server.NodeContainer.Nodes;
-using Content.Server.Power.Components;
-
-namespace Content.Server.NodeContainer.NodeGroups
-{
- public abstract class BaseNetConnectorNodeGroup : BaseNodeGroup where TNetConnector : BaseNetConnectorComponent
- {
- private readonly Dictionary> _netConnectorComponents = new();
-
- protected override void OnAddNode(Node node)
- {
- var newNetConnectorComponents = node.Owner
- .GetAllComponents()
- .Where(powerComp => (NodeGroupID) powerComp.Voltage == node.NodeGroupID)
- .ToList();
- _netConnectorComponents[node] = newNetConnectorComponents;
- foreach (var netConnectorComponent in newNetConnectorComponents)
- {
- SetNetConnectorNet(netConnectorComponent);
- }
- }
-
- protected abstract void SetNetConnectorNet(TNetConnector netConnectorComponent);
-
- protected override void OnRemoveNode(Node node)
- {
- foreach (var netConnectorComponent in _netConnectorComponents[node])
- {
- netConnectorComponent.ClearNet();
- }
- _netConnectorComponents.Remove(node);
- }
- }
-}
diff --git a/Content.Server/NodeContainer/NodeGroups/BaseNodeGroup.cs b/Content.Server/NodeContainer/NodeGroups/BaseNodeGroup.cs
new file mode 100644
index 0000000000..133c8c3a33
--- /dev/null
+++ b/Content.Server/NodeContainer/NodeGroups/BaseNodeGroup.cs
@@ -0,0 +1,91 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.NodeContainer.EntitySystems;
+using Content.Server.NodeContainer.Nodes;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.NodeContainer.NodeGroups
+{
+ ///
+ /// Maintains a collection of s, and performs operations requiring a list of
+ /// all connected s.
+ ///
+ public interface INodeGroup
+ {
+ bool Remaking { get; }
+
+ IReadOnlyList Nodes { get; }
+
+ void Create(NodeGroupID groupId);
+
+ void Initialize(Node sourceNode);
+
+ void RemoveNode(Node node);
+
+ void LoadNodes(List groupNodes);
+
+ // In theory, the SS13 curse ensures this method will never be called.
+ void AfterRemake(IEnumerable> newGroups);
+
+ // TODO: Why is this method needed?
+ void QueueRemake();
+ }
+
+ [NodeGroup(NodeGroupID.Default, NodeGroupID.WireNet)]
+ public class BaseNodeGroup : INodeGroup
+ {
+ public bool Remaking { get; set; }
+
+ IReadOnlyList INodeGroup.Nodes => Nodes;
+
+ [ViewVariables] public readonly List Nodes = new();
+
+ [ViewVariables] public int NodeCount => Nodes.Count;
+
+ ///
+ /// Debug variable to indicate that this NodeGroup should not be being used by anything.
+ ///
+ [ViewVariables]
+ public bool Removed { get; set; } = false;
+
+ [ViewVariables]
+ protected GridId GridId { get; private set; }
+
+ [ViewVariables]
+ public int NetId;
+
+ [ViewVariables]
+ public NodeGroupID GroupId { get; private set; }
+
+ public void Create(NodeGroupID groupId)
+ {
+ GroupId = groupId;
+ }
+
+ public virtual void Initialize(Node sourceNode)
+ {
+ // TODO: Can we get rid of this GridId?
+ GridId = sourceNode.Owner.Transform.GridID;
+ }
+
+ public virtual void RemoveNode(Node node)
+ {
+ }
+
+ public virtual void LoadNodes(
+ List groupNodes)
+ {
+ Nodes.AddRange(groupNodes);
+ }
+
+ public virtual void AfterRemake(IEnumerable> newGroups) { }
+
+ public void QueueRemake()
+ {
+ EntitySystem.Get().QueueRemakeGroup(this);
+ }
+ }
+}
diff --git a/Content.Server/NodeContainer/NodeGroups/INodeGroup.cs b/Content.Server/NodeContainer/NodeGroups/INodeGroup.cs
deleted file mode 100644
index bc8868bc11..0000000000
--- a/Content.Server/NodeContainer/NodeGroups/INodeGroup.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using Content.Server.NodeContainer.EntitySystems;
-using Content.Server.NodeContainer.Nodes;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Server.NodeContainer.NodeGroups
-{
- ///
- /// Maintains a collection of s, and performs operations requiring a list of
- /// all connected s.
- ///
- public interface INodeGroup
- {
- IReadOnlyList Nodes { get; }
-
- void Initialize(Node sourceNode);
-
- void AddNode(Node node);
-
- void RemoveNode(Node node);
-
- void CombineGroup(INodeGroup newGroup);
-
- void RemakeGroup();
- }
-
- [NodeGroup(NodeGroupID.Default, NodeGroupID.WireNet)]
- public class BaseNodeGroup : INodeGroup
- {
- [ViewVariables]
- public IReadOnlyList Nodes => _nodes;
- private readonly List _nodes = new();
-
- [ViewVariables]
- public int NodeCount => Nodes.Count;
-
- ///
- /// Debug variable to indicate that this NodeGroup should not be being used by anything.
- ///
- [ViewVariables]
- public bool Removed { get; private set; } = false;
-
- public static readonly INodeGroup NullGroup = new NullNodeGroup();
-
- protected GridId GridId { get; private set;}
-
- public virtual void Initialize(Node sourceNode)
- {
- GridId = sourceNode.Owner.Transform.GridID;
- }
-
- public void AddNode(Node node)
- {
- _nodes.Add(node);
- OnAddNode(node);
- }
-
- public void RemoveNode(Node node)
- {
- _nodes.Remove(node);
- OnRemoveNode(node);
- EntitySystem.Get().AddDirtyNodeGroup(this);
- }
-
- public void CombineGroup(INodeGroup newGroup)
- {
- if (newGroup.Nodes.Count < Nodes.Count)
- {
- newGroup.CombineGroup(this);
- return;
- }
-
- OnGivingNodesForCombine(newGroup);
-
- foreach (var node in Nodes)
- {
- node.NodeGroup = newGroup;
- }
-
- Removed = true;
- }
-
- ///
- /// Causes all s to remake their groups. Called when a is removed
- /// and may have split a group in two, so multiple new groups may need to be formed.
- ///
- public void RemakeGroup()
- {
- foreach (var node in Nodes)
- {
- node.ClearNodeGroup();
- }
-
- var newGroups = new HashSet();
-
- foreach (var node in Nodes)
- {
- if (node.TryAssignGroupIfNeeded())
- {
- node.SpreadGroup();
- newGroups.Add(node.NodeGroup);
- }
- }
-
- AfterRemake(newGroups);
-
- Removed = true;
- }
-
- protected virtual void OnAddNode(Node node) { }
-
- protected virtual void OnRemoveNode(Node node) { }
-
- protected virtual void OnGivingNodesForCombine(INodeGroup newGroup) { }
-
- protected virtual void AfterRemake(IEnumerable newGroups) { }
-
- protected class NullNodeGroup : INodeGroup
- {
- public IReadOnlyList Nodes => _nodes;
- private readonly List _nodes = new();
- public void Initialize(Node sourceNode) { }
- public void AddNode(Node node) { }
- public void CombineGroup(INodeGroup newGroup) { }
- public void RemoveNode(Node node) { }
- public void RemakeGroup() { }
- }
- }
-}
diff --git a/Content.Server/NodeContainer/NodeGroups/NodeGroupAttribute.cs b/Content.Server/NodeContainer/NodeGroups/NodeGroupAttribute.cs
index d41b8dc265..27ef01eafc 100644
--- a/Content.Server/NodeContainer/NodeGroups/NodeGroupAttribute.cs
+++ b/Content.Server/NodeContainer/NodeGroups/NodeGroupAttribute.cs
@@ -1,5 +1,6 @@
#nullable enable
using System;
+using JetBrains.Annotations;
namespace Content.Server.NodeContainer.NodeGroups
{
@@ -9,6 +10,7 @@ namespace Content.Server.NodeContainer.NodeGroups
/// have the same type of . Used by .
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ [MeansImplicitUse]
public class NodeGroupAttribute : Attribute
{
public NodeGroupID[] NodeGroupIDs { get; }
diff --git a/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs b/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs
index 0391439ec3..33b81a55d5 100644
--- a/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs
+++ b/Content.Server/NodeContainer/NodeGroups/NodeGroupFactory.cs
@@ -19,7 +19,7 @@ namespace Content.Server.NodeContainer.NodeGroups
///
/// Returns a new instance.
///
- INodeGroup MakeNodeGroup(Node sourceNode);
+ INodeGroup MakeNodeGroup(NodeGroupID id);
}
public class NodeGroupFactory : INodeGroupFactory
@@ -45,15 +45,14 @@ namespace Content.Server.NodeContainer.NodeGroups
}
}
- public INodeGroup MakeNodeGroup(Node sourceNode)
+ public INodeGroup MakeNodeGroup(NodeGroupID id)
{
- if (_groupTypes.TryGetValue(sourceNode.NodeGroupID, out var type))
- {
- var nodeGroup = _typeFactory.CreateInstance(type);
- nodeGroup.Initialize(sourceNode);
- return nodeGroup;
- }
- throw new ArgumentException($"{sourceNode.NodeGroupID} did not have an associated {nameof(INodeGroup)}.");
+ if (!_groupTypes.TryGetValue(id, out var type))
+ throw new ArgumentException($"{id} did not have an associated {nameof(INodeGroup)} implementation.");
+
+ var instance = _typeFactory.CreateInstance(type);
+ instance.Create(id);
+ return instance;
}
}
diff --git a/Content.Server/NodeContainer/NodeGroups/IPipeNet.cs b/Content.Server/NodeContainer/NodeGroups/PipeNet.cs
similarity index 51%
rename from Content.Server/NodeContainer/NodeGroups/IPipeNet.cs
rename to Content.Server/NodeContainer/NodeGroups/PipeNet.cs
index ed740b39a4..573694cdd6 100644
--- a/Content.Server/NodeContainer/NodeGroups/IPipeNet.cs
+++ b/Content.Server/NodeContainer/NodeGroups/PipeNet.cs
@@ -1,14 +1,15 @@
#nullable enable
using System;
using System.Collections.Generic;
+using System.Linq;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
-using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.Interfaces;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
+using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Content.Server.NodeContainer.NodeGroups
@@ -24,17 +25,15 @@ namespace Content.Server.NodeContainer.NodeGroups
[NodeGroup(NodeGroupID.Pipe)]
public class PipeNet : BaseNodeGroup, IPipeNet
{
- [ViewVariables]
- public GasMixture Air { get; set; } = new() {Temperature = Atmospherics.T20C};
+ [ViewVariables] public GasMixture Air { get; set; } = new() {Temperature = Atmospherics.T20C};
- public static readonly IPipeNet NullNet = new NullPipeNet();
-
- [ViewVariables]
- private readonly List _pipes = new();
+ [ViewVariables] private readonly List _pipes = new();
[ViewVariables] private AtmosphereSystem? _atmosphereSystem;
- [ViewVariables] private IGridAtmosphereComponent? GridAtmos => _atmosphereSystem?.GetGridAtmosphere(GridId);
+ [ViewVariables]
+ private IGridAtmosphereComponent? GridAtmos =>
+ _atmosphereSystem?.GetGridAtmosphere(GridId);
public override void Initialize(Node sourceNode)
{
@@ -49,35 +48,30 @@ namespace Content.Server.NodeContainer.NodeGroups
_atmosphereSystem?.React(Air, this);
}
- protected override void OnAddNode(Node node)
+ public override void LoadNodes(List groupNodes)
{
- if (node is not PipeNode pipeNode)
- return;
+ base.LoadNodes(groupNodes);
- _pipes.Add(pipeNode);
- pipeNode.JoinPipeNet(this);
- Air.Volume += pipeNode.Volume;
+ foreach (var node in groupNodes)
+ {
+ var pipeNode = (PipeNode) node;
+ _pipes.Add(pipeNode);
+ pipeNode.JoinPipeNet(this);
+ Air.Volume += pipeNode.Volume;
+ }
}
- protected override void OnRemoveNode(Node node)
+ public override void RemoveNode(Node node)
{
- RemoveFromGridAtmos();
- if (node is not PipeNode pipeNode)
- return;
+ base.RemoveNode(node);
- pipeNode.ClearPipeNet();
+ var pipeNode = (PipeNode) node;
+ Air.Volume -= pipeNode.Volume;
+ // TODO: Bad O(n^2)
_pipes.Remove(pipeNode);
}
- protected override void OnGivingNodesForCombine(INodeGroup newGroup)
- {
- if (newGroup is not IPipeNet newPipeNet)
- return;
-
- EntitySystem.Get().Merge(newPipeNet.Air, Air);
- }
-
- protected override void AfterRemake(IEnumerable newGroups)
+ public override void AfterRemake(IEnumerable> newGroups)
{
RemoveFromGridAtmos();
@@ -86,14 +80,15 @@ namespace Content.Server.NodeContainer.NodeGroups
foreach (var newGroup in newGroups)
{
- if (newGroup is not IPipeNet newPipeNet)
+ if (newGroup.Key is not IPipeNet newPipeNet)
continue;
var newAir = newPipeNet.Air;
+ var newVolume = newGroup.Cast().Sum(n => n.Volume);
buffer.Clear();
atmosphereSystem.Merge(buffer, Air);
- buffer.Multiply(MathF.Min(newAir.Volume / Air.Volume, 1f));
+ buffer.Multiply(MathF.Min(newVolume / Air.Volume, 1f));
atmosphereSystem.Merge(newAir, buffer);
}
}
@@ -102,20 +97,5 @@ namespace Content.Server.NodeContainer.NodeGroups
{
GridAtmos?.RemovePipeNet(this);
}
-
- private class NullPipeNet : NullNodeGroup, IPipeNet
- {
- private readonly GasMixture _air;
-
- GasMixture IGasMixtureHolder.Air { get => _air; set { } }
-
- public NullPipeNet()
- {
- _air = new GasMixture(1f) {Temperature = Atmospherics.T20C};
- _air.MarkImmutable();
- }
-
- public void Update() { }
- }
}
}
diff --git a/Content.Server/NodeContainer/NodeGroups/PowerNetNodeGroup.cs b/Content.Server/NodeContainer/NodeGroups/PowerNetNodeGroup.cs
deleted file mode 100644
index 331a3de40f..0000000000
--- a/Content.Server/NodeContainer/NodeGroups/PowerNetNodeGroup.cs
+++ /dev/null
@@ -1,164 +0,0 @@
-#nullable enable
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using Content.Server.Power.Components;
-using Robust.Shared.IoC;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Server.NodeContainer.NodeGroups
-{
- public interface IPowerNet
- {
- void AddSupplier(PowerSupplierComponent supplier);
-
- void RemoveSupplier(PowerSupplierComponent supplier);
-
- void UpdateSupplierSupply(PowerSupplierComponent supplier, int oldSupplyRate, int newSupplyRate);
-
- void AddConsumer(PowerConsumerComponent consumer);
-
- void RemoveConsumer(PowerConsumerComponent consumer);
-
- void UpdateConsumerDraw(PowerConsumerComponent consumer, int oldDrawRate, int newDrawRate);
-
- void UpdateConsumerPriority(PowerConsumerComponent consumer, Priority oldPriority, Priority newPriority);
-
- void UpdateConsumerReceivedPower();
- }
-
- [NodeGroup(NodeGroupID.HVPower, NodeGroupID.MVPower)]
- public class PowerNetNodeGroup : BaseNetConnectorNodeGroup, IPowerNet
- {
- private static readonly Priority[] CachedPriorities = (Priority[]) Enum.GetValues(typeof(Priority));
-
- [Dependency] private readonly IPowerNetManager _powerNetManager = default!;
-
- [ViewVariables]
- private readonly List _suppliers = new();
-
- [ViewVariables]
- private int _totalSupply = 0;
-
- [ViewVariables]
- private readonly Dictionary> _consumersByPriority = new();
-
- [ViewVariables]
- private readonly Dictionary _drawByPriority = new();
-
- public static readonly IPowerNet NullNet = new NullPowerNet();
-
- public PowerNetNodeGroup()
- {
- foreach (Priority priority in Enum.GetValues(typeof(Priority)))
- {
- _consumersByPriority.Add(priority, new List());
- _drawByPriority.Add(priority, 0);
- }
- }
-
- protected override void SetNetConnectorNet(BasePowerNetComponent netConnectorComponent)
- {
- netConnectorComponent.Net = this;
- }
-
- #region IPowerNet Methods
-
- public void AddSupplier(PowerSupplierComponent supplier)
- {
- _suppliers.Add(supplier);
- _totalSupply += supplier.SupplyRate;
- _powerNetManager.AddDirtyPowerNet(this);
- }
-
- public void RemoveSupplier(PowerSupplierComponent supplier)
- {
- Debug.Assert(_suppliers.Contains(supplier));
- _suppliers.Remove(supplier);
- _totalSupply -= supplier.SupplyRate;
- _powerNetManager.AddDirtyPowerNet(this);
- }
-
- public void UpdateSupplierSupply(PowerSupplierComponent supplier, int oldSupplyRate, int newSupplyRate)
- {
- Debug.Assert(_suppliers.Contains(supplier));
- _totalSupply -= oldSupplyRate;
- _totalSupply += newSupplyRate;
- _powerNetManager.AddDirtyPowerNet(this);
- }
-
- public void AddConsumer(PowerConsumerComponent consumer)
- {
- _consumersByPriority[consumer.Priority].Add(consumer);
- _drawByPriority[consumer.Priority] += consumer.DrawRate;
- _powerNetManager.AddDirtyPowerNet(this);
- }
-
- public void RemoveConsumer(PowerConsumerComponent consumer)
- {
- Debug.Assert(_consumersByPriority[consumer.Priority].Contains(consumer));
- consumer.ReceivedPower = 0;
- _consumersByPriority[consumer.Priority].Remove(consumer);
- _drawByPriority[consumer.Priority] -= consumer.DrawRate;
- _powerNetManager.AddDirtyPowerNet(this);
- }
-
- public void UpdateConsumerDraw(PowerConsumerComponent consumer, int oldDrawRate, int newDrawRate)
- {
- Debug.Assert(_consumersByPriority[consumer.Priority].Contains(consumer));
- _drawByPriority[consumer.Priority] -= oldDrawRate;
- _drawByPriority[consumer.Priority] += newDrawRate;
- _powerNetManager.AddDirtyPowerNet(this);
- }
-
- public void UpdateConsumerPriority(PowerConsumerComponent consumer, Priority oldPriority, Priority newPriority)
- {
- Debug.Assert(_consumersByPriority[oldPriority].Contains(consumer));
- _consumersByPriority[oldPriority].Remove(consumer);
- _drawByPriority[oldPriority] -= consumer.DrawRate;
- _consumersByPriority[newPriority].Add(consumer);
- _drawByPriority[newPriority] += consumer.DrawRate;
- _powerNetManager.AddDirtyPowerNet(this);
- }
-
- public void UpdateConsumerReceivedPower()
- {
- var remainingSupply = _totalSupply;
- foreach (var priority in CachedPriorities)
- {
- var categoryPowerDemand = _drawByPriority[priority];
- if (remainingSupply >= categoryPowerDemand) //can fully power all in category
- {
- remainingSupply -= categoryPowerDemand;
- foreach (var consumer in _consumersByPriority[priority])
- {
- consumer.ReceivedPower = consumer.DrawRate;
- }
- }
- else //cannot fully power all, split power
- {
- var availiablePowerFraction = (float) remainingSupply / categoryPowerDemand;
- remainingSupply = 0;
- foreach (var consumer in _consumersByPriority[priority])
- {
- consumer.ReceivedPower = (int) (consumer.DrawRate * availiablePowerFraction); //give each consumer a fraction of what they requested (rounded down to nearest int)
- }
- }
- }
- }
-
- #endregion
-
- private class NullPowerNet : IPowerNet
- {
- public void AddConsumer(PowerConsumerComponent consumer) { }
- public void AddSupplier(PowerSupplierComponent supplier) { }
- public void UpdateSupplierSupply(PowerSupplierComponent supplier, int oldSupplyRate, int newSupplyRate) { }
- public void RemoveConsumer(PowerConsumerComponent consumer) { }
- public void RemoveSupplier(PowerSupplierComponent supplier) { }
- public void UpdateConsumerDraw(PowerConsumerComponent consumer, int oldDrawRate, int newDrawRate) { }
- public void UpdateConsumerPriority(PowerConsumerComponent consumer, Priority oldPriority, Priority newPriority) { }
- public void UpdateConsumerReceivedPower() { }
- }
- }
-}
diff --git a/Content.Server/NodeContainer/Nodes/AdjacentNode.cs b/Content.Server/NodeContainer/Nodes/AdjacentNode.cs
index 43e9c78c1d..c28589aec9 100644
--- a/Content.Server/NodeContainer/Nodes/AdjacentNode.cs
+++ b/Content.Server/NodeContainer/Nodes/AdjacentNode.cs
@@ -1,5 +1,6 @@
#nullable enable
using System.Collections.Generic;
+using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -12,29 +13,19 @@ namespace Content.Server.NodeContainer.Nodes
[DataDefinition]
public class AdjacentNode : Node
{
- protected override IEnumerable GetReachableNodes()
+ public override IEnumerable GetReachableNodes()
{
if (!Owner.Transform.Anchored)
yield break;
+ var compMgr = IoCManager.Resolve();
var grid = IoCManager.Resolve().GetGrid(Owner.Transform.GridID);
- var coords = Owner.Transform.Coordinates;
- foreach (var cell in grid.GetCardinalNeighborCells(coords))
+ var gridIndex = grid.TileIndicesFor(Owner.Transform.Coordinates);
+
+ foreach (var (_, node) in NodeHelpers.GetCardinalNeighborNodes(compMgr, grid, gridIndex))
{
- foreach (var entity in grid.GetLocal(Owner.EntityManager.GetEntity(cell).Transform.Coordinates))
- {
- if (!Owner.EntityManager.GetEntity(entity).TryGetComponent(out var container))
- continue;
-
- foreach (var node in container.Nodes.Values)
- {
- if (node != null && node != this)
- {
- yield return node;
- }
- }
-
- }
+ if (node != this)
+ yield return node;
}
}
}
diff --git a/Content.Server/NodeContainer/Nodes/Node.cs b/Content.Server/NodeContainer/Nodes/Node.cs
index 614a5bff4c..e427d0d8ec 100644
--- a/Content.Server/NodeContainer/Nodes/Node.cs
+++ b/Content.Server/NodeContainer/Nodes/Node.cs
@@ -1,11 +1,8 @@
#nullable enable
using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
+using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
@@ -26,20 +23,14 @@ namespace Content.Server.NodeContainer.Nodes
[DataField("nodeGroupID")]
public NodeGroupID NodeGroupID { get; private set; } = NodeGroupID.Default;
- [ViewVariables]
- public INodeGroup NodeGroup { get => _nodeGroup; set => SetNodeGroup(value); }
- private INodeGroup _nodeGroup = BaseNodeGroup.NullGroup;
+ [ViewVariables] public INodeGroup? NodeGroup;
- [ViewVariables]
- public IEntity Owner { get; private set; } = default!;
-
- [ViewVariables]
- private bool _needsGroup = true;
+ [ViewVariables] public IEntity Owner { get; private set; } = default!;
///
/// If this node should be considered for connection by other nodes.
///
- public bool Connectable => !_deleting && Anchored;
+ public bool Connectable => !Deleting && Anchored;
protected bool Anchored => !NeedAnchored || Owner.Transform.Anchored;
@@ -50,7 +41,15 @@ namespace Content.Server.NodeContainer.Nodes
///
/// Prevents a node from being used by other nodes while midway through removal.
///
- private bool _deleting;
+ public bool Deleting;
+
+ public readonly HashSet ReachableNodes = new();
+
+ internal int FloodGen;
+ internal int UndirectGen;
+ internal bool FlaggedForFlood;
+ internal int NetId;
+ public string Name = default!;
public virtual void Initialize(IEntity owner)
{
@@ -59,20 +58,23 @@ namespace Content.Server.NodeContainer.Nodes
public virtual void OnContainerStartup()
{
- TryAssignGroupIfNeeded();
- CombineGroupWithReachable();
+ EntitySystem.Get().QueueReflood(this);
+ }
+
+ public void CreateSingleNetImmediate()
+ {
+ EntitySystem.Get().CreateSingleNetImmediate(this);
}
public void AnchorUpdate()
{
if (Anchored)
{
- TryAssignGroupIfNeeded();
- CombineGroupWithReachable();
+ EntitySystem.Get().QueueReflood(this);
}
else
{
- RemoveSelfFromGroup();
+ EntitySystem.Get().QueueNodeRemove(this);
}
}
@@ -80,107 +82,21 @@ namespace Content.Server.NodeContainer.Nodes
{
}
+ public virtual void OnPostRebuild()
+ {
+
+ }
+
public virtual void OnContainerShutdown()
{
- _deleting = true;
- NodeGroup.RemoveNode(this);
- }
-
- public bool TryAssignGroupIfNeeded()
- {
- if (!_needsGroup || !Connectable)
- {
- return false;
- }
- NodeGroup = GetReachableCompatibleGroups().FirstOrDefault() ?? MakeNewGroup();
- return true;
- }
-
- public void SpreadGroup()
- {
- Debug.Assert(!_needsGroup);
- foreach (var node in GetReachableCompatibleNodes())
- {
- if (node._needsGroup)
- {
- node.NodeGroup = NodeGroup;
- node.SpreadGroup();
- }
- }
- }
-
- public void ClearNodeGroup()
- {
- _nodeGroup = BaseNodeGroup.NullGroup;
- _needsGroup = true;
- }
-
- protected void RefreshNodeGroup()
- {
- RemoveSelfFromGroup();
- TryAssignGroupIfNeeded();
- CombineGroupWithReachable();
+ Deleting = true;
+ EntitySystem.Get().QueueNodeRemove(this);
}
///
/// How this node will attempt to find other reachable s to group with.
/// Returns a set of s to consider grouping with. Should not return this current .
///
- protected abstract IEnumerable GetReachableNodes();
-
- private IEnumerable GetReachableCompatibleNodes()
- {
- foreach (var node in GetReachableNodes())
- {
- if (node.NodeGroupID == NodeGroupID && node.Connectable)
- {
- yield return node;
- }
- }
- }
-
- private IEnumerable GetReachableCompatibleGroups()
- {
- foreach (var node in GetReachableCompatibleNodes())
- {
- if (!node._needsGroup)
- {
- var group = node.NodeGroup;
- if (group != NodeGroup)
- {
- yield return group;
- }
- }
- }
- }
-
- private void CombineGroupWithReachable()
- {
- if (_needsGroup || !Connectable)
- return;
-
- foreach (var group in GetReachableCompatibleGroups())
- {
- NodeGroup.CombineGroup(group);
- }
- }
-
- private void SetNodeGroup(INodeGroup newGroup)
- {
- _nodeGroup = newGroup;
- NodeGroup.AddNode(this);
- _needsGroup = false;
- }
-
- private INodeGroup MakeNewGroup()
- {
- return IoCManager.Resolve().MakeNodeGroup(this);
- }
-
- private void RemoveSelfFromGroup()
- {
- NodeGroup.RemoveNode(this);
- ClearNodeGroup();
- }
+ public abstract IEnumerable GetReachableNodes();
}
}
diff --git a/Content.Server/NodeContainer/Nodes/NodeHelpers.cs b/Content.Server/NodeContainer/Nodes/NodeHelpers.cs
new file mode 100644
index 0000000000..115e5a276f
--- /dev/null
+++ b/Content.Server/NodeContainer/Nodes/NodeHelpers.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+
+namespace Content.Server.NodeContainer.Nodes
+{
+ ///
+ /// Helper utilities for implementing .
+ ///
+ public static class NodeHelpers
+ {
+ public static IEnumerable GetNodesInTile(IComponentManager compMgr, IMapGrid grid, Vector2i coords)
+ {
+ foreach (var entityUid in grid.GetAnchoredEntities(coords))
+ {
+ if (!compMgr.TryGetComponent(entityUid, out NodeContainerComponent? container))
+ continue;
+
+ foreach (var node in container.Nodes.Values)
+ {
+ yield return node;
+ }
+ }
+ }
+
+ public static IEnumerable<(Direction dir, Node node)> GetCardinalNeighborNodes(
+ IComponentManager compMgr,
+ IMapGrid grid,
+ Vector2i coords,
+ bool includeSameTile = true)
+ {
+ foreach (var (dir, entityUid) in GetCardinalNeighborCells(grid, coords, includeSameTile))
+ {
+ if (!compMgr.TryGetComponent(entityUid, out NodeContainerComponent? container))
+ continue;
+
+ foreach (var node in container.Nodes.Values)
+ {
+ yield return (dir, node);
+ }
+ }
+ }
+
+ [SuppressMessage("ReSharper", "EnforceForeachStatementBraces")]
+ public static IEnumerable<(Direction dir, EntityUid entity)> GetCardinalNeighborCells(
+ IMapGrid grid,
+ Vector2i coords,
+ bool includeSameTile = true)
+ {
+ if (includeSameTile)
+ {
+ foreach (var uid in grid.GetAnchoredEntities(coords))
+ yield return (Direction.Invalid, uid);
+ }
+
+ foreach (var uid in grid.GetAnchoredEntities(coords + (0, 1)))
+ yield return (Direction.North, uid);
+
+ foreach (var uid in grid.GetAnchoredEntities(coords + (0, -1)))
+ yield return (Direction.South, uid);
+
+ foreach (var uid in grid.GetAnchoredEntities(coords + (1, 0)))
+ yield return (Direction.East, uid);
+
+ foreach (var uid in grid.GetAnchoredEntities(coords + (-1, 0)))
+ yield return (Direction.West, uid);
+ }
+
+ public static Vector2i TileOffsetForDir(Direction dir)
+ {
+ return dir switch
+ {
+ Direction.Invalid => (0, 0),
+ Direction.South => (0, -1),
+ Direction.SouthEast => (1, -1),
+ Direction.East => (1, 0),
+ Direction.NorthEast => (1, 1),
+ Direction.North => (0, 1),
+ Direction.NorthWest => (-1, 1),
+ Direction.West => (-1, 0),
+ Direction.SouthWest => (-1, -1),
+ _ => throw new ArgumentOutOfRangeException(nameof(dir), dir, null)
+ };
+ }
+ }
+}
diff --git a/Content.Server/NodeContainer/Nodes/PipeNode.cs b/Content.Server/NodeContainer/Nodes/PipeNode.cs
index 54f0c0fb54..68d093188c 100644
--- a/Content.Server/NodeContainer/Nodes/PipeNode.cs
+++ b/Content.Server/NodeContainer/Nodes/PipeNode.cs
@@ -3,9 +3,8 @@ using System.Collections.Generic;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Interfaces;
-using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
-using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
@@ -13,9 +12,10 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
-namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
+namespace Content.Server.NodeContainer.Nodes
{
///
/// Connects with other s whose
@@ -63,21 +63,23 @@ namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
set
{
_connectionsEnabled = value;
- RefreshNodeGroup();
+
+ if (NodeGroup != null)
+ EntitySystem.Get().QueueRemakeGroup((BaseNodeGroup) NodeGroup);
}
}
+ [DataField("connectionsEnabled")]
+ private bool _connectionsEnabled = true;
+
[DataField("rotationsEnabled")]
public bool RotationsEnabled { get; set; } = true;
///
- /// The this pipe is a part of. Set to when not in an .
+ /// The this pipe is a part of.
///
[ViewVariables]
- private IPipeNet _pipeNet = PipeNet.NullNet;
-
- [DataField("connectionsEnabled")]
- private bool _connectionsEnabled = true;
+ private IPipeNet? PipeNet => (IPipeNet?) NodeGroup;
///
/// Whether to ignore the pipenet and return the environment's air.
@@ -92,8 +94,12 @@ namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
[ViewVariables]
public GasMixture Air
{
- get => !EnvironmentalAir ? _pipeNet.Air : Owner.Transform.Coordinates.GetTileAir() ?? GasMixture.SpaceGas;
- set => _pipeNet.Air = value;
+ get => (!EnvironmentalAir ? PipeNet?.Air : Owner.Transform.Coordinates.GetTileAir()) ?? GasMixture.SpaceGas;
+ set
+ {
+ DebugTools.Assert(PipeNet != null);
+ PipeNet!.Air = value;
+ }
}
public void AssumeAir(GasMixture giver)
@@ -105,7 +111,7 @@ namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
return;
}
- EntitySystem.Get().Merge(_pipeNet.Air, giver);
+ EntitySystem.Get().Merge(PipeNet!.Air, giver);
}
[ViewVariables]
@@ -128,13 +134,6 @@ namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
public void JoinPipeNet(IPipeNet pipeNet)
{
- _pipeNet = pipeNet;
- OnConnectedDirectionsNeedsUpdating();
- }
-
- public void ClearPipeNet()
- {
- _pipeNet = PipeNet.NullNet;
OnConnectedDirectionsNeedsUpdating();
}
@@ -146,12 +145,11 @@ namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
if (!RotationsEnabled) return;
var diff = ev.NewRotation - ev.OldRotation;
PipeDirection = PipeDirection.RotatePipeDirection(diff);
- RefreshNodeGroup();
OnConnectedDirectionsNeedsUpdating();
UpdateAppearance();
}
- protected override IEnumerable GetReachableNodes()
+ public override IEnumerable GetReachableNodes()
{
for (var i = 0; i < PipeDirectionHelpers.AllPipeDirections; i++)
{
diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs
index b3ef6644ee..8562cccfbe 100644
--- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs
+++ b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorControlBoxComponent.cs
@@ -6,9 +6,10 @@ using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Content.Server.Notification;
using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
using Content.Server.UserInterface;
using Content.Server.VendingMachines;
-using Content.Server.Wires.Components;
+using Content.Server.WireHacking;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
@@ -47,7 +48,7 @@ namespace Content.Server.ParticleAccelerator.Components
///
/// Power receiver for the control console itself.
///
- [ViewVariables] private PowerReceiverComponent _powerReceiverComponent = default!;
+ [ViewVariables] private ApcPowerReceiverComponent _apcPowerReceiverComponent = default!;
[ViewVariables] private ParticleAcceleratorFuelChamberComponent? _partFuelChamber;
[ViewVariables] private ParticleAcceleratorEndCapComponent? _partEndCap;
@@ -88,7 +89,7 @@ namespace Content.Server.ParticleAccelerator.Components
[ViewVariables(VVAccess.ReadWrite)] [DataField("powerDrawBase")] private int _powerDrawBase = 500;
[ViewVariables(VVAccess.ReadWrite)] [DataField("powerDrawMult")] private int _powerDrawMult = 1500;
- [ViewVariables] private bool ConsolePowered => _powerReceiverComponent?.Powered ?? true;
+ [ViewVariables] private bool ConsolePowered => _apcPowerReceiverComponent?.Powered ?? true;
public ParticleAcceleratorControlBoxComponent()
{
@@ -107,9 +108,9 @@ namespace Content.Server.ParticleAccelerator.Components
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
- Owner.EnsureComponent(out _powerReceiverComponent);
+ Owner.EnsureComponent(out _apcPowerReceiverComponent);
- _powerReceiverComponent!.Load = 250;
+ _apcPowerReceiverComponent!.Load = 250;
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
@@ -189,8 +190,8 @@ namespace Content.Server.ParticleAccelerator.Components
public void UpdateUI()
{
- var draw = 0;
- var receive = 0;
+ var draw = 0f;
+ var receive = 0f;
if (_isEnabled)
{
@@ -202,8 +203,8 @@ namespace Content.Server.ParticleAccelerator.Components
_isAssembled,
_isEnabled,
_selectedStrength,
- draw,
- receive,
+ (int) draw,
+ (int) receive,
_partEmitterLeft != null,
_partEmitterCenter != null,
_partEmitterRight != null,
@@ -577,7 +578,7 @@ namespace Content.Server.ParticleAccelerator.Components
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(ParticleAcceleratorVisuals.VisualState,
- _powerReceiverComponent!.Powered
+ _apcPowerReceiverComponent!.Powered
? (ParticleAcceleratorVisualState) _selectedStrength
: ParticleAcceleratorVisualState.Unpowered);
}
@@ -645,7 +646,7 @@ namespace Content.Server.ParticleAccelerator.Components
} * _powerDrawMult + _powerDrawBase;
}
- public void PowerBoxReceivedChanged(object? sender, ReceivedPowerChangedEventArgs eventArgs)
+ public void PowerBoxReceivedChanged(PowerConsumerReceivedChanged eventArgs)
{
DebugTools.Assert(_isAssembled);
diff --git a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs
index c3f79a8be7..c37486bc81 100644
--- a/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs
+++ b/Content.Server/ParticleAccelerator/Components/ParticleAcceleratorPowerBoxComponent.cs
@@ -17,12 +17,6 @@ namespace Content.Server.ParticleAccelerator.Components
base.Initialize();
PowerConsumerComponent = Owner.EnsureComponentWarn();
- PowerConsumerComponent.OnReceivedPowerChanged += PowerReceivedChanged;
- }
-
- private void PowerReceivedChanged(object? sender, ReceivedPowerChangedEventArgs e)
- {
- Master?.PowerBoxReceivedChanged(sender, e);
}
}
}
diff --git a/Content.Server/ParticleAccelerator/ParticleAcceleratorPartSystem.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorPartSystem.cs
similarity index 94%
rename from Content.Server/ParticleAccelerator/ParticleAcceleratorPartSystem.cs
rename to Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorPartSystem.cs
index 10f386d959..79be10ebae 100644
--- a/Content.Server/ParticleAccelerator/ParticleAcceleratorPartSystem.cs
+++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorPartSystem.cs
@@ -3,7 +3,7 @@ using Content.Server.ParticleAccelerator.Components;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
-namespace Content.Server.ParticleAccelerator
+namespace Content.Server.ParticleAccelerator.EntitySystems
{
[UsedImplicitly]
public class ParticleAcceleratorPartSystem : EntitySystem
diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorPowerBoxSystem.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorPowerBoxSystem.cs
new file mode 100644
index 0000000000..92bef8bfeb
--- /dev/null
+++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorPowerBoxSystem.cs
@@ -0,0 +1,27 @@
+using Content.Server.ParticleAccelerator.Components;
+using Content.Server.Power.EntitySystems;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.ParticleAccelerator.EntitySystems
+{
+ [UsedImplicitly]
+ public class ParticleAcceleratorPowerBoxSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(
+ PowerBoxReceivedChanged);
+ }
+
+ private static void PowerBoxReceivedChanged(
+ EntityUid uid,
+ ParticleAcceleratorPowerBoxComponent component,
+ PowerConsumerReceivedChanged args)
+ {
+ component.Master!.PowerBoxReceivedChanged(args);
+ }
+ }
+}
diff --git a/Content.Server/Power/Commands/PowerStatCommand.cs b/Content.Server/Power/Commands/PowerStatCommand.cs
new file mode 100644
index 0000000000..16be3b82a0
--- /dev/null
+++ b/Content.Server/Power/Commands/PowerStatCommand.cs
@@ -0,0 +1,26 @@
+using Content.Server.Administration;
+using Content.Server.Power.EntitySystems;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.Power.Commands
+{
+ [AdminCommand(AdminFlags.Debug)]
+ public sealed class PowerStatCommand : IConsoleCommand
+ {
+ public string Command => "powerstat";
+ public string Description => "Shows statistics for pow3r";
+ public string Help => "Usage: powerstat";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var stats = EntitySystem.Get().GetStatistics();
+
+ shell.WriteLine($"networks: {stats.CountNetworks}");
+ shell.WriteLine($"loads: {stats.CountLoads}");
+ shell.WriteLine($"supplies: {stats.CountSupplies}");
+ shell.WriteLine($"batteries: {stats.CountBatteries}");
+ }
+ }
+}
diff --git a/Content.Server/APC/Components/ApcComponent.cs b/Content.Server/Power/Components/ApcComponent.cs
similarity index 84%
rename from Content.Server/APC/Components/ApcComponent.cs
rename to Content.Server/Power/Components/ApcComponent.cs
index 07df66a3fc..5182551775 100644
--- a/Content.Server/APC/Components/ApcComponent.cs
+++ b/Content.Server/Power/Components/ApcComponent.cs
@@ -1,23 +1,22 @@
#nullable enable
using System;
using Content.Server.Access.Components;
-using Content.Server.Battery.Components;
-using Content.Server.Power.Components;
+using Content.Server.Power.NodeGroups;
using Content.Server.UserInterface;
using Content.Shared.APC;
using Content.Shared.Interaction;
-using Content.Shared.Notification;
using Content.Shared.Notification.Managers;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
+using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
-namespace Content.Server.APC.Components
+namespace Content.Server.Power.Components
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
@@ -57,7 +56,6 @@ namespace Content.Server.APC.Components
{
base.Initialize();
- Owner.EnsureComponent();
Owner.EnsureComponentWarn();
Owner.EnsureComponentWarn();
@@ -89,6 +87,8 @@ namespace Content.Server.APC.Components
if (_accessReader == null || _accessReader.IsAllowed(user))
{
MainBreakerEnabled = !MainBreakerEnabled;
+ Owner.GetComponent().CanDischarge = MainBreakerEnabled;
+
_uiDirty = true;
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
}
@@ -153,44 +153,31 @@ namespace Content.Server.APC.Components
return ApcChargeState.Full;
}
- if (!Owner.TryGetComponent(out PowerConsumerComponent? consumer))
- {
- return ApcChargeState.Full;
- }
+ var netBattery = Owner.GetComponent();
+ var delta = netBattery.CurrentSupply - netBattery.CurrentReceiving;
- if (consumer.DrawRate == consumer.ReceivedPower)
- {
- return ApcChargeState.Charging;
- }
- else
- {
- return ApcChargeState.Lack;
- }
+ return delta < 0 ? ApcChargeState.Charging : ApcChargeState.Lack;
}
private ApcExternalPowerState CalcExtPowerState()
{
- if (!Owner.TryGetComponent(out BatteryStorageComponent? batteryStorage))
+ var bat = Battery;
+ if (bat == null)
+ return ApcExternalPowerState.None;
+
+ var netBat = Owner.GetComponent();
+ if (netBat.CurrentReceiving == 0 && netBat.LoadingNetworkDemand != 0)
{
return ApcExternalPowerState.None;
}
- var consumer = batteryStorage.Consumer;
- if (consumer == null)
- return ApcExternalPowerState.None;
-
- if (consumer.ReceivedPower == 0 && consumer.DrawRate != 0)
- {
- return ApcExternalPowerState.None;
- }
- else if (consumer.ReceivedPower < consumer.DrawRate)
+ var delta = netBat.CurrentReceiving - netBat.LoadingNetworkDemand;
+ if (!MathHelper.CloseTo(delta, 0, 0.1f) && delta < 0)
{
return ApcExternalPowerState.Low;
}
- else
- {
- return ApcExternalPowerState.Good;
- }
+
+ return ApcExternalPowerState.Good;
}
void IActivate.Activate(ActivateEventArgs eventArgs)
diff --git a/Content.Server/Power/Components/ApcPowerProviderComponent.cs b/Content.Server/Power/Components/ApcPowerProviderComponent.cs
new file mode 100644
index 0000000000..c87e4b6c54
--- /dev/null
+++ b/Content.Server/Power/Components/ApcPowerProviderComponent.cs
@@ -0,0 +1,121 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using Content.Server.Power.NodeGroups;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Power.Components
+{
+ [RegisterComponent]
+ public class ApcPowerProviderComponent : BaseApcNetComponent
+ {
+ public override string Name => "PowerProvider";
+
+ public IEntity ProviderOwner => Owner;
+
+ ///
+ /// The max distance this can transmit power to s from.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int PowerTransferRange { get => _powerTransferRange; set => SetPowerTransferRange(value); }
+ [DataField("powerTransferRange")]
+ private int _powerTransferRange = 3;
+
+ [ViewVariables] public List LinkedReceivers { get; } = new();
+
+ ///
+ /// If s should consider connecting to this.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool Connectable { get; private set; } = true;
+
+ public void AddReceiver(ApcPowerReceiverComponent receiver)
+ {
+ LinkedReceivers.Add(receiver);
+ receiver.NetworkLoad.LinkedNetwork = default;
+
+ Net?.QueueNetworkReconnect();
+ }
+
+ public void RemoveReceiver(ApcPowerReceiverComponent receiver)
+ {
+ LinkedReceivers.Remove(receiver);
+ receiver.NetworkLoad.LinkedNetwork = default;
+
+ Net?.QueueNetworkReconnect();
+ }
+
+ protected override void Startup()
+ {
+ base.Startup();
+
+ foreach (var receiver in FindAvailableReceivers())
+ {
+ receiver.Provider = this;
+ }
+ }
+
+ protected override void OnRemove()
+ {
+ Connectable = false;
+ var receivers = LinkedReceivers.ToArray();
+ foreach (var receiver in receivers)
+ {
+ receiver.Provider = null;
+ }
+ foreach (var receiver in receivers)
+ {
+ receiver.TryFindAndSetProvider();
+ }
+ base.OnRemove();
+ }
+
+ private IEnumerable FindAvailableReceivers()
+ {
+ var nearbyEntities = IoCManager.Resolve()
+ .GetEntitiesInRange(Owner, PowerTransferRange);
+
+ foreach (var entity in nearbyEntities)
+ {
+ if (entity.TryGetComponent(out var receiver) &&
+ receiver.Connectable &&
+ receiver.NeedsProvider &&
+ receiver.Owner.Transform.Coordinates.TryDistance(Owner.EntityManager, Owner.Transform.Coordinates, out var distance) &&
+ distance < Math.Min(PowerTransferRange, receiver.PowerReceptionRange))
+ {
+ yield return receiver;
+ }
+ }
+ }
+
+ protected override void AddSelfToNet(IApcNet apcNet)
+ {
+ apcNet.AddPowerProvider(this);
+ }
+
+ protected override void RemoveSelfFromNet(IApcNet apcNet)
+ {
+ apcNet.RemovePowerProvider(this);
+ }
+
+ private void SetPowerTransferRange(int newPowerTransferRange)
+ {
+ var receivers = LinkedReceivers.ToArray();
+
+ foreach (var receiver in receivers)
+ {
+ receiver.Provider = null;
+ }
+
+ _powerTransferRange = newPowerTransferRange;
+
+ foreach (var receiver in receivers)
+ {
+ receiver.TryFindAndSetProvider();
+ }
+ }
+ }
+}
diff --git a/Content.Server/Power/Components/PowerReceiverComponent.cs b/Content.Server/Power/Components/ApcPowerReceiverComponent.cs
similarity index 65%
rename from Content.Server/Power/Components/PowerReceiverComponent.cs
rename to Content.Server/Power/Components/ApcPowerReceiverComponent.cs
index 89d44c13a7..b95db5cc86 100644
--- a/Content.Server/Power/Components/PowerReceiverComponent.cs
+++ b/Content.Server/Power/Components/ApcPowerReceiverComponent.cs
@@ -1,12 +1,15 @@
#nullable enable
using System;
-using Content.Server.APC;
+using System.Diagnostics.CodeAnalysis;
+using Content.Server.Power.NodeGroups;
+using Content.Server.Power.Pow3r;
using Content.Shared.Examine;
using Content.Shared.Power;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
+using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
@@ -15,26 +18,21 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Power.Components
{
///
- /// Attempts to link with a nearby s so that it can receive power from a .
+ /// Attempts to link with a nearby s
+ /// so that it can receive power from a .
///
[RegisterComponent]
- public class PowerReceiverComponent : Component, IExamine
+ public class ApcPowerReceiverComponent : Component, IExamine
{
[ViewVariables] [ComponentDependency] private readonly IPhysBody? _physicsComponent = null;
- public override string Name => "PowerReceiver";
+ public override string Name => "ApcPowerReceiver";
[ViewVariables]
- public bool Powered => (HasApcPower || !NeedsPower) && !PowerDisabled;
+ public bool Powered => (MathHelper.CloseTo(NetworkLoad.ReceivingPower, Load) || !NeedsPower) && !PowerDisabled;
///
- /// If this is being powered by an Apc.
- ///
- [ViewVariables]
- public bool HasApcPower { get; private set; }
-
- ///
- /// The max distance from a that this can receive power from.
+ /// The max distance from a that this can receive power from.
///
[ViewVariables(VVAccess.ReadWrite)]
public int PowerReceptionRange { get => _powerReceptionRange; set => SetPowerReceptionRange(value); }
@@ -42,32 +40,53 @@ namespace Content.Server.Power.Components
private int _powerReceptionRange = 3;
[ViewVariables]
- public IPowerProvider Provider { get => _provider; set => SetProvider(value); }
- private IPowerProvider _provider = PowerProviderComponent.NullProvider;
+ public ApcPowerProviderComponent? Provider
+ {
+ get => _provider;
+ set
+ {
+ // Will get updated before power networks process.
+ NetworkLoad.LinkedNetwork = default;
+ _provider?.RemoveReceiver(this);
+ _provider = value;
+ value?.AddReceiver(this);
+ ApcPowerChanged();
+ }
+ }
+
+ private ApcPowerProviderComponent? _provider;
///
- /// If this should be considered for connection by s.
+ /// If this should be considered for connection by s.
///
public bool Connectable => Anchored;
private bool Anchored => _physicsComponent == null || _physicsComponent.BodyType == BodyType.Static;
- [ViewVariables]
- public bool NeedsProvider { get; private set; } = true;
+ [ViewVariables] public bool NeedsProvider => Provider == null;
///
/// Amount of charge this needs from an APC per second to function.
///
[ViewVariables(VVAccess.ReadWrite)]
- public int Load { get => _load; set => SetLoad(value); }
[DataField("powerLoad")]
- private int _load = 5;
+ public float Load { get => NetworkLoad.DesiredPower; set => NetworkLoad.DesiredPower = value; }
///
/// When false, causes this to appear powered even if not receiving power from an Apc.
///
[ViewVariables(VVAccess.ReadWrite)]
- public bool NeedsPower { get => _needsPower; set => SetNeedsPower(value); }
+ public bool NeedsPower
+ {
+ get => _needsPower;
+ set
+ {
+ _needsPower = value;
+ // Reset this so next tick will do a power update.
+ LastPowerReceived = float.NaN;
+ }
+ }
+
[DataField("needsPower")]
private bool _needsPower = true;
@@ -75,9 +94,16 @@ namespace Content.Server.Power.Components
/// When true, causes this to never appear powered.
///
[ViewVariables(VVAccess.ReadWrite)]
- public bool PowerDisabled { get => _powerDisabled; set => SetPowerDisabled(value); }
[DataField("powerDisabled")]
- private bool _powerDisabled;
+ public bool PowerDisabled { get => !NetworkLoad.Enabled; set => NetworkLoad.Enabled = !value; }
+
+ public float LastPowerReceived = float.NaN;
+
+ [ViewVariables]
+ public PowerState.Load NetworkLoad { get; } = new PowerState.Load
+ {
+ DesiredPower = 5
+ };
protected override void Startup()
{
@@ -94,7 +120,8 @@ namespace Content.Server.Power.Components
protected override void OnRemove()
{
- _provider.RemoveReceiver(this);
+ _provider?.RemoveReceiver(this);
+
base.OnRemove();
}
@@ -108,20 +135,17 @@ namespace Content.Server.Power.Components
public void ApcPowerChanged()
{
- var oldPowered = Powered;
- HasApcPower = Provider.HasApcPower;
- if (Powered != oldPowered)
- OnNewPowerState();
+ OnNewPowerState();
}
- private bool TryFindAvailableProvider(out IPowerProvider foundProvider)
+ private bool TryFindAvailableProvider([NotNullWhen(true)] out ApcPowerProviderComponent? foundProvider)
{
var nearbyEntities = IoCManager.Resolve()
.GetEntitiesInRange(Owner, PowerReceptionRange);
foreach (var entity in nearbyEntities)
{
- if (entity.TryGetComponent(out var provider))
+ if (entity.TryGetComponent(out var provider))
{
if (provider.Connectable)
{
@@ -136,60 +160,18 @@ namespace Content.Server.Power.Components
}
}
}
- foundProvider = default!;
+
+ foundProvider = default;
return false;
}
- public void ClearProvider()
- {
- _provider.RemoveReceiver(this);
- _provider = PowerProviderComponent.NullProvider;
- NeedsProvider = true;
- ApcPowerChanged();
- }
-
- private void SetProvider(IPowerProvider newProvider)
- {
- _provider.RemoveReceiver(this);
- _provider = newProvider;
- newProvider.AddReceiver(this);
- NeedsProvider = false;
- ApcPowerChanged();
- }
-
private void SetPowerReceptionRange(int newPowerReceptionRange)
{
- ClearProvider();
+ Provider = null;
_powerReceptionRange = newPowerReceptionRange;
TryFindAndSetProvider();
}
- private void SetLoad(int newLoad)
- {
- Provider.UpdateReceiverLoad(Load, newLoad);
- _load = newLoad;
- }
-
- private void SetNeedsPower(bool newNeedsPower)
- {
- var oldPowered = Powered;
- _needsPower = newNeedsPower;
- if (oldPowered != Powered)
- {
- OnNewPowerState();
- }
- }
-
- private void SetPowerDisabled(bool newPowerDisabled)
- {
- var oldPowered = Powered;
- _powerDisabled = newPowerDisabled;
- if (oldPowered != Powered)
- {
- OnNewPowerState();
- }
- }
-
private void OnNewPowerState()
{
SendMessage(new PowerChangedMessage(Powered));
@@ -211,7 +193,7 @@ namespace Content.Server.Power.Components
}
else
{
- ClearProvider();
+ Provider = null;
}
}
diff --git a/Content.Server/Power/Components/BaseApcNetComponent.cs b/Content.Server/Power/Components/BaseApcNetComponent.cs
new file mode 100644
index 0000000000..e004606107
--- /dev/null
+++ b/Content.Server/Power/Components/BaseApcNetComponent.cs
@@ -0,0 +1,9 @@
+#nullable enable
+using Content.Server.Power.NodeGroups;
+
+namespace Content.Server.Power.Components
+{
+ public abstract class BaseApcNetComponent : BaseNetConnectorComponent
+ {
+ }
+}
diff --git a/Content.Server/Power/Components/BaseCharger.cs b/Content.Server/Power/Components/BaseCharger.cs
index a438ad97cb..753e5d59e8 100644
--- a/Content.Server/Power/Components/BaseCharger.cs
+++ b/Content.Server/Power/Components/BaseCharger.cs
@@ -1,7 +1,6 @@
#nullable enable
using System;
using System.Threading.Tasks;
-using Content.Server.Battery.Components;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Weapon.Ranged.Barrels.Components;
@@ -45,7 +44,7 @@ namespace Content.Server.Power.Components
{
base.Initialize();
- Owner.EnsureComponent();
+ Owner.EnsureComponent();
_container = ContainerHelpers.EnsureContainer(Owner, $"{Name}-powerCellContainer");
// Default state in the visualizer is OFF, so when this gets powered on during initialization it will generally show empty
}
@@ -191,7 +190,7 @@ namespace Content.Server.Power.Components
private CellChargerStatus GetStatus()
{
- if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) &&
+ if (Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) &&
!receiver.Powered)
{
return CellChargerStatus.Off;
@@ -234,7 +233,7 @@ namespace Content.Server.Power.Components
// Not called UpdateAppearance just because it messes with the load
var status = GetStatus();
if (_status == status ||
- !Owner.TryGetComponent(out PowerReceiverComponent? receiver))
+ !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{
return;
}
@@ -279,7 +278,7 @@ namespace Content.Server.Power.Components
private void TransferPower(float frameTime)
{
- if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) &&
+ if (Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) &&
!receiver.Powered)
{
return;
diff --git a/Content.Server/Power/Components/BaseNetConnectorComponent.cs b/Content.Server/Power/Components/BaseNetConnectorComponent.cs
index 017b80ca65..d24fb16191 100644
--- a/Content.Server/Power/Components/BaseNetConnectorComponent.cs
+++ b/Content.Server/Power/Components/BaseNetConnectorComponent.cs
@@ -17,23 +17,18 @@ namespace Content.Server.Power.Components
private Voltage _voltage = Voltage.High;
[ViewVariables]
- public TNetType Net { get => _net; set => SetNet(value); }
- private TNetType _net = default!; //set in OnAdd()
-
- protected abstract TNetType NullNet { get; }
+ public TNetType? Net { get => _net; set => SetNet(value); }
+ private TNetType? _net;
[ViewVariables]
- private bool _needsNet = true;
+ private bool _needsNet => _net != null;
- protected override void OnAdd()
- {
- base.OnAdd();
- _net = NullNet;
- }
+ [DataField("node")] [ViewVariables] public string? NodeId;
protected override void Initialize()
{
base.Initialize();
+
if (_needsNet)
{
TryFindAndSetNet();
@@ -56,9 +51,8 @@ namespace Content.Server.Power.Components
public void ClearNet()
{
- RemoveSelfFromNet(_net);
- _net = NullNet;
- _needsNet = true;
+ if (_net != null)
+ RemoveSelfFromNet(_net);
}
protected abstract void AddSelfToNet(TNetType net);
@@ -70,7 +64,7 @@ namespace Content.Server.Power.Components
if (Owner.TryGetComponent(out var container))
{
var compatibleNet = container.Nodes.Values
- .Where(node => node.NodeGroupID == (NodeGroupID) Voltage)
+ .Where(node => (NodeId == null || NodeId == node.Name) && node.NodeGroupID == (NodeGroupID) Voltage)
.Select(node => node.NodeGroup)
.OfType()
.FirstOrDefault();
@@ -85,12 +79,15 @@ namespace Content.Server.Power.Components
return false;
}
- private void SetNet(TNetType newNet)
+ private void SetNet(TNetType? newNet)
{
- RemoveSelfFromNet(_net);
- AddSelfToNet(newNet);
+ if (_net != null)
+ RemoveSelfFromNet(_net);
+
+ if (newNet != null)
+ AddSelfToNet(newNet);
+
_net = newNet;
- _needsNet = false;
}
private void SetVoltage(Voltage newVoltage)
diff --git a/Content.Server/Power/Components/BasePowerNetComponent.cs b/Content.Server/Power/Components/BasePowerNetComponent.cs
index 78e00cf9dd..c25715abb4 100644
--- a/Content.Server/Power/Components/BasePowerNetComponent.cs
+++ b/Content.Server/Power/Components/BasePowerNetComponent.cs
@@ -1,10 +1,10 @@
#nullable enable
using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.Power.NodeGroups;
namespace Content.Server.Power.Components
{
public abstract class BasePowerNetComponent : BaseNetConnectorComponent
{
- protected override IPowerNet NullNet => PowerNetNodeGroup.NullNet;
}
}
diff --git a/Content.Server/Power/Components/BatteryChargerComponent.cs b/Content.Server/Power/Components/BatteryChargerComponent.cs
new file mode 100644
index 0000000000..f6b472cdb4
--- /dev/null
+++ b/Content.Server/Power/Components/BatteryChargerComponent.cs
@@ -0,0 +1,24 @@
+using Content.Server.Power.NodeGroups;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.Power.Components
+{
+ ///
+ /// Connects the loading side of a to a non-APC power network.
+ ///
+ [RegisterComponent]
+ public class BatteryChargerComponent : BasePowerNetComponent
+ {
+ public override string Name => "BatteryCharger";
+
+ protected override void AddSelfToNet(IPowerNet net)
+ {
+ net.AddCharger(this);
+ }
+
+ protected override void RemoveSelfFromNet(IPowerNet net)
+ {
+ net.RemoveCharger(this);
+ }
+ }
+}
diff --git a/Content.Server/Battery/Components/BatteryComponent.cs b/Content.Server/Power/Components/BatteryComponent.cs
similarity index 73%
rename from Content.Server/Battery/Components/BatteryComponent.cs
rename to Content.Server/Power/Components/BatteryComponent.cs
index bc0ea33f65..480afbde0e 100644
--- a/Content.Server/Battery/Components/BatteryComponent.cs
+++ b/Content.Server/Power/Components/BatteryComponent.cs
@@ -5,8 +5,11 @@ using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
-namespace Content.Server.Battery.Components
+namespace Content.Server.Power.Components
{
+ ///
+ /// Battery node on the pow3r network. Needs other components to connect to actual networks.
+ ///
[RegisterComponent]
public class BatteryComponent : Component
{
@@ -15,9 +18,9 @@ namespace Content.Server.Battery.Components
///
/// Maximum charge of the battery in joules (ie. watt seconds)
///
- [ViewVariables(VVAccess.ReadWrite)] public int MaxCharge { get => _maxCharge; set => SetMaxCharge(value); }
+ [ViewVariables(VVAccess.ReadWrite)] public float MaxCharge { get => _maxCharge; set => SetMaxCharge(value); }
[DataField("maxCharge")]
- private int _maxCharge = 1000;
+ private float _maxCharge;
///
/// Current charge of the battery in joules (ie. watt seconds)
@@ -25,7 +28,7 @@ namespace Content.Server.Battery.Components
[ViewVariables(VVAccess.ReadWrite)]
public float CurrentCharge { get => _currentCharge; set => SetCurrentCharge(value); }
[DataField("startingCharge")]
- private float _currentCharge = 500;
+ private float _currentCharge;
///
/// True if the battery is fully charged.
@@ -36,14 +39,6 @@ namespace Content.Server.Battery.Components
[ViewVariables(VVAccess.ReadWrite)] [DataField("autoRechargeRate")] public float AutoRechargeRate { get; set; }
- [ViewVariables] public BatteryState BatteryState { get; private set; }
-
- protected override void Initialize()
- {
- base.Initialize();
- UpdateStorageState();
- }
-
///
/// If sufficient charge is avaiable on the battery, use it. Otherwise, don't.
///
@@ -83,34 +78,16 @@ namespace Content.Server.Battery.Components
protected virtual void OnChargeChanged() { }
- private void UpdateStorageState()
- {
- if (IsFullyCharged)
- {
- BatteryState = BatteryState.Full;
- }
- else if (CurrentCharge == 0)
- {
- BatteryState = BatteryState.Empty;
- }
- else
- {
- BatteryState = BatteryState.PartlyFull;
- }
- }
-
- private void SetMaxCharge(int newMax)
+ private void SetMaxCharge(float newMax)
{
_maxCharge = Math.Max(newMax, 0);
_currentCharge = Math.Min(_currentCharge, MaxCharge);
- UpdateStorageState();
OnChargeChanged();
}
private void SetCurrentCharge(float newChargeAmount)
{
_currentCharge = MathHelper.Clamp(newChargeAmount, 0, MaxCharge);
- UpdateStorageState();
OnChargeChanged();
}
@@ -121,11 +98,4 @@ namespace Content.Server.Battery.Components
CurrentCharge += AutoRechargeRate * frameTime;
}
}
-
- public enum BatteryState
- {
- Full,
- PartlyFull,
- Empty
- }
}
diff --git a/Content.Server/Power/Components/BatteryDischargerComponent.cs b/Content.Server/Power/Components/BatteryDischargerComponent.cs
index 62335e2727..37cb8dbe58 100644
--- a/Content.Server/Power/Components/BatteryDischargerComponent.cs
+++ b/Content.Server/Power/Components/BatteryDischargerComponent.cs
@@ -1,79 +1,21 @@
-#nullable enable
-using Content.Server.Battery.Components;
+using Content.Server.Power.NodeGroups;
using Robust.Shared.GameObjects;
-using Robust.Shared.Serialization.Manager.Attributes;
-using Robust.Shared.ViewVariables;
namespace Content.Server.Power.Components
{
- ///
- /// Uses charge from a to supply power via a .
- ///
[RegisterComponent]
- public class BatteryDischargerComponent : Component
+ public class BatteryDischargerComponent : BasePowerNetComponent
{
public override string Name => "BatteryDischarger";
- [ViewVariables]
- [ComponentDependency] private BatteryComponent? _battery = default!;
-
- [ViewVariables]
- [ComponentDependency] private PowerSupplierComponent? _supplier = default!;
-
- [ViewVariables(VVAccess.ReadWrite)]
- public int ActiveSupplyRate { get => _activeSupplyRate; set => SetActiveSupplyRate(value); }
-
- [DataField("activeSupplyRate")]
- private int _activeSupplyRate = 50;
-
- protected override void Initialize()
+ protected override void AddSelfToNet(IPowerNet net)
{
- base.Initialize();
- Owner.EnsureComponentWarn();
- UpdateSupplyRate();
+ net.AddDischarger(this);
}
- public void Update(float frameTime)
+ protected override void RemoveSelfFromNet(IPowerNet net)
{
- if (_battery == null)
- return;
-
- //Simplified implementation - if the battery is empty, and charge is being added to the battery
- //at a lower rate that this is using it, the charge is used without creating power supply.
- _battery.CurrentCharge -= ActiveSupplyRate * frameTime;
- UpdateSupplyRate();
- }
-
- private void UpdateSupplyRate()
- {
- if (_battery == null)
- return;
-
- if (_battery.BatteryState == BatteryState.Empty)
- {
- SetSupplierSupplyRate(0);
- }
- else
- {
- SetSupplierSupplyRate(ActiveSupplyRate);
- }
- }
-
- private void SetSupplierSupplyRate(int newSupplierSupplyRate)
- {
- if (_supplier == null)
- return;
-
- if (_supplier.SupplyRate != newSupplierSupplyRate)
- {
- _supplier.SupplyRate = newSupplierSupplyRate;
- }
- }
-
- private void SetActiveSupplyRate(int newEnabledSupplyRate)
- {
- _activeSupplyRate = newEnabledSupplyRate;
- UpdateSupplyRate();
+ net.RemoveDischarger(this);
}
}
}
diff --git a/Content.Server/Power/Components/BatteryStorageComponent.cs b/Content.Server/Power/Components/BatteryStorageComponent.cs
deleted file mode 100644
index daa1dba4d6..0000000000
--- a/Content.Server/Power/Components/BatteryStorageComponent.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-#nullable enable
-using Content.Server.Battery.Components;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Serialization.Manager.Attributes;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Server.Power.Components
-{
- ///
- /// Takes power via a to charge a .
- ///
- [RegisterComponent]
- public class BatteryStorageComponent : Component
- {
- public override string Name => "BatteryStorage";
-
- [ViewVariables(VVAccess.ReadWrite)]
- public int ActiveDrawRate { get => _activeDrawRate; set => SetActiveDrawRate(value); }
- [DataField("activeDrawRate")]
- private int _activeDrawRate = 100;
-
- [ViewVariables]
- [ComponentDependency] private BatteryComponent? _battery = default!;
-
- [ViewVariables]
- public PowerConsumerComponent? Consumer => _consumer;
-
- [ComponentDependency] private PowerConsumerComponent? _consumer = default!;
-
- protected override void Initialize()
- {
- base.Initialize();
- Owner.EnsureComponentWarn();
- UpdateDrawRate();
- }
-
- public void Update(float frameTime)
- {
- if (_consumer == null || _battery == null)
- return;
-
- //Simplified implementation - If a frame adds more power to a partially full battery than it can hold, the power is lost.
- _battery.CurrentCharge += _consumer.ReceivedPower * frameTime;
- UpdateDrawRate();
- }
-
- private void UpdateDrawRate()
- {
- if (_battery == null)
- return;
-
- if (_battery.BatteryState == BatteryState.Full)
- {
- SetConsumerDraw(0);
- }
- else
- {
- SetConsumerDraw(ActiveDrawRate);
- }
- }
-
- private void SetConsumerDraw(int newConsumerDrawRate)
- {
- if (_consumer == null)
- return;
-
- if (_consumer.DrawRate != newConsumerDrawRate)
- {
- _consumer.DrawRate = newConsumerDrawRate;
- }
- }
-
- private void SetActiveDrawRate(int newEnabledDrawRate)
- {
- _activeDrawRate = newEnabledDrawRate;
- UpdateDrawRate();
- }
- }
-}
diff --git a/Content.Server/Wires/Components/WireComponent.cs b/Content.Server/Power/Components/CableComponent.cs
similarity index 61%
rename from Content.Server/Wires/Components/WireComponent.cs
rename to Content.Server/Power/Components/CableComponent.cs
index 473a6d8375..b95f790953 100644
--- a/Content.Server/Wires/Components/WireComponent.cs
+++ b/Content.Server/Power/Components/CableComponent.cs
@@ -3,45 +3,44 @@ using System.Threading.Tasks;
using Content.Server.Stack;
using Content.Server.Tools.Components;
using Content.Shared.Interaction;
-using Content.Shared.Stacks;
using Content.Shared.Tool;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
-namespace Content.Server.Wires.Components
+namespace Content.Server.Power.Components
{
///
- /// Allows the attached entity to be destroyed by a cutting tool, dropping a piece of wire.
+ /// Allows the attached entity to be destroyed by a cutting tool, dropping a piece of cable.
///
[RegisterComponent]
- public class WireComponent : Component, IInteractUsing
+ public class CableComponent : Component, IInteractUsing
{
- public override string Name => "Wire";
+ public override string Name => "Cable";
[ViewVariables]
- [DataField("wireDroppedOnCutPrototype")]
- private string? _wireDroppedOnCutPrototype = "HVWireStack1";
+ [DataField("cableDroppedOnCutPrototype")]
+ private string? _cableDroppedOnCutPrototype = "CableHVStack1";
///
- /// Checked by to determine if there is
- /// already a wire of a type on a tile.
+ /// Checked by to determine if there is
+ /// already a cable of a type on a tile.
///
[ViewVariables]
- public WireType WireType => _wireType;
- [DataField("wireType")]
- private WireType _wireType = WireType.HighVoltage;
+ public CableType CableType => _cableType;
+ [DataField("cableType")]
+ private CableType _cableType = CableType.HighVoltage;
async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
- if (_wireDroppedOnCutPrototype == null)
+ if (_cableDroppedOnCutPrototype == null)
return false;
if (!eventArgs.Using.TryGetComponent(out var tool)) return false;
if (!await tool.UseTool(eventArgs.User, Owner, 0.25f, ToolQuality.Cutting)) return false;
Owner.Delete();
- var droppedEnt = Owner.EntityManager.SpawnEntity(_wireDroppedOnCutPrototype, eventArgs.ClickLocation);
+ var droppedEnt = Owner.EntityManager.SpawnEntity(_cableDroppedOnCutPrototype, eventArgs.ClickLocation);
// TODO: Literally just use a prototype that has a single thing in the stack, it's not that complicated...
if (droppedEnt.TryGetComponent(out var stack))
@@ -51,7 +50,7 @@ namespace Content.Server.Wires.Components
}
}
- public enum WireType
+ public enum CableType
{
HighVoltage,
MediumVoltage,
diff --git a/Content.Server/Wires/Components/WirePlacerComponent.cs b/Content.Server/Power/Components/CablePlacerComponent.cs
similarity index 73%
rename from Content.Server/Wires/Components/WirePlacerComponent.cs
rename to Content.Server/Power/Components/CablePlacerComponent.cs
index 6b83e9de6c..a2eb805730 100644
--- a/Content.Server/Wires/Components/WirePlacerComponent.cs
+++ b/Content.Server/Power/Components/CablePlacerComponent.cs
@@ -9,28 +9,28 @@ using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
-namespace Content.Server.Wires.Components
+namespace Content.Server.Power.Components
{
[RegisterComponent]
- internal class WirePlacerComponent : Component, IAfterInteract
+ internal class CablePlacerComponent : Component, IAfterInteract
{
[Dependency] private readonly IMapManager _mapManager = default!;
///
- public override string Name => "WirePlacer";
+ public override string Name => "CablePlacer";
[ViewVariables]
- [DataField("wirePrototypeID")]
- private string? _wirePrototypeID = "HVWire";
+ [DataField("cablePrototypeID")]
+ private string? _cablePrototypeID = "CableHV";
[ViewVariables]
[DataField("blockingWireType")]
- private WireType _blockingWireType = WireType.HighVoltage;
+ private CableType _blockingCableType = CableType.HighVoltage;
///
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
- if (_wirePrototypeID == null)
+ if (_cablePrototypeID == null)
return true;
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return true;
@@ -41,7 +41,7 @@ namespace Content.Server.Wires.Components
return true;
foreach (var anchored in grid.GetAnchoredEntities(snapPos))
{
- if (Owner.EntityManager.ComponentManager.TryGetComponent(anchored, out var wire) && wire.WireType == _blockingWireType)
+ if (Owner.EntityManager.ComponentManager.TryGetComponent(anchored, out var wire) && wire.CableType == _blockingCableType)
{
return true;
}
@@ -51,7 +51,7 @@ namespace Content.Server.Wires.Components
&& !EntitySystem.Get().Use(Owner.Uid, stack, 1))
return true;
- Owner.EntityManager.SpawnEntity(_wirePrototypeID, grid.GridTileToLocal(snapPos));
+ Owner.EntityManager.SpawnEntity(_cablePrototypeID, grid.GridTileToLocal(snapPos));
return true;
}
}
diff --git a/Content.Server/Power/Components/CableVisComponent.cs b/Content.Server/Power/Components/CableVisComponent.cs
new file mode 100644
index 0000000000..add61d7de5
--- /dev/null
+++ b/Content.Server/Power/Components/CableVisComponent.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Power.Components
+{
+ [RegisterComponent]
+ public sealed class CableVisComponent : Component
+ {
+ public override string Name => "CableVis";
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("node")]
+ public string? Node;
+ }
+}
diff --git a/Content.Server/Battery/Components/ExaminableBatteryComponent.cs b/Content.Server/Power/Components/ExaminableBatteryComponent.cs
similarity index 96%
rename from Content.Server/Battery/Components/ExaminableBatteryComponent.cs
rename to Content.Server/Power/Components/ExaminableBatteryComponent.cs
index 202572e568..e5da57a61c 100644
--- a/Content.Server/Battery/Components/ExaminableBatteryComponent.cs
+++ b/Content.Server/Power/Components/ExaminableBatteryComponent.cs
@@ -5,7 +5,7 @@ using Robust.Shared.Localization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
-namespace Content.Server.Battery.Components
+namespace Content.Server.Power.Components
{
[RegisterComponent]
public class ExaminableBatteryComponent : Component, IExamine
diff --git a/Content.Server/Power/Components/IPowerNetManager.cs b/Content.Server/Power/Components/IPowerNetManager.cs
deleted file mode 100644
index 51487f2297..0000000000
--- a/Content.Server/Power/Components/IPowerNetManager.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using Content.Server.NodeContainer.NodeGroups;
-
-namespace Content.Server.Power.Components
-{
- ///
- /// Maintains a set of s that need to be updated with .
- /// Defers updating to reduce recalculations when a group is altered multiple times in a frame.
- ///
- public interface IPowerNetManager
- {
- ///
- /// Queue up an to be updated.
- ///
- void AddDirtyPowerNet(IPowerNet powerNet);
-
- void Update(float frameTime);
- }
-
- public class PowerNetManager : IPowerNetManager
- {
- private readonly HashSet _dirtyPowerNets = new();
-
- public void AddDirtyPowerNet(IPowerNet powerNet)
- {
- _dirtyPowerNets.Add(powerNet);
- }
-
- public void Update(float frameTime)
- {
- foreach (var powerNet in _dirtyPowerNets)
- {
- powerNet.UpdateConsumerReceivedPower();
- }
- _dirtyPowerNets.Clear();
- }
- }
-}
diff --git a/Content.Server/Power/Components/PowerConsumerComponent.cs b/Content.Server/Power/Components/PowerConsumerComponent.cs
index 94c3f2b064..a9c44434bd 100644
--- a/Content.Server/Power/Components/PowerConsumerComponent.cs
+++ b/Content.Server/Power/Components/PowerConsumerComponent.cs
@@ -1,13 +1,15 @@
#nullable enable
-using System;
-using System.Diagnostics;
-using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.Power.NodeGroups;
+using Content.Server.Power.Pow3r;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Power.Components
{
+ ///
+ /// Draws power directly from an MV or HV wire it is on top of.
+ ///
[RegisterComponent]
public class PowerConsumerComponent : BasePowerNetComponent
{
@@ -16,28 +18,19 @@ namespace Content.Server.Power.Components
///
/// How much power this needs to be fully powered.
///
- [ViewVariables(VVAccess.ReadWrite)]
- public int DrawRate { get => _drawRate; set => SetDrawRate(value); }
[DataField("drawRate")]
- private int _drawRate;
-
- ///
- /// Determines which s receive power when there is not enough
- /// power for each.
- ///
[ViewVariables(VVAccess.ReadWrite)]
- public Priority Priority { get => _priority; set => SetPriority(value); }
- [DataField("priority")]
- private Priority _priority = Priority.First;
+ public float DrawRate { get => NetworkLoad.DesiredPower; set => NetworkLoad.DesiredPower = value; }
///
/// How much power this is currently receiving from s.
///
[ViewVariables]
- public int ReceivedPower { get => _receivedPower; set => SetReceivedPower(value); }
- private int _receivedPower;
+ public float ReceivedPower => NetworkLoad.ReceivingPower;
- public event EventHandler? OnReceivedPowerChanged;
+ public float LastReceived = float.NaN;
+
+ public PowerState.Load NetworkLoad { get; } = new();
protected override void AddSelfToNet(IPowerNet powerNet)
{
@@ -48,44 +41,5 @@ namespace Content.Server.Power.Components
{
powerNet.RemoveConsumer(this);
}
-
- private void SetDrawRate(int newDrawRate)
- {
- var oldDrawRate = DrawRate;
- _drawRate = newDrawRate; //must be set before updating powernet, as it checks the DrawRate of every consumer
- Net.UpdateConsumerDraw(this, oldDrawRate, newDrawRate);
- }
-
- private void SetReceivedPower(int newReceivedPower)
- {
- Debug.Assert(newReceivedPower >= 0 && newReceivedPower <= DrawRate);
- if(_receivedPower == newReceivedPower) return;
- _receivedPower = newReceivedPower;
- OnReceivedPowerChanged?.Invoke(this, new ReceivedPowerChangedEventArgs(_drawRate, _receivedPower));
- }
-
- private void SetPriority(Priority newPriority)
- {
- Net.UpdateConsumerPriority(this, Priority, newPriority);
- _priority = newPriority;
- }
- }
-
- public enum Priority
- {
- First,
- Last,
- }
-
- public class ReceivedPowerChangedEventArgs : EventArgs
- {
- public readonly int DrawRate;
- public readonly int ReceivedPower;
-
- public ReceivedPowerChangedEventArgs(int drawRate, int receivedPower)
- {
- DrawRate = drawRate;
- ReceivedPower = receivedPower;
- }
}
}
diff --git a/Content.Server/Power/Components/PowerNetworkBatteryComponent.cs b/Content.Server/Power/Components/PowerNetworkBatteryComponent.cs
new file mode 100644
index 0000000000..5da505e03d
--- /dev/null
+++ b/Content.Server/Power/Components/PowerNetworkBatteryComponent.cs
@@ -0,0 +1,119 @@
+using Content.Server.Power.Pow3r;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Power.Components
+{
+ ///
+ /// Glue component that manages the pow3r network node for batteries that are connected to the power network.
+ ///
+ ///
+ /// This needs components like to work correctly,
+ /// and battery storage should be handed off to components like .
+ ///
+ [RegisterComponent]
+ public sealed class PowerNetworkBatteryComponent : Component
+ {
+ public override string Name => "PowerNetworkBattery";
+
+ [DataField("maxChargeRate")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float MaxChargeRate
+ {
+ get => NetworkBattery.MaxChargeRate;
+ set => NetworkBattery.MaxChargeRate = value;
+ }
+
+ [DataField("maxSupply")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float MaxSupply
+ {
+ get => NetworkBattery.MaxSupply;
+ set => NetworkBattery.MaxSupply = value;
+ }
+
+ [DataField("supplyRampTolerance")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float SupplyRampTolerance
+ {
+ get => NetworkBattery.SupplyRampTolerance;
+ set => NetworkBattery.SupplyRampTolerance = value;
+ }
+
+ [DataField("supplyRampRate")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float SupplyRampRate
+ {
+ get => NetworkBattery.SupplyRampRate;
+ set => NetworkBattery.SupplyRampRate = value;
+ }
+
+ [DataField("supplyRampPosition")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float SupplyRampPosition
+ {
+ get => NetworkBattery.SupplyRampPosition;
+ set => NetworkBattery.SupplyRampPosition = value;
+ }
+
+ [DataField("currentSupply")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float CurrentSupply
+ {
+ get => NetworkBattery.CurrentSupply;
+ set => NetworkBattery.CurrentSupply = value;
+ }
+
+ [DataField("currentReceiving")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float CurrentReceiving
+ {
+ get => NetworkBattery.CurrentReceiving;
+ set => NetworkBattery.CurrentReceiving = value;
+ }
+
+ [DataField("loadingNetworkDemand")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float LoadingNetworkDemand
+ {
+ get => NetworkBattery.LoadingNetworkDemand;
+ set => NetworkBattery.LoadingNetworkDemand = value;
+ }
+
+ [DataField("enabled")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool Enabled
+ {
+ get => NetworkBattery.Enabled;
+ set => NetworkBattery.Enabled = value;
+ }
+
+ [DataField("canCharge")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool CanCharge
+ {
+ get => NetworkBattery.CanCharge;
+ set => NetworkBattery.CanCharge = value;
+ }
+
+ [DataField("canDisharge")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool CanDischarge
+ {
+ get => NetworkBattery.CanDischarge;
+ set => NetworkBattery.CanDischarge = value;
+ }
+
+ [DataField("efficiency")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float Efficiency
+ {
+ get => NetworkBattery.Efficiency;
+ set => NetworkBattery.Efficiency = value;
+ }
+
+ [ViewVariables]
+ public PowerState.Battery NetworkBattery { get; } = new();
+ }
+}
diff --git a/Content.Server/Power/Components/PowerProviderComponent.cs b/Content.Server/Power/Components/PowerProviderComponent.cs
deleted file mode 100644
index 1b8a35cbf2..0000000000
--- a/Content.Server/Power/Components/PowerProviderComponent.cs
+++ /dev/null
@@ -1,174 +0,0 @@
-#nullable enable
-using System;
-using System.Collections.Generic;
-using Content.Server.APC;
-using Content.Server.APC.Components;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Serialization.Manager.Attributes;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Server.Power.Components
-{
- ///
- /// Relays s in an area to a so they can receive power.
- ///
- public interface IPowerProvider
- {
- void AddReceiver(PowerReceiverComponent receiver);
-
- void RemoveReceiver(PowerReceiverComponent receiver);
-
- void UpdateReceiverLoad(int oldLoad, int newLoad);
-
- public IEntity? ProviderOwner { get; }
-
- public bool HasApcPower { get; }
- }
-
- [RegisterComponent]
- public class PowerProviderComponent : BaseApcNetComponent, IPowerProvider
- {
- public override string Name => "PowerProvider";
-
- public IEntity ProviderOwner => Owner;
-
- [ViewVariables]
- public bool HasApcPower => Net.Powered;
-
- ///
- /// The max distance this can transmit power to s from.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public int PowerTransferRange { get => _powerTransferRange; set => SetPowerTransferRange(value); }
- [DataField("powerTransferRange")]
- private int _powerTransferRange = 3;
-
- [ViewVariables]
- public IReadOnlyList LinkedReceivers => _linkedReceivers;
- private List _linkedReceivers = new();
-
- ///
- /// If s should consider connecting to this.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public bool Connectable { get; private set; } = true;
-
- public static readonly IPowerProvider NullProvider = new NullPowerProvider();
-
- public void AddReceiver(PowerReceiverComponent receiver)
- {
- var oldLoad = GetTotalLoad();
- _linkedReceivers.Add(receiver);
- var newLoad = oldLoad + receiver.Load;
- Net.UpdatePowerProviderReceivers(this, oldLoad, newLoad);
- }
-
- public void RemoveReceiver(PowerReceiverComponent receiver)
- {
- var oldLoad = GetTotalLoad();
- _linkedReceivers.Remove(receiver);
- var newLoad = oldLoad - receiver.Load;
- Net.UpdatePowerProviderReceivers(this, oldLoad, newLoad);
- }
-
- public void UpdateReceiverLoad(int oldLoad, int newLoad)
- {
- Net.UpdatePowerProviderReceivers(this, oldLoad, newLoad);
- }
-
- protected override void Startup()
- {
- base.Startup();
- foreach (var receiver in FindAvailableReceivers())
- {
- receiver.Provider = this;
- }
- }
-
- protected override void OnRemove()
- {
- Connectable = false;
- var receivers = _linkedReceivers.ToArray();
- foreach (var receiver in receivers)
- {
- receiver.ClearProvider();
- }
- foreach (var receiver in receivers)
- {
- receiver.TryFindAndSetProvider();
- }
- base.OnRemove();
- }
-
- private List FindAvailableReceivers()
- {
- var nearbyEntities = IoCManager.Resolve()
- .GetEntitiesInRange(Owner, PowerTransferRange);
-
- var receivers = new List();
-
- foreach (var entity in nearbyEntities)
- {
- if (entity.TryGetComponent(out var receiver) &&
- receiver.Connectable &&
- receiver.NeedsProvider &&
- receiver.Owner.Transform.Coordinates.TryDistance(Owner.EntityManager, Owner.Transform.Coordinates, out var distance) &&
- distance < Math.Min(PowerTransferRange, receiver.PowerReceptionRange))
- {
- receivers.Add(receiver);
- }
- }
- return receivers;
- }
-
- protected override void AddSelfToNet(IApcNet apcNet)
- {
- apcNet.AddPowerProvider(this);
- }
-
- protected override void RemoveSelfFromNet(IApcNet apcNet)
- {
- apcNet.RemovePowerProvider(this);
- }
-
- private void SetPowerTransferRange(int newPowerTransferRange)
- {
- var receivers = _linkedReceivers.ToArray();
-
- foreach (var receiver in receivers)
- {
- receiver.ClearProvider();
- }
- _powerTransferRange = newPowerTransferRange;
-
- foreach (var receiver in receivers)
- {
- receiver.TryFindAndSetProvider();
- }
- }
-
- private int GetTotalLoad()
- {
- var load = 0;
- foreach (var receiver in _linkedReceivers)
- {
- load += receiver.Load;
- }
- return load;
- }
-
- private class NullPowerProvider : IPowerProvider
- {
- ///
- /// It is important that this returns false, so s with a have no power.
- ///
- public bool HasApcPower => false;
-
- public void AddReceiver(PowerReceiverComponent receiver) { }
- public void RemoveReceiver(PowerReceiverComponent receiver) { }
- public void UpdateReceiverLoad(int oldLoad, int newLoad) { }
- public IEntity? ProviderOwner => default;
- }
- }
-}
diff --git a/Content.Server/Power/Components/PowerSupplierComponent.cs b/Content.Server/Power/Components/PowerSupplierComponent.cs
index bbd8b658fd..c145cd6803 100644
--- a/Content.Server/Power/Components/PowerSupplierComponent.cs
+++ b/Content.Server/Power/Components/PowerSupplierComponent.cs
@@ -1,5 +1,6 @@
#nullable enable
-using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.Power.NodeGroups;
+using Content.Server.Power.Pow3r;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
@@ -12,9 +13,45 @@ namespace Content.Server.Power.Components
public override string Name => "PowerSupplier";
[ViewVariables(VVAccess.ReadWrite)]
- public int SupplyRate { get => _supplyRate; set => SetSupplyRate(value); }
[DataField("supplyRate")]
- private int _supplyRate;
+ public float MaxSupply { get => NetworkSupply.MaxSupply; set => NetworkSupply.MaxSupply = value; }
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("supplyRampTolerance")]
+ public float SupplyRampTolerance
+ {
+ get => NetworkSupply.SupplyRampTolerance;
+ set => NetworkSupply.SupplyRampTolerance = value;
+ }
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("supplyRampRate")]
+ public float SupplyRampRate
+ {
+ get => NetworkSupply.SupplyRampRate;
+ set => NetworkSupply.SupplyRampRate = value;
+ }
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("supplyRampPosition")]
+ public float SupplyRampPosition
+ {
+ get => NetworkSupply.SupplyRampPosition;
+ set => NetworkSupply.SupplyRampPosition = value;
+ }
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("enabled")]
+ public bool Enabled
+ {
+ get => NetworkSupply.Enabled;
+ set => NetworkSupply.Enabled = value;
+ }
+
+ [ViewVariables] public float CurrentSupply => NetworkSupply.CurrentSupply;
+
+ [ViewVariables]
+ public PowerState.Supply NetworkSupply { get; } = new();
protected override void AddSelfToNet(IPowerNet powerNet)
{
@@ -25,11 +62,5 @@ namespace Content.Server.Power.Components
{
powerNet.RemoveSupplier(this);
}
-
- private void SetSupplyRate(int newSupplyRate)
- {
- Net.UpdateSupplierSupply(this, SupplyRate, newSupplyRate);
- _supplyRate = newSupplyRate;
- }
}
}
diff --git a/Content.Server/Battery/DrainAllBatteriesCommand.cs b/Content.Server/Power/DrainAllBatteriesCommand.cs
similarity index 78%
rename from Content.Server/Battery/DrainAllBatteriesCommand.cs
rename to Content.Server/Power/DrainAllBatteriesCommand.cs
index 561d171d5b..431e68c4d8 100644
--- a/Content.Server/Battery/DrainAllBatteriesCommand.cs
+++ b/Content.Server/Power/DrainAllBatteriesCommand.cs
@@ -1,12 +1,12 @@
#nullable enable
using Content.Server.Administration;
-using Content.Server.Battery.Components;
+using Content.Server.Power.Components;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
-namespace Content.Server.Battery
+namespace Content.Server.Power
{
[AdminCommand(AdminFlags.Admin)]
public class DrainAllBatteriesCommand : IConsoleCommand
@@ -23,8 +23,8 @@ namespace Content.Server.Battery
return;
}
- var entityManager = IoCManager.Resolve();
- foreach (var batteryComp in entityManager.ComponentManager.EntityQuery())
+ var comp = IoCManager.Resolve();
+ foreach (var batteryComp in comp.EntityQuery())
{
batteryComp.CurrentCharge = 0;
}
diff --git a/Content.Server/Power/EntitySystems/BatterySystem.cs b/Content.Server/Power/EntitySystems/BatterySystem.cs
new file mode 100644
index 0000000000..ce54a7e866
--- /dev/null
+++ b/Content.Server/Power/EntitySystems/BatterySystem.cs
@@ -0,0 +1,42 @@
+#nullable enable
+using Content.Server.Power.Components;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+
+namespace Content.Server.Power.EntitySystems
+{
+ [UsedImplicitly]
+ public class BatterySystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(PreSync);
+ SubscribeLocalEvent(PostSync);
+ }
+
+ private void PreSync(EntityUid uid, BatteryComponent component, NetworkBatteryPreSync args)
+ {
+ var networkBattery = ComponentManager.GetComponent(uid);
+
+ networkBattery.NetworkBattery.Capacity = component.MaxCharge;
+ networkBattery.NetworkBattery.CurrentStorage = component.CurrentCharge;
+ }
+
+ private void PostSync(EntityUid uid, BatteryComponent component, NetworkBatteryPostSync args)
+ {
+ var networkBattery = ComponentManager.GetComponent(uid);
+
+ component.CurrentCharge = networkBattery.NetworkBattery.CurrentStorage;
+ }
+
+ public override void Update(float frameTime)
+ {
+ foreach (var comp in ComponentManager.EntityQuery())
+ {
+ comp.OnUpdate(frameTime);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Power/EntitySystems/CableVisSystem.cs b/Content.Server/Power/EntitySystems/CableVisSystem.cs
new file mode 100644
index 0000000000..d0425d63b5
--- /dev/null
+++ b/Content.Server/Power/EntitySystems/CableVisSystem.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.EntitySystems;
+using Content.Server.Power.Components;
+using Content.Server.Power.Nodes;
+using Content.Shared.Wires;
+using JetBrains.Annotations;
+using Robust.Server.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+
+namespace Content.Server.Power.EntitySystems
+{
+ [UsedImplicitly]
+ public sealed class CableVisSystem : EntitySystem
+ {
+ [Dependency] private readonly IMapManager _mapManager = default!;
+
+ private readonly HashSet _toUpdate = new();
+
+ public void QueueUpdate(EntityUid uid)
+ {
+ _toUpdate.Add(uid);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ UpdatesAfter.Add(typeof(NodeGroupSystem));
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ foreach (var uid in _toUpdate)
+ {
+ if (!ComponentManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer)
+ || !ComponentManager.TryGetComponent(uid, out CableVisComponent? cableVis)
+ || !ComponentManager.TryGetComponent(uid, out AppearanceComponent? appearance))
+ {
+ continue;
+ }
+
+ if (cableVis.Node == null)
+ continue;
+
+ var mask = WireVisDirFlags.None;
+
+ var transform = ComponentManager.GetComponent(uid);
+ var grid = _mapManager.GetGrid(transform.GridID);
+ var tile = grid.TileIndicesFor(transform.Coordinates);
+ var node = nodeContainer.GetNode(cableVis.Node);
+
+ foreach (var reachable in node.ReachableNodes)
+ {
+ if (reachable is not CableNode)
+ continue;
+
+ var otherTransform = reachable.Owner.Transform;
+ if (otherTransform.GridID != grid.Index)
+ continue;
+
+ var otherTile = grid.TileIndicesFor(otherTransform.Coordinates);
+ var diff = otherTile - tile;
+
+ mask |= diff switch
+ {
+ (0, 1) => WireVisDirFlags.North,
+ (0, -1) => WireVisDirFlags.South,
+ (1, 0) => WireVisDirFlags.East,
+ (-1, 0) => WireVisDirFlags.West,
+ _ => WireVisDirFlags.None
+ };
+ }
+
+ appearance.SetData(WireVisVisuals.ConnectedMask, mask);
+ }
+
+ _toUpdate.Clear();
+ }
+ }
+}
diff --git a/Content.Server/APC/PowerApcSystem.cs b/Content.Server/Power/EntitySystems/PowerApcSystem.cs
similarity index 59%
rename from Content.Server/APC/PowerApcSystem.cs
rename to Content.Server/Power/EntitySystems/PowerApcSystem.cs
index 1aefa669f4..e44b66f260 100644
--- a/Content.Server/APC/PowerApcSystem.cs
+++ b/Content.Server/Power/EntitySystems/PowerApcSystem.cs
@@ -1,16 +1,23 @@
#nullable enable
-using Content.Server.APC.Components;
+using Content.Server.Power.Components;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
-namespace Content.Server.APC
+namespace Content.Server.Power.EntitySystems
{
[UsedImplicitly]
internal sealed class PowerApcSystem : EntitySystem
{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ UpdatesAfter.Add(typeof(PowerNetSystem));
+ }
+
public override void Update(float frameTime)
{
- foreach (var apc in ComponentManager.EntityQuery(false))
+ foreach (var apc in ComponentManager.EntityQuery())
{
apc.Update();
}
diff --git a/Content.Server/Power/EntitySystems/PowerNetSystem.cs b/Content.Server/Power/EntitySystems/PowerNetSystem.cs
index bcb8ec6926..e2b54ac769 100644
--- a/Content.Server/Power/EntitySystems/PowerNetSystem.cs
+++ b/Content.Server/Power/EntitySystems/PowerNetSystem.cs
@@ -1,20 +1,351 @@
#nullable enable
+using System.Collections.Generic;
+using Content.Server.NodeContainer.EntitySystems;
using Content.Server.Power.Components;
+using Content.Server.Power.NodeGroups;
+using Content.Server.Power.Pow3r;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
+using Robust.Shared.Maths;
namespace Content.Server.Power.EntitySystems
{
+ ///
+ /// Manages power networks, power state, and all power components.
+ ///
[UsedImplicitly]
public class PowerNetSystem : EntitySystem
{
- [Dependency] private readonly IPowerNetManager _powerNetManager = default!;
+ private readonly PowerState _powerState = new();
+ private readonly HashSet _powerNetReconnectQueue = new();
+ private readonly HashSet _apcNetReconnectQueue = new();
+
+ private int _nextId = 1;
+ private readonly BatteryRampPegSolver _solver = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ UpdatesAfter.Add(typeof(NodeGroupSystem));
+
+ SubscribeLocalEvent(ApcPowerReceiverInit);
+ SubscribeLocalEvent