diff --git a/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs b/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs
index 2f35cd924f..3765d45fa2 100644
--- a/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs
+++ b/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs
@@ -27,7 +27,6 @@ namespace Content.Client.AME.UI
_window.ToggleInjection.OnPressed += _ => ButtonPressed(UiButton.ToggleInjection);
_window.IncreaseFuelButton.OnPressed += _ => ButtonPressed(UiButton.IncreaseFuel);
_window.DecreaseFuelButton.OnPressed += _ => ButtonPressed(UiButton.DecreaseFuel);
- _window.RefreshPartsButton.OnPressed += _ => ButtonPressed(UiButton.RefreshParts);
}
///
diff --git a/Content.Client/AME/UI/AMEWindow.cs b/Content.Client/AME/UI/AMEWindow.cs
index 94b0e006b5..108806eb55 100644
--- a/Content.Client/AME/UI/AMEWindow.cs
+++ b/Content.Client/AME/UI/AMEWindow.cs
@@ -16,7 +16,6 @@ namespace Content.Client.AME.UI
public Button ToggleInjection { get; set; }
public Button IncreaseFuelButton { get; set; }
public Button DecreaseFuelButton { get; set; }
- public Button RefreshPartsButton { get; set; }
public ProgressBar? FuelMeter { get; set; }
public Label FuelAmount { get; set; }
public Label InjectionAmount { get; set; }
@@ -85,7 +84,6 @@ namespace Content.Client.AME.UI
{
Children =
{
- (RefreshPartsButton = new Button {Text = Loc.GetString("ame-window-refresh-parts-button"), StyleClasses = {StyleBase.ButtonOpenBoth }, Disabled = true }),
new Label { Text = Loc.GetString("ame-window-core-count-label")},
(CoreCount = new Label { Text = "0"}),
}
@@ -159,8 +157,6 @@ namespace Content.Client.AME.UI
InjectionStatus.Text = Loc.GetString("ame-window-engine-injection-status-injecting-label");
}
- RefreshPartsButton.Disabled = castState.Injecting;
-
CoreCount.Text = $"{castState.CoreCount}";
InjectionAmount.Text = $"{castState.InjectionAmount}";
}
diff --git a/Content.Client/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs
index fe18e1fc5f..d714789aa0 100644
--- a/Content.Client/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs
+++ b/Content.Client/Atmos/EntitySystems/AtmosDebugOverlaySystem.cs
@@ -13,7 +13,7 @@ using Robust.Shared.Maths;
namespace Content.Client.Atmos.EntitySystems
{
[UsedImplicitly]
- internal sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem, IResettingEntitySystem
+ internal sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
@@ -37,6 +37,7 @@ namespace Content.Client.Atmos.EntitySystems
{
base.Initialize();
+ SubscribeNetworkEvent(Reset);
SubscribeNetworkEvent(HandleAtmosDebugOverlayMessage);
SubscribeNetworkEvent(HandleAtmosDebugOverlayDisableMessage);
@@ -66,7 +67,7 @@ namespace Content.Client.Atmos.EntitySystems
overlayManager.RemoveOverlay();
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
_tileData.Clear();
}
diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs
index 5f77e97c15..4af4b111e4 100644
--- a/Content.Client/Chat/Managers/ChatManager.cs
+++ b/Content.Client/Chat/Managers/ChatManager.cs
@@ -585,6 +585,10 @@ namespace Content.Client.Chat.Managers
private void EnqueueSpeechBubble(IEntity entity, string contents, SpeechBubble.SpeechType speechType)
{
+ // Don't enqueue speech bubbles for other maps. TODO: Support multiple viewports/maps?
+ if (entity.Transform.MapID != _eyeManager.CurrentMap)
+ return;
+
if (!_queuedSpeechBubbles.TryGetValue(entity.Uid, out var queueData))
{
queueData = new SpeechBubbleQueueData();
@@ -600,8 +604,7 @@ namespace Content.Client.Chat.Managers
private void CreateSpeechBubble(IEntity entity, SpeechBubbleData speechData)
{
- var bubble =
- SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this);
+ var bubble = SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this);
if (_activeSpeechBubbles.TryGetValue(entity.Uid, out var existing))
{
diff --git a/Content.Client/Chat/UI/ChatBox.cs b/Content.Client/Chat/UI/ChatBox.cs
index 08a6ffaf2f..1052f499c2 100644
--- a/Content.Client/Chat/UI/ChatBox.cs
+++ b/Content.Client/Chat/UI/ChatBox.cs
@@ -185,10 +185,10 @@ namespace Content.Client.Chat.UI
{
Children =
{
- new Control{MinSize = (10,0)},
+ new Control{MinSize = (4,0)},
(_filterVBox = new VBoxContainer
{
- SeparationOverride = 10
+ SeparationOverride = 4
})
}
}
@@ -203,7 +203,7 @@ namespace Content.Client.Chat.UI
{
(_channelSelectorHBox = new HBoxContainer
{
- SeparationOverride = 4
+ SeparationOverride = 1
})
}
};
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/HealthOverlay/HealthOverlaySystem.cs b/Content.Client/HealthOverlay/HealthOverlaySystem.cs
index 5dd348934f..2e92fb4245 100644
--- a/Content.Client/HealthOverlay/HealthOverlaySystem.cs
+++ b/Content.Client/HealthOverlay/HealthOverlaySystem.cs
@@ -13,7 +13,7 @@ using Robust.Shared.IoC;
namespace Content.Client.HealthOverlay
{
[UsedImplicitly]
- public class HealthOverlaySystem : EntitySystem, IResettingEntitySystem
+ public class HealthOverlaySystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
@@ -44,10 +44,11 @@ namespace Content.Client.HealthOverlay
{
base.Initialize();
+ SubscribeNetworkEvent(Reset);
SubscribeLocalEvent(HandlePlayerAttached);
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
foreach (var gui in _guis.Values)
{
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/MobState/MobStateComponent.cs b/Content.Client/MobState/MobStateComponent.cs
index caa215c0c1..55d67780ef 100644
--- a/Content.Client/MobState/MobStateComponent.cs
+++ b/Content.Client/MobState/MobStateComponent.cs
@@ -1,4 +1,5 @@
using Content.Shared.MobState;
+using Content.Shared.MobState.Components;
using Content.Shared.MobState.State;
using Robust.Shared.GameObjects;
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/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs
index a40dd6bef3..3bdd6478cf 100644
--- a/Content.Client/Stylesheets/StyleNano.cs
+++ b/Content.Client/Stylesheets/StyleNano.cs
@@ -1,4 +1,4 @@
-#nullable enable
+#nullable enable
using System.Linq;
using Content.Client.Actions.UI;
using Content.Client.Examine;
diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs
index a0fd186a07..36a01098d6 100644
--- a/Content.Client/Verbs/VerbSystem.cs
+++ b/Content.Client/Verbs/VerbSystem.cs
@@ -28,7 +28,7 @@ using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.Verbs
{
[UsedImplicitly]
- public sealed class VerbSystem : SharedVerbSystem, IResettingEntitySystem
+ public sealed class VerbSystem : SharedVerbSystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
@@ -47,6 +47,7 @@ namespace Content.Client.Verbs
{
base.Initialize();
+ SubscribeNetworkEvent(Reset);
SubscribeNetworkEvent(FillEntityPopup);
SubscribeNetworkEvent(HandleContainerVisibilityMessage);
@@ -68,7 +69,7 @@ namespace Content.Client.Verbs
CommandBinds.Unregister();
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
ToggleContainerVisibility?.Invoke(this, false);
}
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/ContentIntegrationTest.cs b/Content.IntegrationTests/ContentIntegrationTest.cs
index d550bc7cc9..765d3ade75 100644
--- a/Content.IntegrationTests/ContentIntegrationTest.cs
+++ b/Content.IntegrationTests/ContentIntegrationTest.cs
@@ -7,6 +7,8 @@ using Content.Server.IoC;
using Content.Shared.CCVar;
using Moq;
using NUnit.Framework;
+using Robust.Client;
+using Robust.Server;
using Robust.Server.Maps;
using Robust.Shared;
using Robust.Shared.ContentPack;
@@ -31,6 +33,13 @@ namespace Content.IntegrationTests
FailureLogLevel = LogLevel.Warning
};
+ // Load content resources, but not config and user data.
+ options.Options = new GameControllerOptions()
+ {
+ LoadContentResources = true,
+ LoadConfigAndUserData = false,
+ };
+
options.ContentStart = true;
options.ContentAssemblies = new[]
@@ -57,13 +66,6 @@ namespace Content.IntegrationTests
});
};
- // Connecting to Discord is a massive waste of time.
- // Basically just makes the CI logs a mess.
- options.CVarOverrides[CVars.DiscordEnabled.Name] = "false";
-
- // Avoid preloading textures in tests.
- options.CVarOverrides.TryAdd(CVars.TexturePreloadingEnabled.Name, "false");
-
return base.StartClient(options);
}
@@ -74,6 +76,13 @@ namespace Content.IntegrationTests
FailureLogLevel = LogLevel.Warning
};
+ // Load content resources, but not config and user data.
+ options.Options = new ServerOptions()
+ {
+ LoadConfigAndUserData = false,
+ LoadContentResources = true,
+ };
+
options.ContentStart = true;
options.ContentAssemblies = new[]
@@ -105,15 +114,23 @@ namespace Content.IntegrationTests
// Disable holidays as some of them might mess with the map at round start.
options.CVarOverrides[CCVars.HolidaysEnabled.Name] = "false";
- // Avoid loading a large map by default for integration tests.
- options.CVarOverrides[CCVars.GameMap.Name] = "Maps/Test/empty.yml";
+ // Avoid loading a large map by default for integration tests if none has been specified.
+ if(!options.CVarOverrides.ContainsKey(CCVars.GameMap.Name))
+ options.CVarOverrides[CCVars.GameMap.Name] = "Maps/Test/empty.yml";
return base.StartServer(options);
}
protected ServerIntegrationInstance StartServerDummyTicker(ServerIntegrationOptions options = null)
{
- options ??= new ServerIntegrationOptions();
+ options ??= new ServerContentIntegrationOption();
+
+ // Load content resources, but not config and user data.
+ options.Options = new ServerOptions()
+ {
+ LoadConfigAndUserData = false,
+ LoadContentResources = true,
+ };
// Dummy game ticker.
options.CVarOverrides[CCVars.GameDummyTicker.Name] = "true";
@@ -229,6 +246,12 @@ namespace Content.IntegrationTests
FailureLogLevel = LogLevel.Warning;
}
+ public override GameControllerOptions Options { get; set; } = new()
+ {
+ LoadContentResources = true,
+ LoadConfigAndUserData = false,
+ };
+
public Action ContentBeforeIoC { get; set; }
}
@@ -239,6 +262,12 @@ namespace Content.IntegrationTests
FailureLogLevel = LogLevel.Warning;
}
+ public override ServerOptions Options { get; set; } = new()
+ {
+ LoadContentResources = true,
+ LoadConfigAndUserData = false,
+ };
+
public Action ContentBeforeIoC { get; set; }
}
}
diff --git a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs
index ebc11c9c2e..c6cf878851 100644
--- a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs
+++ b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs
@@ -20,7 +20,13 @@ namespace Content.IntegrationTests.Tests.Commands
[TestCase(false)]
public async Task RestartRoundAfterStart(bool lobbyEnabled)
{
- var (_, server) = await StartConnectedServerClientPair();
+ var (_, server) = await StartConnectedServerClientPair(serverOptions: new ServerContentIntegrationOption
+ {
+ CVarOverrides =
+ {
+ [CCVars.GameMap.Name] = "Maps/saltern.yml"
+ }
+ });
await server.WaitIdleAsync();
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/DoAfter/DoAfterServerTest.cs b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
index 16ef7b43c9..570178cf44 100644
--- a/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
+++ b/Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
@@ -39,7 +39,7 @@ namespace Content.IntegrationTests.Tests.DoAfter
var mob = entityManager.SpawnEntity("Dummy", MapCoordinates.Nullspace);
var cancelToken = new CancellationTokenSource();
var args = new DoAfterEventArgs(mob, tickTime / 2, cancelToken.Token);
- task = EntitySystem.Get().DoAfter(args);
+ task = EntitySystem.Get().WaitDoAfter(args);
});
await server.WaitRunTicks(1);
@@ -62,7 +62,7 @@ namespace Content.IntegrationTests.Tests.DoAfter
var mob = entityManager.SpawnEntity("Dummy", MapCoordinates.Nullspace);
var cancelToken = new CancellationTokenSource();
var args = new DoAfterEventArgs(mob, tickTime * 2, cancelToken.Token);
- task = EntitySystem.Get().DoAfter(args);
+ task = EntitySystem.Get().WaitDoAfter(args);
cancelToken.Cancel();
});
diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs
index 63f5f48633..6e5bbb5469 100644
--- a/Content.IntegrationTests/Tests/EntityTest.cs
+++ b/Content.IntegrationTests/Tests/EntityTest.cs
@@ -2,8 +2,9 @@ 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;
using NUnit.Framework;
using Robust.Shared.GameObjects;
@@ -21,7 +22,12 @@ namespace Content.IntegrationTests.Tests
[Test]
public async Task SpawnTest()
{
- var server = StartServerDummyTicker();
+ var options = new ServerContentIntegrationOption()
+ {
+ CVarOverrides = {{CCVars.AIMaxUpdates.Name, int.MaxValue.ToString()}}
+ };
+
+ var server = StartServerDummyTicker(options);
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency();
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/ResettingEntitySystemTests.cs b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs
index a7b8740c73..58af6d2bc4 100644
--- a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs
+++ b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs
@@ -9,15 +9,22 @@ using Robust.Shared.Reflection;
namespace Content.IntegrationTests.Tests
{
[TestFixture]
- [TestOf(typeof(IResettingEntitySystem))]
+ [TestOf(typeof(RoundRestartCleanupEvent))]
public class ResettingEntitySystemTests : ContentIntegrationTest
{
[Reflect(false)]
- private class TestResettingEntitySystem : EntitySystem, IResettingEntitySystem
+ private class TestRoundRestartCleanupEvent : EntitySystem
{
public bool HasBeenReset { get; set; }
- public void Reset()
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(Reset);
+ }
+
+ public void Reset(RoundRestartCleanupEvent ev)
{
HasBeenReset = true;
}
@@ -30,7 +37,7 @@ namespace Content.IntegrationTests.Tests
{
ContentBeforeIoC = () =>
{
- IoCManager.Resolve().LoadExtraSystemType();
+ IoCManager.Resolve().LoadExtraSystemType();
}
});
@@ -43,7 +50,7 @@ namespace Content.IntegrationTests.Tests
{
Assert.That(gameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
- var system = entitySystemManager.GetEntitySystem();
+ var system = entitySystemManager.GetEntitySystem();
system.HasBeenReset = false;
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.IntegrationTests/Tests/StationEvents/StationEventsSystemTest.cs b/Content.IntegrationTests/Tests/StationEvents/StationEventsSystemTest.cs
index f58c049720..099e5fb6b1 100644
--- a/Content.IntegrationTests/Tests/StationEvents/StationEventsSystemTest.cs
+++ b/Content.IntegrationTests/Tests/StationEvents/StationEventsSystemTest.cs
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Content.Server.StationEvents;
+using Content.Shared.GameTicking;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -33,7 +34,7 @@ namespace Content.IntegrationTests.Tests.StationEvents
Assert.That(stationEvent.Occurrences > 0);
}
- stationEventsSystem.Reset();
+ stationEventsSystem.Reset(new RoundRestartCleanupEvent());
foreach (var stationEvent in stationEventsSystem.StationEvents)
{
diff --git a/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs b/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs
index 7b39db4ea0..5f66ec5e2e 100644
--- a/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs
+++ b/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs
@@ -21,7 +21,7 @@ namespace Content.Server.AI.Pathfinding.Accessible
///
/// Long-term can be used to do hierarchical pathfinding
[UsedImplicitly]
- public sealed class AiReachableSystem : EntitySystem, IResettingEntitySystem
+ public sealed class AiReachableSystem : EntitySystem
{
/*
* The purpose of this is to provide a higher-level / hierarchical abstraction of the actual pathfinding graph
@@ -81,6 +81,7 @@ namespace Content.Server.AI.Pathfinding.Accessible
public override void Initialize()
{
_pathfindingSystem = Get();
+ SubscribeLocalEvent(Reset);
SubscribeLocalEvent(RecalculateNodeRegions);
#if DEBUG
SubscribeNetworkEvent(HandleSubscription);
@@ -699,7 +700,7 @@ namespace Content.Server.AI.Pathfinding.Accessible
#endif
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
_queuedUpdates.Clear();
_regions.Clear();
diff --git a/Content.Server/AI/Pathfinding/PathfindingSystem.cs b/Content.Server/AI/Pathfinding/PathfindingSystem.cs
index 91c189a069..98cf1f500c 100644
--- a/Content.Server/AI/Pathfinding/PathfindingSystem.cs
+++ b/Content.Server/AI/Pathfinding/PathfindingSystem.cs
@@ -27,7 +27,7 @@ namespace Content.Server.AI.Pathfinding
/// This system handles pathfinding graph updates as well as dispatches to the pathfinder
/// (90% of what it's doing is graph updates so not much point splitting the 2 roles)
///
- public class PathfindingSystem : EntitySystem, IResettingEntitySystem
+ public class PathfindingSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
@@ -201,6 +201,7 @@ namespace Content.Server.AI.Pathfinding
public override void Initialize()
{
+ SubscribeLocalEvent(Reset);
SubscribeLocalEvent(QueueCollisionChangeMessage);
SubscribeLocalEvent(QueueMoveEvent);
SubscribeLocalEvent(QueueAccessChangeMessage);
@@ -385,7 +386,7 @@ namespace Content.Server.AI.Pathfinding
return true;
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
_graph.Clear();
_collidableUpdateQueue.Clear();
diff --git a/Content.Server/AME/AMENodeGroup.cs b/Content.Server/AME/AMENodeGroup.cs
index 920cff987c..a015d20a72 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,64 +37,42 @@ 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)
- {
- node.Owner.TryGetComponent(out var controller);
- _masterController = controller;
- }
- }
+ base.LoadNodes(groupNodes);
- protected override void OnRemoveNode(Node node)
- {
- base.OnRemoveNode(node);
- RefreshAMENodes(_masterController);
- if (_masterController != null && _masterController?.Owner == node.Owner) { _masterController = null; }
- }
+ var mapManager = IoCManager.Resolve();
+ var grid = mapManager.GetGrid(GridId);
- public void RefreshAMENodes(AMEControllerComponent? controller)
- {
- if(_masterController == null && controller != null)
- {
- _masterController = controller;
- }
-
- foreach (AMEShieldComponent core in _cores)
- {
- core.UnsetCore();
- }
- _cores.Clear();
-
- //Check each shield node to see if it meets core criteria
- foreach (Node node in Nodes)
+ foreach (var node in groupNodes)
{
var nodeOwner = node.Owner;
- if (!nodeOwner.TryGetComponent(out var shield)) { continue; }
-
- var grid = IoCManager.Resolve().GetGrid(nodeOwner.Transform.GridID);
- var nodeNeighbors = grid.GetCellsInSquareArea(nodeOwner.Transform.Coordinates, 1)
- .Select(sgc => nodeOwner.EntityManager.GetEntity(sgc))
- .Where(entity => entity != nodeOwner)
- .Select(entity => entity.TryGetComponent(out var adjshield) ? adjshield : null)
- .Where(adjshield => adjshield != null);
-
- if (nodeNeighbors.Count() >= 8)
+ if (nodeOwner.TryGetComponent(out AMEControllerComponent? controller))
{
- _cores.Add(shield);
+ _masterController = controller;
}
- }
- foreach (AMEShieldComponent core in _cores)
- {
- core.SetCore();
+ if (nodeOwner.TryGetComponent(out AMEShieldComponent? shield))
+ {
+ var nodeNeighbors = grid.GetCellsInSquareArea(nodeOwner.Transform.Coordinates, 1)
+ .Select(sgc => nodeOwner.EntityManager.GetEntity(sgc))
+ .Where(entity => entity != nodeOwner && entity.HasComponent());
+
+ if (nodeNeighbors.Count() >= 8)
+ {
+ _cores.Add(shield);
+ shield.SetCore();
+ }
+ else
+ {
+ shield.UnsetCore();
+ }
+ }
}
}
public void UpdateCoreVisuals(int injectionAmount, bool injecting)
{
-
var injectionStrength = CoreCount > 0 ? injectionAmount / CoreCount : 0;
foreach (AMEShieldComponent core in _cores)
@@ -119,7 +98,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..54873aa4e8 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();
@@ -212,9 +212,6 @@ namespace Content.Server.AME.Components
case UiButton.DecreaseFuel:
InjectionAmount = InjectionAmount > 0 ? InjectionAmount -= 2 : 0;
break;
- case UiButton.RefreshParts:
- RefreshParts();
- break;
}
GetAMENodeGroup()?.UpdateCoreVisuals(InjectionAmount, _injecting);
@@ -252,7 +249,7 @@ namespace Content.Server.AME.Components
_appearance?.SetData(AMEControllerVisuals.DisplayState, "off");
if (_powerSupplier != null)
{
- _powerSupplier.SupplyRate = 0;
+ _powerSupplier.MaxSupply = 0;
}
}
_injecting = !_injecting;
@@ -277,12 +274,6 @@ namespace Content.Server.AME.Components
}
- private void RefreshParts()
- {
- GetAMENodeGroup()?.RefreshAMENodes(this);
- UpdateUserInterface();
- }
-
private AMENodeGroup? GetAMENodeGroup()
{
Owner.TryGetComponent(out NodeContainerComponent? nodeContainer);
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 ccf5835eac..0000000000
--- a/Content.Server/APC/ApcNetSystem.cs
+++ /dev/null
@@ -1,45 +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, IResettingEntitySystem
- {
- [Dependency] private readonly IPauseManager _pauseManager = default!;
-
- private HashSet _apcNets = new();
-
- 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()
- {
- // 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/Actions/Spells/GiveItemSpell.cs b/Content.Server/Actions/Spells/GiveItemSpell.cs
new file mode 100644
index 0000000000..d7cac3a1b2
--- /dev/null
+++ b/Content.Server/Actions/Spells/GiveItemSpell.cs
@@ -0,0 +1,75 @@
+using Content.Server.Hands.Components;
+using Content.Server.Items;
+using Content.Server.Notification;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Actions.Components;
+using Content.Shared.Cooldown;
+using Content.Shared.Notification.Managers;
+using JetBrains.Annotations;
+using Robust.Shared.Audio;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Log;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Actions.Spells
+{
+ [UsedImplicitly]
+ [DataDefinition]
+ public class GiveItemSpell : IInstantAction
+ { //TODO: Needs to be an EntityPrototype for proper validation
+ [ViewVariables] [DataField("castMessage")] public string? CastMessage { get; set; } = default!;
+ [ViewVariables] [DataField("cooldown")] public float CoolDown { get; set; } = 1f;
+ [ViewVariables] [DataField("spellItem")] public string ItemProto { get; set; } = default!;
+
+ [ViewVariables] [DataField("castSound")] public string? CastSound { get; set; } = default!;
+
+ //Rubber-band snapping items into player's hands, originally was a workaround, later found it works quite well with stuns
+ //Not sure if needs fixing
+
+ public void DoInstantAction(InstantActionEventArgs args)
+ {
+ var caster = args.Performer;
+
+ if (!caster.TryGetComponent(out HandsComponent? handsComponent))
+ {
+ caster.PopupMessage(Loc.GetString("spell-fail-no-hands"));
+ return;
+ }
+
+ if (!EntitySystem.Get().CanInteract(caster)) return;
+
+ // TODO: Nix when we get EntityPrototype serializers
+ if (!IoCManager.Resolve().HasIndex(ItemProto))
+ {
+ Logger.Error($"Invalid prototype {ItemProto} supplied for {nameof(GiveItemSpell)}");
+ return;
+ }
+
+ // TODO: Look this is shitty and ideally a test would do it
+ var spawnedProto = caster.EntityManager.SpawnEntity(ItemProto, caster.Transform.MapPosition);
+
+ if (!spawnedProto.TryGetComponent(out ItemComponent? itemComponent))
+ {
+ Logger.Error($"Tried to use {nameof(GiveItemSpell)} but prototype has no {nameof(ItemComponent)}?");
+ spawnedProto.Delete();
+ return;
+ }
+
+ args.PerformerActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(CoolDown));
+
+ if (CastMessage != null)
+ caster.PopupMessageEveryone(CastMessage);
+
+ handsComponent.PutInHandOrDrop(itemComponent);
+
+ if (CastSound != null)
+ SoundSystem.Play(Filter.Pvs(caster), CastSound, caster);
+ }
+ }
+}
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/EntitySystems/GasTileOverlaySystem.cs b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs
index fd0101e5c4..4c9d0516b6 100644
--- a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs
+++ b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs
@@ -25,7 +25,7 @@ using Dependency = Robust.Shared.IoC.DependencyAttribute;
namespace Content.Server.Atmos.EntitySystems
{
[UsedImplicitly]
- internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem, IResettingEntitySystem
+ internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -67,6 +67,8 @@ namespace Content.Server.Atmos.EntitySystems
{
base.Initialize();
+ SubscribeLocalEvent(Reset);
+
_atmosphereSystem = Get();
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_mapManager.OnGridRemoved += OnGridRemoved;
@@ -479,7 +481,7 @@ namespace Content.Server.Atmos.EntitySystems
}
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
_invalidTiles.Clear();
_overlay.Clear();
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/Body/Surgery/BiologicalSurgeryDataComponent.cs b/Content.Server/Body/Surgery/BiologicalSurgeryDataComponent.cs
index b6db086dc7..5d932a611f 100644
--- a/Content.Server/Body/Surgery/BiologicalSurgeryDataComponent.cs
+++ b/Content.Server/Body/Surgery/BiologicalSurgeryDataComponent.cs
@@ -68,7 +68,7 @@ namespace Content.Server.Body.Surgery
BreakOnTargetMove = true
};
- return await doAfterSystem.DoAfter(args) == DoAfterStatus.Finished;
+ return await doAfterSystem.WaitDoAfter(args) == DoAfterStatus.Finished;
}
private bool HasIncisionNotClamped()
diff --git a/Content.Server/Body/Surgery/Components/SurgeryToolSystem.cs b/Content.Server/Body/Surgery/Components/SurgeryToolSystem.cs
index 36f3eb743c..b628e7f89c 100644
--- a/Content.Server/Body/Surgery/Components/SurgeryToolSystem.cs
+++ b/Content.Server/Body/Surgery/Components/SurgeryToolSystem.cs
@@ -10,7 +10,7 @@ using Robust.Shared.GameObjects;
namespace Content.Server.Body.Surgery.Components
{
[UsedImplicitly]
- public class SurgeryToolSystem : EntitySystem, IResettingEntitySystem
+ public class SurgeryToolSystem : EntitySystem
{
private readonly HashSet _openSurgeryUIs = new();
@@ -18,11 +18,12 @@ namespace Content.Server.Body.Surgery.Components
{
base.Initialize();
+ SubscribeLocalEvent(Reset);
SubscribeLocalEvent(OnSurgeryWindowOpen);
SubscribeLocalEvent(OnSurgeryWindowClose);
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
_openSurgeryUIs.Clear();
}
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/CargoConsoleSystem.cs b/Content.Server/Cargo/CargoConsoleSystem.cs
index db81e4629b..183bd723a1 100644
--- a/Content.Server/Cargo/CargoConsoleSystem.cs
+++ b/Content.Server/Cargo/CargoConsoleSystem.cs
@@ -7,7 +7,7 @@ using Robust.Shared.GameObjects;
namespace Content.Server.Cargo
{
- public class CargoConsoleSystem : EntitySystem, IResettingEntitySystem
+ public class CargoConsoleSystem : EntitySystem
{
///
/// How much time to wait (in seconds) before increasing bank accounts balance.
@@ -45,6 +45,8 @@ namespace Content.Server.Cargo
public override void Initialize()
{
+ SubscribeLocalEvent(Reset);
+
CreateBankAccount("Space Station 14", 1000);
CreateOrderDatabase(0);
}
@@ -64,7 +66,7 @@ namespace Content.Server.Cargo
}
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
_accountsDict.Clear();
_databasesDict.Clear();
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/Climbing/ClimbSystem.cs b/Content.Server/Climbing/ClimbSystem.cs
index 862795ddec..3a4a0517bb 100644
--- a/Content.Server/Climbing/ClimbSystem.cs
+++ b/Content.Server/Climbing/ClimbSystem.cs
@@ -8,10 +8,17 @@ using Robust.Shared.GameObjects;
namespace Content.Server.Climbing
{
[UsedImplicitly]
- internal sealed class ClimbSystem : EntitySystem, IResettingEntitySystem
+ internal sealed class ClimbSystem : EntitySystem
{
private readonly HashSet _activeClimbers = new();
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(Reset);
+ }
+
public void AddActiveClimber(ClimbingComponent climbingComponent)
{
_activeClimbers.Add(climbingComponent);
@@ -30,7 +37,7 @@ namespace Content.Server.Climbing
}
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
_activeClimbers.Clear();
}
diff --git a/Content.Server/Climbing/Components/ClimbableComponent.cs b/Content.Server/Climbing/Components/ClimbableComponent.cs
index 5b928cdf54..e8033f269d 100644
--- a/Content.Server/Climbing/Components/ClimbableComponent.cs
+++ b/Content.Server/Climbing/Components/ClimbableComponent.cs
@@ -157,7 +157,7 @@ namespace Content.Server.Climbing.Components
BreakOnStun = true
};
- var result = await EntitySystem.Get().DoAfter(doAfterEventArgs);
+ var result = await EntitySystem.Get().WaitDoAfter(doAfterEventArgs);
if (result != DoAfterStatus.Cancelled && entityToMove.TryGetComponent(out PhysicsComponent? body) && body.Fixtures.Count >= 1)
{
@@ -204,7 +204,7 @@ namespace Content.Server.Climbing.Components
BreakOnStun = true
};
- var result = await EntitySystem.Get().DoAfter(doAfterEventArgs);
+ var result = await EntitySystem.Get().WaitDoAfter(doAfterEventArgs);
if (result != DoAfterStatus.Cancelled && user.TryGetComponent(out PhysicsComponent? body) && body.Fixtures.Count >= 1)
{
diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs
index 774d00f585..4622ae660b 100644
--- a/Content.Server/Cloning/CloningSystem.cs
+++ b/Content.Server/Cloning/CloningSystem.cs
@@ -16,7 +16,7 @@ using static Content.Shared.Cloning.SharedCloningPodComponent;
namespace Content.Server.Cloning
{
- internal sealed class CloningSystem : EntitySystem, IResettingEntitySystem
+ internal sealed class CloningSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
public readonly Dictionary MindToId = new();
@@ -29,6 +29,7 @@ namespace Content.Server.Cloning
{
base.Initialize();
+ SubscribeLocalEvent(Reset);
SubscribeLocalEvent(HandleActivate);
SubscribeLocalEvent(HandleMindAdded);
}
@@ -73,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)
{
@@ -142,7 +143,7 @@ namespace Content.Server.Cloning
return IdToDNA.ToDictionary(m => m.Key, m => m.Value.Mind.CharacterName);
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
MindToId.Clear();
IdToDNA.Clear();
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/Components/ConstructionComponent.cs b/Content.Server/Construction/Components/ConstructionComponent.cs
index 8b86182e5a..a4de447e4c 100644
--- a/Content.Server/Construction/Components/ConstructionComponent.cs
+++ b/Content.Server/Construction/Components/ConstructionComponent.cs
@@ -258,7 +258,7 @@ namespace Content.Server.Construction.Components
{
case ArbitraryInsertConstructionGraphStep arbitraryStep:
if (arbitraryStep.EntityValid(eventArgs.Using)
- && await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
+ && await doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Finished)
{
valid = true;
}
@@ -267,7 +267,7 @@ namespace Content.Server.Construction.Components
case MaterialConstructionGraphStep materialStep:
if (materialStep.EntityValid(eventArgs.Using, out var stack)
- && await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
+ && await doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Finished)
{
var splitStack = EntitySystem.Get().Split(eventArgs.Using.Uid, stack, materialStep.Amount, eventArgs.User.Transform.Coordinates);
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/Construction/ConstructionSystem.cs b/Content.Server/Construction/ConstructionSystem.cs
index a102f51697..4b4bcaab3c 100644
--- a/Content.Server/Construction/ConstructionSystem.cs
+++ b/Content.Server/Construction/ConstructionSystem.cs
@@ -238,7 +238,7 @@ namespace Content.Server.Construction
NeedHand = false,
};
- if (await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Cancelled)
+ if (await doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Cancelled)
{
FailCleanup();
return null;
@@ -457,9 +457,16 @@ namespace Content.Server.Construction
return;
}
+ // We do this to be able to move the construction to its proper position in case it's anchored...
+ // Oh wow transform anchoring is amazing wow I love it!!!!
+ var wasAnchored = structure.Transform.Anchored;
+ structure.Transform.Anchored = false;
+
structure.Transform.Coordinates = ev.Location;
structure.Transform.LocalRotation = constructionPrototype.CanRotate ? ev.Angle : Angle.Zero;
+ structure.Transform.Anchored = wasAnchored;
+
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack));
Cleanup();
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/Cuffs/Components/CuffableComponent.cs b/Content.Server/Cuffs/Components/CuffableComponent.cs
index 7fc5d27503..4293558043 100644
--- a/Content.Server/Cuffs/Components/CuffableComponent.cs
+++ b/Content.Server/Cuffs/Components/CuffableComponent.cs
@@ -253,7 +253,7 @@ namespace Content.Server.Cuffs.Components
var doAfterSystem = EntitySystem.Get();
_uncuffing = true;
- var result = await doAfterSystem.DoAfter(doAfterEventArgs);
+ var result = await doAfterSystem.WaitDoAfter(doAfterEventArgs);
_uncuffing = false;
diff --git a/Content.Server/Cuffs/Components/HandcuffComponent.cs b/Content.Server/Cuffs/Components/HandcuffComponent.cs
index 66fd797321..bac2720916 100644
--- a/Content.Server/Cuffs/Components/HandcuffComponent.cs
+++ b/Content.Server/Cuffs/Components/HandcuffComponent.cs
@@ -214,7 +214,7 @@ namespace Content.Server.Cuffs.Components
_cuffing = true;
- var result = await EntitySystem.Get().DoAfter(doAfterEventArgs);
+ var result = await EntitySystem.Get().WaitDoAfter(doAfterEventArgs);
_cuffing = false;
diff --git a/Content.Server/Damage/GodmodeSystem.cs b/Content.Server/Damage/GodmodeSystem.cs
index b3ddb7dd39..788fe4bca7 100644
--- a/Content.Server/Damage/GodmodeSystem.cs
+++ b/Content.Server/Damage/GodmodeSystem.cs
@@ -12,11 +12,18 @@ using Robust.Shared.GameObjects;
namespace Content.Server.Damage
{
[UsedImplicitly]
- public class GodmodeSystem : EntitySystem, IResettingEntitySystem
+ public class GodmodeSystem : EntitySystem
{
private readonly Dictionary _entities = new();
- public void Reset()
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(Reset);
+ }
+
+ public void Reset(RoundRestartCleanupEvent ev)
{
_entities.Clear();
}
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/Dice/DiceComponent.cs b/Content.Server/Dice/DiceComponent.cs
index 94cb65945b..99bdf36eaa 100644
--- a/Content.Server/Dice/DiceComponent.cs
+++ b/Content.Server/Dice/DiceComponent.cs
@@ -1,6 +1,7 @@
using Content.Shared.Audio;
using Content.Shared.Examine;
using Content.Shared.Interaction;
+using Content.Shared.Sound;
using Content.Shared.Throwing;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
@@ -24,15 +25,16 @@ namespace Content.Server.Dice
public override string Name => "Dice";
- [DataField("step")]
- private int _step = 1;
private int _sides = 20;
- private int _currentSide = 20;
+
[ViewVariables]
- [DataField("diceSoundCollection")]
- public string _soundCollectionName = "dice";
+ [DataField("sound")]
+ private readonly SoundSpecifier _sound = new SoundCollectionSpecifier("Dice");
+
[ViewVariables]
- public int Step => _step;
+ [DataField("step")]
+ public int Step { get; } = 1;
+
[ViewVariables]
[DataField("sides")]
public int Sides
@@ -41,29 +43,28 @@ namespace Content.Server.Dice
set
{
_sides = value;
- _currentSide = value;
+ CurrentSide = value;
}
}
[ViewVariables]
- public int CurrentSide => _currentSide;
+ public int CurrentSide { get; private set; } = 20;
public void Roll()
{
- _currentSide = _random.Next(1, (_sides/_step)+1) * _step;
- if (!Owner.TryGetComponent(out SpriteComponent? sprite)) return;
- sprite.LayerSetState(0, $"d{_sides}{_currentSide}");
+ CurrentSide = _random.Next(1, (_sides/Step)+1) * Step;
+
PlayDiceEffect();
+
+ if (!Owner.TryGetComponent(out SpriteComponent? sprite))
+ return;
+
+ sprite.LayerSetState(0, $"d{_sides}{CurrentSide}");
}
public void PlayDiceEffect()
{
- if (!string.IsNullOrWhiteSpace(_soundCollectionName))
- {
- var soundCollection = _prototypeManager.Index(_soundCollectionName);
- var file = _random.Pick(soundCollection.PickFiles);
- SoundSystem.Play(Filter.Pvs(Owner), file, Owner, AudioParams.Default);
- }
+ SoundSystem.Play(Filter.Pvs(Owner), _sound.GetSound(), Owner, AudioParams.Default);
}
void IActivate.Activate(ActivateEventArgs eventArgs)
@@ -89,7 +90,7 @@ namespace Content.Server.Dice
("sidesAmount", _sides))
+ "\n" +
Loc.GetString("dice-component-on-examine-message-part-2",
- ("currentSide", _currentSide)));
+ ("currentSide", CurrentSide)));
}
}
}
diff --git a/Content.Server/Disposal/Mailing/DisposalMailingUnitComponent.cs b/Content.Server/Disposal/Mailing/DisposalMailingUnitComponent.cs
index af7225fd4a..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]
@@ -217,7 +217,7 @@ namespace Content.Server.Disposal.Mailing
NeedHand = false,
};
- var result = await doAfterSystem.DoAfter(doAfterArgs);
+ var result = await doAfterSystem.WaitDoAfter(doAfterArgs);
if (result == DoAfterStatus.Cancelled)
return false;
@@ -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 25075c8be7..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]
@@ -196,7 +196,7 @@ namespace Content.Server.Disposal.Unit.Components
NeedHand = false,
};
- var result = await doAfterSystem.DoAfter(doAfterArgs);
+ var result = await doAfterSystem.WaitDoAfter(doAfterArgs);
if (result == DoAfterStatus.Cancelled)
return false;
@@ -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/DoAfter/DoAfter.cs b/Content.Server/DoAfter/DoAfter.cs
index 3c41363818..4b929a060b 100644
--- a/Content.Server/DoAfter/DoAfter.cs
+++ b/Content.Server/DoAfter/DoAfter.cs
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Stunnable.Components;
+using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Timing;
@@ -16,7 +17,7 @@ namespace Content.Server.DoAfter
private TaskCompletionSource Tcs { get; }
- public DoAfterEventArgs EventArgs;
+ public readonly DoAfterEventArgs EventArgs;
public TimeSpan StartTime { get; }
diff --git a/Content.Server/DoAfter/DoAfterEventArgs.cs b/Content.Server/DoAfter/DoAfterEventArgs.cs
index b7ec522f5c..c4291fdc9c 100644
--- a/Content.Server/DoAfter/DoAfterEventArgs.cs
+++ b/Content.Server/DoAfter/DoAfterEventArgs.cs
@@ -81,6 +81,36 @@ namespace Content.Server.DoAfter
///
public Func? ExtraCheck { get; set; }
+ ///
+ /// Event to be raised directed to the entity when the DoAfter is cancelled.
+ ///
+ public EntityEventArgs? UserCancelledEvent { get; set; }
+
+ ///
+ /// Event to be raised directed to the entity when the DoAfter is finished successfully.
+ ///
+ public EntityEventArgs? UserFinishedEvent { get; set; }
+
+ ///
+ /// Event to be raised directed to the entity when the DoAfter is cancelled.
+ ///
+ public EntityEventArgs? TargetCancelledEvent { get; set; }
+
+ ///
+ /// Event to be raised directed to the entity when the DoAfter is finished successfully.
+ ///
+ public EntityEventArgs? TargetFinishedEvent { get; set; }
+
+ ///
+ /// Event to be broadcast when the DoAfter is cancelled.
+ ///
+ public object? BroadcastCancelledEvent { get; set; }
+
+ ///
+ /// Event to be broadcast when the DoAfter is finished successfully.
+ ///
+ public object? BroadcastFinishedEvent { get; set; }
+
public DoAfterEventArgs(
IEntity user,
float delay,
diff --git a/Content.Server/DoAfter/DoAfterSystem.cs b/Content.Server/DoAfter/DoAfterSystem.cs
index 8ee9372983..6934a93c61 100644
--- a/Content.Server/DoAfter/DoAfterSystem.cs
+++ b/Content.Server/DoAfter/DoAfterSystem.cs
@@ -11,15 +11,16 @@ namespace Content.Server.DoAfter
[UsedImplicitly]
public sealed class DoAfterSystem : EntitySystem
{
+ // We cache these lists as to not allocate them every update tick...
+ private readonly List _cancelled = new();
+ private readonly List _finished = new();
+
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var comp in ComponentManager.EntityQuery(true))
{
- var cancelled = new List(0);
- var finished = new List(0);
-
foreach (var doAfter in comp.DoAfters.ToArray())
{
doAfter.Run(frameTime);
@@ -29,27 +30,47 @@ namespace Content.Server.DoAfter
case DoAfterStatus.Running:
break;
case DoAfterStatus.Cancelled:
- cancelled.Add(doAfter);
+ _cancelled.Add(doAfter);
break;
case DoAfterStatus.Finished:
- finished.Add(doAfter);
+ _finished.Add(doAfter);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
- foreach (var doAfter in cancelled)
+ foreach (var doAfter in _cancelled)
{
comp.Cancelled(doAfter);
+
+ if(!doAfter.EventArgs.User.Deleted && doAfter.EventArgs.UserCancelledEvent != null)
+ RaiseLocalEvent(doAfter.EventArgs.User.Uid, doAfter.EventArgs.UserCancelledEvent, false);
+
+ if(doAfter.EventArgs.Target is { Deleted: false } && doAfter.EventArgs.TargetCancelledEvent != null)
+ RaiseLocalEvent(doAfter.EventArgs.Target.Uid, doAfter.EventArgs.TargetCancelledEvent, false);
+
+ if(doAfter.EventArgs.BroadcastCancelledEvent != null)
+ RaiseLocalEvent(doAfter.EventArgs.BroadcastCancelledEvent);
}
- foreach (var doAfter in finished)
+ foreach (var doAfter in _finished)
{
comp.Finished(doAfter);
+
+ if(!doAfter.EventArgs.User.Deleted && doAfter.EventArgs.UserFinishedEvent != null)
+ RaiseLocalEvent(doAfter.EventArgs.User.Uid, doAfter.EventArgs.UserFinishedEvent, false);
+
+ if(doAfter.EventArgs.Target is { Deleted: false } && doAfter.EventArgs.TargetFinishedEvent != null)
+ RaiseLocalEvent(doAfter.EventArgs.Target.Uid, doAfter.EventArgs.TargetFinishedEvent, false);
+
+ if(doAfter.EventArgs.BroadcastFinishedEvent != null)
+ RaiseLocalEvent(doAfter.EventArgs.BroadcastFinishedEvent);
}
- finished.Clear();
+ // Clean the shared lists at the end, ensuring they'll be clean for the next time we need them.
+ _cancelled.Clear();
+ _finished.Clear();
}
}
@@ -59,17 +80,33 @@ namespace Content.Server.DoAfter
///
///
///
- public async Task DoAfter(DoAfterEventArgs eventArgs)
+ public async Task WaitDoAfter(DoAfterEventArgs eventArgs)
+ {
+ var doAfter = CreateDoAfter(eventArgs);
+
+ await doAfter.AsTask;
+
+ return doAfter.Status;
+ }
+
+ ///
+ /// Creates a DoAfter without waiting for it to finish. You can use events with this.
+ /// These can be potentially cancelled by the user moving or when other things happen.
+ ///
+ ///
+ public void DoAfter(DoAfterEventArgs eventArgs)
+ {
+ CreateDoAfter(eventArgs);
+ }
+
+ private DoAfter CreateDoAfter(DoAfterEventArgs eventArgs)
{
// Setup
var doAfter = new DoAfter(eventArgs);
// Caller's gonna be responsible for this I guess
var doAfterComponent = eventArgs.User.GetComponent();
doAfterComponent.Add(doAfter);
-
- await doAfter.AsTask;
-
- return doAfter.Status;
+ return doAfter;
}
}
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/Engineering/EntitySystems/DisassembleOnActivateSystem.cs b/Content.Server/Engineering/EntitySystems/DisassembleOnActivateSystem.cs
index b123077e94..7dedae6d28 100644
--- a/Content.Server/Engineering/EntitySystems/DisassembleOnActivateSystem.cs
+++ b/Content.Server/Engineering/EntitySystems/DisassembleOnActivateSystem.cs
@@ -33,7 +33,7 @@ namespace Content.Server.Engineering.EntitySystems
BreakOnUserMove = true,
BreakOnStun = true,
};
- var result = await doAfterSystem.DoAfter(doAfterArgs);
+ var result = await doAfterSystem.WaitDoAfter(doAfterArgs);
if (result != DoAfterStatus.Finished)
return;
diff --git a/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs b/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs
index 1ccb603416..ef71b16e43 100644
--- a/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs
+++ b/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs
@@ -50,7 +50,7 @@ namespace Content.Server.Engineering.EntitySystems
BreakOnStun = true,
PostCheck = IsTileClear,
};
- var result = await doAfterSystem.DoAfter(doAfterArgs);
+ var result = await doAfterSystem.WaitDoAfter(doAfterArgs);
if (result != DoAfterStatus.Finished)
return;
diff --git a/Content.Server/Fluids/Components/BucketComponent.cs b/Content.Server/Fluids/Components/BucketComponent.cs
index a205e507e3..2774549cb8 100644
--- a/Content.Server/Fluids/Components/BucketComponent.cs
+++ b/Content.Server/Fluids/Components/BucketComponent.cs
@@ -82,7 +82,7 @@ namespace Content.Server.Fluids.Components
BreakOnStun = true,
BreakOnDamage = true,
};
- var result = await EntitySystem.Get().DoAfter(doAfterArgs);
+ var result = await EntitySystem.Get().WaitDoAfter(doAfterArgs);
_currentlyUsing.Remove(eventArgs.Using.Uid);
diff --git a/Content.Server/Fluids/Components/MopComponent.cs b/Content.Server/Fluids/Components/MopComponent.cs
index 864358d321..fa0c7ec11e 100644
--- a/Content.Server/Fluids/Components/MopComponent.cs
+++ b/Content.Server/Fluids/Components/MopComponent.cs
@@ -123,7 +123,7 @@ namespace Content.Server.Fluids.Components
BreakOnStun = true,
BreakOnDamage = true,
};
- var result = await EntitySystem.Get().DoAfter(doAfterArgs);
+ var result = await EntitySystem.Get().WaitDoAfter(doAfterArgs);
Mopping = false;
diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
index 0d6df65844..e47c36c58a 100644
--- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs
+++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
@@ -11,6 +11,7 @@ using Robust.Server.Player;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
+using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -296,13 +297,12 @@ namespace Content.Server.GameTicking
_gameRules.Clear();
- foreach (var system in _entitySystemManager.AllSystems)
- {
- if (system is IResettingEntitySystem resetting)
- {
- resetting.Reset();
- }
- }
+ // Round restart cleanup event, so entity systems can reset.
+ var ev = new RoundRestartCleanupEvent();
+ RaiseLocalEvent(ev);
+
+ // So clients' entity systems can clean up too...
+ RaiseNetworkEvent(ev, Filter.Broadcast());
_spawnedPositions.Clear();
_manifest.Clear();
diff --git a/Content.Server/Ghost/Roles/GhostRoleSystem.cs b/Content.Server/Ghost/Roles/GhostRoleSystem.cs
index fb878ae83b..38352f7d86 100644
--- a/Content.Server/Ghost/Roles/GhostRoleSystem.cs
+++ b/Content.Server/Ghost/Roles/GhostRoleSystem.cs
@@ -17,7 +17,7 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Ghost.Roles
{
[UsedImplicitly]
- public class GhostRoleSystem : EntitySystem, IResettingEntitySystem
+ public class GhostRoleSystem : EntitySystem
{
[Dependency] private readonly EuiManager _euiManager = default!;
@@ -33,6 +33,7 @@ namespace Content.Server.Ghost.Roles
{
base.Initialize();
+ SubscribeLocalEvent(Reset);
SubscribeLocalEvent(OnPlayerAttached);
}
@@ -137,7 +138,7 @@ namespace Content.Server.Ghost.Roles
CloseEui(message.Player);
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
foreach (var session in _openUis.Keys)
{
diff --git a/Content.Server/Gravity/EntitySystems/WeightlessSystem.cs b/Content.Server/Gravity/EntitySystems/WeightlessSystem.cs
index 54bd008b42..06e452aea0 100644
--- a/Content.Server/Gravity/EntitySystems/WeightlessSystem.cs
+++ b/Content.Server/Gravity/EntitySystems/WeightlessSystem.cs
@@ -12,7 +12,7 @@ using Robust.Shared.Utility;
namespace Content.Server.Gravity.EntitySystems
{
[UsedImplicitly]
- public class WeightlessSystem : EntitySystem, IResettingEntitySystem
+ public class WeightlessSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
@@ -22,11 +22,12 @@ namespace Content.Server.Gravity.EntitySystems
{
base.Initialize();
+ SubscribeLocalEvent(Reset);
SubscribeLocalEvent(GravityChanged);
SubscribeLocalEvent(EntParentChanged);
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
_alerts.Clear();
}
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/Instruments/InstrumentComponent.cs b/Content.Server/Instruments/InstrumentComponent.cs
index 62749507e2..98fed4906d 100644
--- a/Content.Server/Instruments/InstrumentComponent.cs
+++ b/Content.Server/Instruments/InstrumentComponent.cs
@@ -292,26 +292,30 @@ namespace Content.Server.Instruments
void IActivate.Activate(ActivateEventArgs eventArgs)
{
- if (Handheld || !eventArgs.User.TryGetComponent(out ActorComponent? actor)) return;
+ if (Handheld)
+ return;
- if (InstrumentPlayer != null) return;
-
- InstrumentPlayer = actor.PlayerSession;
- OpenUserInterface(actor.PlayerSession);
+ InteractInstrument(eventArgs.User);
}
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
- if (!eventArgs.User.TryGetComponent(out ActorComponent? actor)) return false;
-
- if (InstrumentPlayer == actor.PlayerSession)
- {
- OpenUserInterface(actor.PlayerSession);
- }
-
+ InteractInstrument(eventArgs.User);
return false;
}
+ private void InteractInstrument(IEntity user)
+ {
+ if (!user.TryGetComponent(out ActorComponent? actor)) return;
+
+ if (InstrumentPlayer != null || !EntitySystem.Get().CanInteract(user)) return;
+
+ InstrumentPlayer = actor.PlayerSession;
+ OpenUserInterface(InstrumentPlayer);
+
+ return;
+ }
+
private void UserInterfaceOnClosed(IPlayerSession player)
{
if (Handheld || player != InstrumentPlayer) return;
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/KitchenSpikeComponent.cs b/Content.Server/Kitchen/Components/KitchenSpikeComponent.cs
index 4403a12c4f..8e5c27d0fd 100644
--- a/Content.Server/Kitchen/Components/KitchenSpikeComponent.cs
+++ b/Content.Server/Kitchen/Components/KitchenSpikeComponent.cs
@@ -133,7 +133,7 @@ namespace Content.Server.Kitchen.Components
_beingButchered.Add(victimUid);
- var result = await doAfterSystem.DoAfter(doAfterArgs);
+ var result = await doAfterSystem.WaitDoAfter(doAfterArgs);
_beingButchered.Remove(victimUid);
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/MobState/States/MobStateManager.cs b/Content.Server/MobState/States/MobStateManager.cs
index 3e607b46e2..ac15fba39a 100644
--- a/Content.Server/MobState/States/MobStateManager.cs
+++ b/Content.Server/MobState/States/MobStateManager.cs
@@ -1,4 +1,5 @@
using Content.Shared.MobState;
+using Content.Shared.MobState.Components;
using Content.Shared.MobState.State;
using Robust.Shared.GameObjects;
diff --git a/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs b/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs
index 519ddee323..ba11446302 100644
--- a/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs
+++ b/Content.Server/NodeContainer/EntitySystems/NodeContainerSystem.cs
@@ -4,6 +4,10 @@ using Robust.Shared.GameObjects;
namespace Content.Server.NodeContainer.EntitySystems
{
+ ///
+ /// Manages events.
+ ///
+ ///
[UsedImplicitly]
public class NodeContainerSystem : EntitySystem
{
@@ -11,16 +15,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..36f4d2c459 100644
--- a/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs
+++ b/Content.Server/NodeContainer/EntitySystems/NodeGroupSystem.cs
@@ -1,30 +1,387 @@
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
{
+ ///
+ /// Entity system that manages and updating.
+ ///
+ ///
[UsedImplicitly]
public class NodeGroupSystem : EntitySystem
{
- private readonly HashSet _dirtyNodeGroups = new();
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IAdminManager _adminManager = default!;
+ [Dependency] private readonly INodeGroupFactory _nodeGroupFactory = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
- public void AddDirtyNodeGroup(INodeGroup nodeGroup)
+ 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..81edd4e643
--- /dev/null
+++ b/Content.Server/NodeContainer/NodeGroups/BaseNodeGroup.cs
@@ -0,0 +1,119 @@
+#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; }
+
+ ///
+ /// The list of nodes currently in this group.
+ ///
+ 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;
+
+ ///
+ /// The list of nodes in this group.
+ ///
+ [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; }
+
+ ///
+ /// Network ID of this group for client-side debug visualization of nodes.
+ ///
+ [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;
+ }
+
+ ///
+ /// Called when a node has been removed from this group via deletion of the node.
+ ///
+ ///
+ /// Note that this always still results in a complete remake of the group later,
+ /// but hooking this method is good for book keeping.
+ ///
+ /// The node that was deleted.
+ public virtual void RemoveNode(Node node)
+ {
+ }
+
+ ///
+ /// Called to load this newly created group up with new nodes.
+ ///
+ /// The new nodes for this group.
+ public virtual void LoadNodes(
+ List groupNodes)
+ {
+ Nodes.AddRange(groupNodes);
+ }
+
+ ///
+ /// Called after the nodes in this group have been made into one or more new groups.
+ ///
+ ///
+ /// Use this to split in-group data such as pipe gas mixtures into newly split nodes.
+ ///
+ /// A list of new groups for this group's former nodes.
+ 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..58519e5e23 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,20 @@ 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;
+ ///
+ /// The node group this node is a part of.
+ ///
+ [ViewVariables] public INodeGroup? NodeGroup;
- [ViewVariables]
- public IEntity Owner { get; private set; } = default!;
-
- [ViewVariables]
- private bool _needsGroup = true;
+ ///
+ /// The entity that owns this node via its .
+ ///
+ [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,137 +47,104 @@ namespace Content.Server.NodeContainer.Nodes
///
/// Prevents a node from being used by other nodes while midway through removal.
///
- private bool _deleting;
+ public bool Deleting;
+ ///
+ /// All compatible nodes that are reachable by this node.
+ /// Effectively, active connections out of this node.
+ ///
+ public readonly HashSet ReachableNodes = new();
+
+ internal int FloodGen;
+ internal int UndirectGen;
+ internal bool FlaggedForFlood;
+ internal int NetId;
+
+ ///
+ /// Name of this node on the owning .
+ ///
+ public string Name = default!;
+
+ ///
+ /// Invoked when the owning is initialized.
+ ///
+ /// The owning entity.
public virtual void Initialize(IEntity owner)
{
Owner = owner;
}
+ ///
+ /// Invoked when the owning is started.
+ ///
public virtual void OnContainerStartup()
{
- TryAssignGroupIfNeeded();
- CombineGroupWithReachable();
+ EntitySystem.Get().QueueReflood(this);
+ }
+
+ ///
+ /// Immediately create a single-node node group for this node if it does not have one yet.
+ ///
+ ///
+ /// This can be useful for nodes like pipes
+ /// that need immediate access to their node group to set parameters like node volume.
+ /// The node group created by this function (if necessary) will still update and form new,
+ /// merged groups later if necessary.
+ /// Set parameters like pipe net volume should then be transferred/merged there.
+ ///
+ public void CreateSingleNetImmediate()
+ {
+ EntitySystem.Get().CreateSingleNetImmediate(this);
}
public void AnchorUpdate()
{
if (Anchored)
{
- TryAssignGroupIfNeeded();
- CombineGroupWithReachable();
+ EntitySystem.Get().QueueReflood(this);
}
else
{
- RemoveSelfFromGroup();
+ EntitySystem.Get().QueueNodeRemove(this);
}
}
+ ///
+ /// Called when the anchored state of the owning entity changes.
+ ///
public virtual void AnchorStateChanged()
{
}
+ ///
+ /// Called after the parent node group has been rebuilt.
+ ///
+ public virtual void OnPostRebuild()
+ {
+
+ }
+
+ ///
+ /// Called when the owning is shut down.
+ ///
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();
- }
+ ///
+ ///
+ /// The set of nodes returned can be asymmetrical
+ /// (meaning that it can return other nodes whose does not return this node).
+ /// If this is used, creation of a new node may not correctly merge networks unless both sides
+ /// of this asymmetric relation are made to manually update with .
+ ///
+ ///
+ 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/Plants/PlantSystem.cs b/Content.Server/Plants/PlantSystem.cs
index 541a606e51..2dbe10b10e 100644
--- a/Content.Server/Plants/PlantSystem.cs
+++ b/Content.Server/Plants/PlantSystem.cs
@@ -10,7 +10,7 @@ using Robust.Shared.Prototypes;
namespace Content.Server.Plants
{
[UsedImplicitly]
- public class PlantSystem : EntitySystem, IResettingEntitySystem
+ public class PlantSystem : EntitySystem
{
[Dependency] private readonly IComponentManager _componentManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -26,6 +26,8 @@ namespace Content.Server.Plants
{
base.Initialize();
+ SubscribeLocalEvent(Reset);
+
PopulateDatabase();
}
@@ -73,7 +75,7 @@ namespace Content.Server.Plants
}
}
- public void Reset()
+ public void Reset(RoundRestartCleanupEvent ev)
{
PopulateDatabase();
}
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(ApcPowerReceiverShutdown);
+ SubscribeLocalEvent(ApcPowerReceiverPaused);
+ SubscribeLocalEvent(BatteryInit);
+ SubscribeLocalEvent(BatteryShutdown);
+ SubscribeLocalEvent(BatteryPaused);
+ SubscribeLocalEvent(PowerConsumerInit);
+ SubscribeLocalEvent(PowerConsumerShutdown);
+ SubscribeLocalEvent(PowerConsumerPaused);
+ SubscribeLocalEvent(PowerSupplierInit);
+ SubscribeLocalEvent(PowerSupplierShutdown);
+ SubscribeLocalEvent(PowerSupplierPaused);
+ }
+
+ private void ApcPowerReceiverInit(EntityUid uid, ApcPowerReceiverComponent component, ComponentInit args)
+ {
+ AllocLoad(component.NetworkLoad);
+ }
+
+ private void ApcPowerReceiverShutdown(EntityUid uid, ApcPowerReceiverComponent component,
+ ComponentShutdown args)
+ {
+ _powerState.Loads.Remove(component.NetworkLoad.Id);
+ }
+
+ private static void ApcPowerReceiverPaused(
+ EntityUid uid,
+ ApcPowerReceiverComponent component,
+ EntityPausedEvent args)
+ {
+ component.NetworkLoad.Paused = args.Paused;
+ }
+
+ private void BatteryInit(EntityUid uid, PowerNetworkBatteryComponent component, ComponentInit args)
+ {
+ AllocBattery(component.NetworkBattery);
+ }
+
+ private void BatteryShutdown(EntityUid uid, PowerNetworkBatteryComponent component, ComponentShutdown args)
+ {
+ _powerState.Batteries.Remove(component.NetworkBattery.Id);
+ }
+
+ private static void BatteryPaused(EntityUid uid, PowerNetworkBatteryComponent component, EntityPausedEvent args)
+ {
+ component.NetworkBattery.Paused = args.Paused;
+ }
+
+ private void PowerConsumerInit(EntityUid uid, PowerConsumerComponent component, ComponentInit args)
+ {
+ AllocLoad(component.NetworkLoad);
+ }
+
+ private void PowerConsumerShutdown(EntityUid uid, PowerConsumerComponent component, ComponentShutdown args)
+ {
+ _powerState.Loads.Remove(component.NetworkLoad.Id);
+ }
+
+ private static void PowerConsumerPaused(EntityUid uid, PowerConsumerComponent component, EntityPausedEvent args)
+ {
+ component.NetworkLoad.Paused = args.Paused;
+ }
+
+ private void PowerSupplierInit(EntityUid uid, PowerSupplierComponent component, ComponentInit args)
+ {
+ AllocSupply(component.NetworkSupply);
+ }
+
+ private void PowerSupplierShutdown(EntityUid uid, PowerSupplierComponent component, ComponentShutdown args)
+ {
+ _powerState.Supplies.Remove(component.NetworkSupply.Id);
+ }
+
+ private static void PowerSupplierPaused(EntityUid uid, PowerSupplierComponent component, EntityPausedEvent args)
+ {
+ component.NetworkSupply.Paused = args.Paused;
+ }
+
+ public void InitPowerNet(PowerNet powerNet)
+ {
+ AllocNetwork(powerNet.NetworkNode);
+ }
+
+ public void DestroyPowerNet(PowerNet powerNet)
+ {
+ _powerState.Networks.Remove(powerNet.NetworkNode.Id);
+ }
+
+ public void QueueReconnectPowerNet(PowerNet powerNet)
+ {
+ _powerNetReconnectQueue.Add(powerNet);
+ }
+
+ public void InitApcNet(ApcNet apcNet)
+ {
+ AllocNetwork(apcNet.NetworkNode);
+ }
+
+ public void DestroyApcNet(ApcNet apcNet)
+ {
+ _powerState.Networks.Remove(apcNet.NetworkNode.Id);
+ }
+
+ public void QueueReconnectApcNet(ApcNet apcNet)
+ {
+ _apcNetReconnectQueue.Add(apcNet);
+ }
+
+ public PowerStatistics GetStatistics()
+ {
+ return new()
+ {
+ CountBatteries = _powerState.Batteries.Count,
+ CountLoads = _powerState.Loads.Count,
+ CountNetworks = _powerState.Networks.Count,
+ CountSupplies = _powerState.Supplies.Count
+ };
+ }
public override void Update(float frameTime)
{
base.Update(frameTime);
- _powerNetManager.Update(frameTime);
+
+ // Reconnect networks.
+ {
+ foreach (var apcNet in _apcNetReconnectQueue)
+ {
+ if (apcNet.Removed)
+ continue;
+
+ DoReconnectApcNet(apcNet);
+ }
+
+ _apcNetReconnectQueue.Clear();
+
+ foreach (var powerNet in _powerNetReconnectQueue)
+ {
+ if (powerNet.Removed)
+ continue;
+
+ DoReconnectPowerNet(powerNet);
+ }
+
+ _powerNetReconnectQueue.Clear();
+ }
+
+ // Synchronize batteries
+ foreach (var battery in ComponentManager.EntityQuery())
+ {
+ RaiseLocalEvent(battery.Owner.Uid, new NetworkBatteryPreSync());
+ }
+
+ // Run power solver.
+ _solver.Tick(frameTime, _powerState);
+
+ // Synchronize batteries, the other way around.
+ foreach (var battery in ComponentManager.EntityQuery())
+ {
+ RaiseLocalEvent(battery.Owner.Uid, new NetworkBatteryPostSync());
+ }
+
+ // Send events where necessary.
+ {
+ foreach (var apcReceiver in ComponentManager.EntityQuery())
+ {
+ var recv = apcReceiver.NetworkLoad.ReceivingPower;
+ ref var last = ref apcReceiver.LastPowerReceived;
+
+ if (!MathHelper.CloseTo(recv, last))
+ {
+ last = recv;
+ apcReceiver.ApcPowerChanged();
+ }
+ }
+
+ foreach (var consumer in ComponentManager.EntityQuery())
+ {
+ var newRecv = consumer.NetworkLoad.ReceivingPower;
+ ref var lastRecv = ref consumer.LastReceived;
+ if (!MathHelper.CloseTo(lastRecv, newRecv))
+ {
+ lastRecv = newRecv;
+ var msg = new PowerConsumerReceivedChanged(newRecv, consumer.DrawRate);
+ RaiseLocalEvent(consumer.Owner.Uid, msg);
+ }
+ }
+ }
+ }
+
+ private void AllocLoad(PowerState.Load load)
+ {
+ load.Id = AllocId();
+ _powerState.Loads.Add(load.Id, load);
+ }
+
+ private void AllocSupply(PowerState.Supply supply)
+ {
+ supply.Id = AllocId();
+ _powerState.Supplies.Add(supply.Id, supply);
+ }
+
+ private void AllocBattery(PowerState.Battery battery)
+ {
+ battery.Id = AllocId();
+ _powerState.Batteries.Add(battery.Id, battery);
+ }
+
+ private void AllocNetwork(PowerState.Network network)
+ {
+ network.Id = AllocId();
+ _powerState.Networks.Add(network.Id, network);
+ }
+
+ private static void DoReconnectApcNet(ApcNet net)
+ {
+ var netNode = net.NetworkNode;
+
+ netNode.Loads.Clear();
+ netNode.BatteriesDischarging.Clear();
+ netNode.BatteriesCharging.Clear();
+ netNode.Supplies.Clear();
+
+ foreach (var provider in net.Providers)
+ {
+ foreach (var receiver in provider.LinkedReceivers)
+ {
+ netNode.Loads.Add(receiver.NetworkLoad.Id);
+ receiver.NetworkLoad.LinkedNetwork = netNode.Id;
+ }
+ }
+
+ foreach (var apc in net.Apcs)
+ {
+ var netBattery = apc.Owner.GetComponent();
+ netNode.BatteriesDischarging.Add(netBattery.NetworkBattery.Id);
+ netBattery.NetworkBattery.LinkedNetworkDischarging = netNode.Id;
+ }
+ }
+
+ private static void DoReconnectPowerNet(PowerNet net)
+ {
+ var netNode = net.NetworkNode;
+
+ netNode.Loads.Clear();
+ netNode.Supplies.Clear();
+ netNode.BatteriesCharging.Clear();
+ netNode.BatteriesDischarging.Clear();
+
+ foreach (var consumer in net.Consumers)
+ {
+ netNode.Loads.Add(consumer.NetworkLoad.Id);
+ consumer.NetworkLoad.LinkedNetwork = netNode.Id;
+ }
+
+ foreach (var supplier in net.Suppliers)
+ {
+ netNode.Supplies.Add(supplier.NetworkSupply.Id);
+ supplier.NetworkSupply.LinkedNetwork = netNode.Id;
+ }
+
+ foreach (var charger in net.Chargers)
+ {
+ var battery = charger.Owner.GetComponent();
+ netNode.BatteriesCharging.Add(battery.NetworkBattery.Id);
+ battery.NetworkBattery.LinkedNetworkCharging = netNode.Id;
+ }
+
+ foreach (var discharger in net.Dischargers)
+ {
+ var battery = discharger.Owner.GetComponent();
+ netNode.BatteriesDischarging.Add(battery.NetworkBattery.Id);
+ battery.NetworkBattery.LinkedNetworkDischarging = netNode.Id;
+ }
+ }
+
+ private PowerState.NodeId AllocId()
+ {
+ return new(_nextId++);
}
}
+
+ ///
+ /// Raised before power network simulation happens, to synchronize battery state from
+ /// components like into .
+ ///
+ public sealed class NetworkBatteryPreSync : EntityEventArgs
+ {
+ }
+
+ ///
+ /// Raised after power network simulation happens, to synchronize battery charge changes from
+ /// to components like .
+ ///
+ public sealed class NetworkBatteryPostSync : EntityEventArgs
+ {
+ }
+
+ ///
+ /// Raised when the amount of receiving power on a changes.
+ ///
+ public sealed class PowerConsumerReceivedChanged : EntityEventArgs
+ {
+ public float ReceivedPower { get; }
+ public float DrawRate { get; }
+
+ public PowerConsumerReceivedChanged(float receivedPower, float drawRate)
+ {
+ ReceivedPower = receivedPower;
+ DrawRate = drawRate;
+ }
+ }
+
+ public struct PowerStatistics
+ {
+ public int CountNetworks;
+ public int CountLoads;
+ public int CountSupplies;
+ public int CountBatteries;
+ }
}
diff --git a/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs b/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs
index 54ed7847de..119e66195c 100644
--- a/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs
+++ b/Content.Server/Power/EntitySystems/PowerReceiverSystem.cs
@@ -9,12 +9,12 @@ namespace Content.Server.Power.EntitySystems
{
base.Initialize();
- SubscribeLocalEvent(BodyTypeChanged);
+ SubscribeLocalEvent(BodyTypeChanged);
}
private static void BodyTypeChanged(
EntityUid uid,
- PowerReceiverComponent component,
+ ApcPowerReceiverComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.AnchorUpdate();
diff --git a/Content.Server/Power/NodeGroups/ApcNet.cs b/Content.Server/Power/NodeGroups/ApcNet.cs
new file mode 100644
index 0000000000..a2f77eedb0
--- /dev/null
+++ b/Content.Server/Power/NodeGroups/ApcNet.cs
@@ -0,0 +1,112 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.NodeContainer.Nodes;
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Server.Power.Pow3r;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Power.NodeGroups
+{
+ public interface IApcNet
+ {
+ void AddApc(ApcComponent apc);
+
+ void RemoveApc(ApcComponent apc);
+
+ void AddPowerProvider(ApcPowerProviderComponent provider);
+
+ void RemovePowerProvider(ApcPowerProviderComponent provider);
+
+ void QueueNetworkReconnect();
+
+ PowerState.Network NetworkNode { get; }
+
+ GridId? GridId { get; }
+ }
+
+ [NodeGroup(NodeGroupID.Apc)]
+ [UsedImplicitly]
+ public class ApcNet : BaseNetConnectorNodeGroup, IApcNet
+ {
+ private readonly PowerNetSystem _powerNetSystem = EntitySystem.Get();
+
+ [ViewVariables] public readonly List Apcs = new();
+
+ [ViewVariables] public readonly List Providers = new();
+
+ //Debug property
+ [ViewVariables] private int TotalReceivers => Providers.Sum(provider => provider.LinkedReceivers.Count);
+
+ [ViewVariables]
+ private IEnumerable AllReceivers =>
+ Providers.SelectMany(provider => provider.LinkedReceivers);
+
+ GridId? IApcNet.GridId => GridId;
+
+ [ViewVariables]
+ public PowerState.Network NetworkNode { get; } = new();
+
+ public override void Initialize(Node sourceNode)
+ {
+ base.Initialize(sourceNode);
+
+ _powerNetSystem.InitApcNet(this);
+ }
+
+ public override void AfterRemake(IEnumerable> newGroups)
+ {
+ base.AfterRemake(newGroups);
+
+ _powerNetSystem.DestroyApcNet(this);
+ }
+
+ public void AddApc(ApcComponent apc)
+ {
+ if (apc.Owner.TryGetComponent(out PowerNetworkBatteryComponent? netBattery))
+ netBattery.NetworkBattery.LinkedNetworkDischarging = default;
+
+ _powerNetSystem.QueueReconnectApcNet(this);
+ Apcs.Add(apc);
+ }
+
+ public void RemoveApc(ApcComponent apc)
+ {
+ if (apc.Owner.TryGetComponent(out PowerNetworkBatteryComponent? netBattery))
+ netBattery.NetworkBattery.LinkedNetworkDischarging = default;
+
+ _powerNetSystem.QueueReconnectApcNet(this);
+ Apcs.Remove(apc);
+ }
+
+ public void AddPowerProvider(ApcPowerProviderComponent provider)
+ {
+ Providers.Add(provider);
+
+ _powerNetSystem.QueueReconnectApcNet(this);
+ }
+
+ public void RemovePowerProvider(ApcPowerProviderComponent provider)
+ {
+ Providers.Remove(provider);
+
+ _powerNetSystem.QueueReconnectApcNet(this);
+ }
+
+ public void QueueNetworkReconnect()
+ {
+ _powerNetSystem.QueueReconnectApcNet(this);
+ }
+
+ protected override void SetNetConnectorNet(BaseApcNetComponent netConnectorComponent)
+ {
+ netConnectorComponent.Net = this;
+ }
+ }
+}
diff --git a/Content.Server/Power/NodeGroups/BaseNetConnectorNodeGroup.cs b/Content.Server/Power/NodeGroups/BaseNetConnectorNodeGroup.cs
new file mode 100644
index 0000000000..17785078ef
--- /dev/null
+++ b/Content.Server/Power/NodeGroups/BaseNetConnectorNodeGroup.cs
@@ -0,0 +1,34 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.NodeContainer.Nodes;
+using Content.Server.Power.Components;
+
+namespace Content.Server.Power.NodeGroups
+{
+ public abstract class BaseNetConnectorNodeGroup : BaseNodeGroup
+ where TNetConnector : BaseNetConnectorComponent
+ {
+ public override void LoadNodes(List groupNodes)
+ {
+ base.LoadNodes(groupNodes);
+
+ foreach (var node in groupNodes)
+ {
+ var newNetConnectorComponents = node.Owner
+ .GetAllComponents()
+ .Where(powerComp => (powerComp.NodeId == null || powerComp.NodeId == node.Name) &&
+ (NodeGroupID) powerComp.Voltage == node.NodeGroupID)
+ .ToList();
+
+ foreach (var netConnector in newNetConnectorComponents)
+ {
+ SetNetConnectorNet(netConnector);
+ }
+ }
+ }
+
+ protected abstract void SetNetConnectorNet(TNetConnector netConnectorComponent);
+ }
+}
diff --git a/Content.Server/Power/NodeGroups/PowerNet.cs b/Content.Server/Power/NodeGroups/PowerNet.cs
new file mode 100644
index 0000000000..38df0af231
--- /dev/null
+++ b/Content.Server/Power/NodeGroups/PowerNet.cs
@@ -0,0 +1,132 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.NodeContainer.Nodes;
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Server.Power.Pow3r;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Maths;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Power.NodeGroups
+{
+ public interface IPowerNet
+ {
+ void AddSupplier(PowerSupplierComponent supplier);
+
+ void RemoveSupplier(PowerSupplierComponent supplier);
+
+ void AddConsumer(PowerConsumerComponent consumer);
+
+ void RemoveConsumer(PowerConsumerComponent consumer);
+
+ void AddDischarger(BatteryDischargerComponent discharger);
+
+ void RemoveDischarger(BatteryDischargerComponent discharger);
+
+ void AddCharger(BatteryChargerComponent charger);
+
+ void RemoveCharger(BatteryChargerComponent charger);
+ }
+
+ [NodeGroup(NodeGroupID.HVPower, NodeGroupID.MVPower)]
+ [UsedImplicitly]
+ public class PowerNet : BaseNetConnectorNodeGroup, IPowerNet
+ {
+ private readonly PowerNetSystem _powerNetSystem = EntitySystem.Get();
+
+ [ViewVariables] public readonly List Suppliers = new();
+ [ViewVariables] public readonly List Consumers = new();
+ [ViewVariables] public readonly List Chargers = new();
+ [ViewVariables] public readonly List Dischargers = new();
+
+ [ViewVariables]
+ public PowerState.Network NetworkNode { get; } = new();
+
+ public override void Initialize(Node sourceNode)
+ {
+ base.Initialize(sourceNode);
+
+ _powerNetSystem.InitPowerNet(this);
+ }
+
+ public override void AfterRemake(IEnumerable> newGroups)
+ {
+ base.AfterRemake(newGroups);
+
+ _powerNetSystem.DestroyPowerNet(this);
+ }
+
+ protected override void SetNetConnectorNet(BasePowerNetComponent netConnectorComponent)
+ {
+ netConnectorComponent.Net = this;
+ }
+
+ public void AddSupplier(PowerSupplierComponent supplier)
+ {
+ supplier.NetworkSupply.LinkedNetwork = default;
+ Suppliers.Add(supplier);
+ _powerNetSystem.QueueReconnectPowerNet(this);
+ }
+
+ public void RemoveSupplier(PowerSupplierComponent supplier)
+ {
+ supplier.NetworkSupply.LinkedNetwork = default;
+ Suppliers.Remove(supplier);
+ _powerNetSystem.QueueReconnectPowerNet(this);
+ }
+
+ public void AddConsumer(PowerConsumerComponent consumer)
+ {
+ consumer.NetworkLoad.LinkedNetwork = default;
+ Consumers.Add(consumer);
+ _powerNetSystem.QueueReconnectPowerNet(this);
+ }
+
+ public void RemoveConsumer(PowerConsumerComponent consumer)
+ {
+ consumer.NetworkLoad.LinkedNetwork = default;
+ Consumers.Remove(consumer);
+ _powerNetSystem.QueueReconnectPowerNet(this);
+ }
+
+ public void AddDischarger(BatteryDischargerComponent discharger)
+ {
+ var battery = discharger.Owner.GetComponent();
+ battery.NetworkBattery.LinkedNetworkCharging = default;
+ Dischargers.Add(discharger);
+ _powerNetSystem.QueueReconnectPowerNet(this);
+ }
+
+ public void RemoveDischarger(BatteryDischargerComponent discharger)
+ {
+ // Can be missing if the entity is being deleted, not a big deal.
+ if (discharger.Owner.TryGetComponent(out PowerNetworkBatteryComponent? battery))
+ battery.NetworkBattery.LinkedNetworkCharging = default;
+
+ Dischargers.Remove(discharger);
+ _powerNetSystem.QueueReconnectPowerNet(this);
+ }
+
+ public void AddCharger(BatteryChargerComponent charger)
+ {
+ var battery = charger.Owner.GetComponent();
+ battery.NetworkBattery.LinkedNetworkCharging = default;
+ Chargers.Add(charger);
+ _powerNetSystem.QueueReconnectPowerNet(this);
+ }
+
+ public void RemoveCharger(BatteryChargerComponent charger)
+ {
+ // Can be missing if the entity is being deleted, not a big deal.
+ if (charger.Owner.TryGetComponent(out PowerNetworkBatteryComponent? battery))
+ battery.NetworkBattery.LinkedNetworkCharging = default;
+
+ Chargers.Remove(charger);
+ _powerNetSystem.QueueReconnectPowerNet(this);
+ }
+ }
+}
diff --git a/Content.Server/Power/Nodes/CableDeviceNode.cs b/Content.Server/Power/Nodes/CableDeviceNode.cs
new file mode 100644
index 0000000000..48a7942728
--- /dev/null
+++ b/Content.Server/Power/Nodes/CableDeviceNode.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Content.Server.NodeContainer.Nodes;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Power.Nodes
+{
+ ///
+ /// Type of node that connects to a below it.
+ ///
+ [DataDefinition]
+ public class CableDeviceNode : Node
+ {
+ public override IEnumerable GetReachableNodes()
+ {
+ var compMgr = IoCManager.Resolve();
+ var grid = IoCManager.Resolve().GetGrid(Owner.Transform.GridID);
+ var gridIndex = grid.TileIndicesFor(Owner.Transform.Coordinates);
+
+ foreach (var node in NodeHelpers.GetNodesInTile(compMgr, grid, gridIndex))
+ {
+ if (node is CableNode)
+ yield return node;
+ }
+ }
+ }
+}
diff --git a/Content.Server/Power/Nodes/CableNode.cs b/Content.Server/Power/Nodes/CableNode.cs
new file mode 100644
index 0000000000..07d519656d
--- /dev/null
+++ b/Content.Server/Power/Nodes/CableNode.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using Content.Server.NodeContainer.Nodes;
+using Content.Server.Power.EntitySystems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Power.Nodes
+{
+ [DataDefinition]
+ public class CableNode : Node
+ {
+ public override IEnumerable GetReachableNodes()
+ {
+ if (!Anchored)
+ yield break;
+
+ var compMgr = IoCManager.Resolve();
+ var grid = IoCManager.Resolve().GetGrid(Owner.Transform.GridID);
+ var gridIndex = grid.TileIndicesFor(Owner.Transform.Coordinates);
+
+ // While we go over adjacent nodes, we build a list of blocked directions due to
+ // incoming or outgoing wire terminals.
+ var terminalDirs = 0;
+ List<(Direction, Node)> nodeDirs = new();
+
+ foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(compMgr, grid, gridIndex))
+ {
+ if (node is CableNode && node != this)
+ {
+ nodeDirs.Add((dir, node));
+ }
+
+ if (node is CableDeviceNode && dir == Direction.Invalid)
+ {
+ // device on same tile
+ nodeDirs.Add((Direction.Invalid, node));
+ }
+
+ if (node is CableTerminalNode)
+ {
+ if (dir == Direction.Invalid)
+ {
+ // On own tile, block direction it faces
+ terminalDirs |= 1 << (int) node.Owner.Transform.LocalRotation.GetCardinalDir();
+ }
+ else
+ {
+ var terminalDir = node.Owner.Transform.LocalRotation.GetCardinalDir();
+ if (terminalDir.GetOpposite() == dir)
+ {
+ // Target tile has a terminal towards us, block the direction.
+ terminalDirs |= 1 << (int) dir;
+ break;
+ }
+ }
+ }
+ }
+
+ foreach (var (dir, node) in nodeDirs)
+ {
+ // If there is a wire terminal connecting across this direction, skip the node.
+ if (dir != Direction.Invalid && (terminalDirs & (1 << (int) dir)) != 0)
+ continue;
+
+ yield return node;
+ }
+ }
+
+ public override void OnPostRebuild()
+ {
+ base.OnPostRebuild();
+
+ EntitySystem.Get().QueueUpdate(Owner.Uid);
+ }
+ }
+}
diff --git a/Content.Server/Power/Nodes/CableTerminalNode.cs b/Content.Server/Power/Nodes/CableTerminalNode.cs
new file mode 100644
index 0000000000..7bcf0a323c
--- /dev/null
+++ b/Content.Server/Power/Nodes/CableTerminalNode.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using Content.Server.NodeContainer.Nodes;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Power.Nodes
+{
+ [DataDefinition]
+ public class CableTerminalNode : CableDeviceNode
+ {
+ public override IEnumerable GetReachableNodes()
+ {
+ var compMgr = IoCManager.Resolve();
+ var grid = IoCManager.Resolve().GetGrid(Owner.Transform.GridID);
+ var gridIndex = grid.TileIndicesFor(Owner.Transform.Coordinates);
+
+ var dir = Owner.Transform.LocalRotation.GetDir();
+ var targetIdx = gridIndex + NodeHelpers.TileOffsetForDir(dir);
+
+ foreach (var node in NodeHelpers.GetNodesInTile(compMgr, grid, targetIdx))
+ {
+ if (node is CableTerminalPortNode)
+ yield return node;
+ }
+
+ foreach (var node in base.GetReachableNodes())
+ {
+ yield return node;
+ }
+ }
+ }
+}
diff --git a/Content.Server/Power/Nodes/CableTerminalPortNode.cs b/Content.Server/Power/Nodes/CableTerminalPortNode.cs
new file mode 100644
index 0000000000..aa703aefd8
--- /dev/null
+++ b/Content.Server/Power/Nodes/CableTerminalPortNode.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using Content.Server.NodeContainer.Nodes;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Power.Nodes
+{
+ [DataDefinition]
+ public class CableTerminalPortNode : Node
+ {
+ public override IEnumerable GetReachableNodes()
+ {
+ var compMgr = IoCManager.Resolve();
+ var grid = IoCManager.Resolve().GetGrid(Owner.Transform.GridID);
+ var gridIndex = grid.TileIndicesFor(Owner.Transform.Coordinates);
+
+ var nodes = NodeHelpers.GetCardinalNeighborNodes(compMgr, grid, gridIndex, includeSameTile: false);
+ foreach (var (_, node) in nodes)
+ {
+ if (node is CableTerminalNode)
+ yield return node;
+ }
+ }
+ }
+}
diff --git a/Content.Server/Power/Pow3r/BatteryRampPegSolver.cs b/Content.Server/Power/Pow3r/BatteryRampPegSolver.cs
new file mode 100644
index 0000000000..591e4add55
--- /dev/null
+++ b/Content.Server/Power/Pow3r/BatteryRampPegSolver.cs
@@ -0,0 +1,305 @@
+using System;
+using System.Collections.Generic;
+using Robust.Shared.Utility;
+using static Content.Server.Power.Pow3r.PowerState;
+
+namespace Content.Server.Power.Pow3r
+{
+ public sealed class BatteryRampPegSolver : IPowerSolver
+ {
+ private sealed class HeightComparer : IComparer
+ {
+ public static HeightComparer Instance { get; } = new();
+
+ public int Compare(Network? x, Network? y)
+ {
+ if (ReferenceEquals(x, y)) return 0;
+ if (ReferenceEquals(null, y)) return 1;
+ if (ReferenceEquals(null, x)) return -1;
+ return x.Height.CompareTo(y.Height);
+ }
+ }
+
+ private Network[] _sortBuffer = Array.Empty();
+
+ public void Tick(float frameTime, PowerState state)
+ {
+ // Clear loads and supplies.
+ foreach (var load in state.Loads.Values)
+ {
+ if (load.Paused)
+ continue;
+
+ load.ReceivingPower = 0;
+ }
+
+ foreach (var supply in state.Supplies.Values)
+ {
+ if (supply.Paused)
+ continue;
+
+ supply.CurrentSupply = 0;
+ supply.SupplyRampTarget = 0;
+ }
+
+ // Run a pass to estimate network tree graph height.
+ // This is so that we can run networks before their children,
+ // to avoid draining batteries for a tick if their passing-supply gets cut off.
+ // It's not a big loss if this doesn't work (it won't, in some scenarios), but it's a nice-to-have.
+ foreach (var network in state.Networks.Values)
+ {
+ network.HeightTouched = false;
+ network.Height = -1;
+ }
+
+ foreach (var network in state.Networks.Values)
+ {
+ if (network.BatteriesDischarging.Count != 0)
+ continue;
+
+ EstimateNetworkDepth(state, network);
+ }
+
+ if (_sortBuffer.Length != state.Networks.Count)
+ _sortBuffer = new Network[state.Networks.Count];
+
+ var i = 0;
+ foreach (var network in state.Networks.Values)
+ {
+ _sortBuffer[i++] = network;
+ }
+
+ Array.Sort(_sortBuffer, HeightComparer.Instance);
+
+ // Go over every network.
+ foreach (var network in _sortBuffer)
+ {
+ // Add up demand in network.
+ var demand = 0f;
+ foreach (var loadId in network.Loads)
+ {
+ var load = state.Loads[loadId];
+
+ if (!load.Enabled || load.Paused)
+ continue;
+
+ DebugTools.Assert(load.DesiredPower >= 0);
+ demand += load.DesiredPower;
+ }
+
+ // TODO: Consider having battery charge loads be processed "after" pass-through loads.
+ // This would mean that charge rate would have no impact on throughput rate like it does currently.
+ // Would require a second pass over the network, or something. Not sure.
+
+ // Loading batteries.
+ foreach (var batteryId in network.BatteriesCharging)
+ {
+ var battery = state.Batteries[batteryId];
+ if (!battery.Enabled || !battery.CanCharge || battery.Paused)
+ continue;
+
+ var batterySpace = (battery.Capacity - battery.CurrentStorage) * (1 / battery.Efficiency);
+ batterySpace = Math.Max(0, batterySpace);
+ var scaledSpace = batterySpace / frameTime;
+
+ var chargeRate = battery.MaxChargeRate + battery.LoadingNetworkDemand / battery.Efficiency;
+
+ var batDemand = Math.Min(chargeRate, scaledSpace);
+
+ DebugTools.Assert(batDemand >= 0);
+
+ battery.DesiredPower = batDemand;
+ demand += batDemand;
+ }
+
+ DebugTools.Assert(demand >= 0);
+
+ // Add up supply in network.
+ var availableSupplySum = 0f;
+ var maxSupplySum = 0f;
+ foreach (var supplyId in network.Supplies)
+ {
+ var supply = state.Supplies[supplyId];
+ if (!supply.Enabled || supply.Paused)
+ continue;
+
+ var rampMax = supply.SupplyRampPosition + supply.SupplyRampTolerance;
+ var effectiveSupply = Math.Min(rampMax, supply.MaxSupply);
+
+ DebugTools.Assert(effectiveSupply >= 0);
+ DebugTools.Assert(supply.MaxSupply >= 0);
+
+ supply.EffectiveMaxSupply = effectiveSupply;
+ availableSupplySum += effectiveSupply;
+ maxSupplySum += supply.MaxSupply;
+ }
+
+ var unmet = Math.Max(0, demand - availableSupplySum);
+
+ DebugTools.Assert(availableSupplySum >= 0);
+ DebugTools.Assert(maxSupplySum >= 0);
+
+ // Supplying batteries.
+ // Batteries need to go after local supplies so that local supplies are prioritized.
+ // Also, it makes demand-pulling of batteries
+ // Because all batteries will will desire the unmet demand of their loading network,
+ // there will be a "rush" of input current when a network powers on,
+ // before power stabilizes in the network.
+ // This is fine.
+ foreach (var batteryId in network.BatteriesDischarging)
+ {
+ var battery = state.Batteries[batteryId];
+ if (!battery.Enabled || !battery.CanDischarge || battery.Paused)
+ continue;
+
+ var scaledSpace = battery.CurrentStorage / frameTime;
+ var supplyCap = Math.Min(battery.MaxSupply,
+ battery.SupplyRampPosition + battery.SupplyRampTolerance);
+ var supplyAndPassthrough = supplyCap + battery.CurrentReceiving * battery.Efficiency;
+ var tempSupply = Math.Min(scaledSpace, supplyAndPassthrough);
+ // Clamp final supply to the unmet demand, so that batteries refrain from taking power away from supplies.
+ var clampedSupply = Math.Min(unmet, tempSupply);
+
+ DebugTools.Assert(clampedSupply >= 0);
+
+ battery.TempMaxSupply = clampedSupply;
+ availableSupplySum += clampedSupply;
+ // TODO: Calculate this properly.
+ maxSupplySum += clampedSupply;
+
+ battery.LoadingNetworkDemand = unmet;
+ battery.LoadingDemandMarked = true;
+ }
+
+ var met = Math.Min(demand, availableSupplySum);
+
+ if (met != 0)
+ {
+ // Distribute supply to loads.
+ foreach (var loadId in network.Loads)
+ {
+ var load = state.Loads[loadId];
+ if (!load.Enabled || load.DesiredPower == 0 || load.Paused)
+ continue;
+
+ var ratio = load.DesiredPower / demand;
+ load.ReceivingPower = ratio * met;
+ }
+
+ // Loading batteries
+ foreach (var batteryId in network.BatteriesCharging)
+ {
+ var battery = state.Batteries[batteryId];
+
+ if (!battery.Enabled || battery.DesiredPower == 0 || battery.Paused)
+ continue;
+
+ var ratio = battery.DesiredPower / demand;
+ battery.CurrentReceiving = ratio * met;
+ var receivedPower = frameTime * battery.CurrentReceiving;
+ receivedPower *= battery.Efficiency;
+ battery.CurrentStorage = Math.Min(
+ battery.Capacity,
+ battery.CurrentStorage + receivedPower);
+ battery.LoadingMarked = true;
+ }
+
+ // Load to supplies
+ foreach (var supplyId in network.Supplies)
+ {
+ var supply = state.Supplies[supplyId];
+ if (!supply.Enabled || supply.EffectiveMaxSupply == 0 || supply.Paused)
+ continue;
+
+ var ratio = supply.EffectiveMaxSupply / availableSupplySum;
+ supply.CurrentSupply = ratio * met;
+
+ if (supply.MaxSupply != 0)
+ {
+ var maxSupplyRatio = supply.MaxSupply / maxSupplySum;
+
+ supply.SupplyRampTarget = maxSupplyRatio * demand;
+ }
+ else
+ {
+ supply.SupplyRampTarget = 0;
+ }
+ }
+
+ // Supplying batteries
+ foreach (var batteryId in network.BatteriesDischarging)
+ {
+ var battery = state.Batteries[batteryId];
+ if (!battery.Enabled || battery.TempMaxSupply == 0 || battery.Paused)
+ continue;
+
+ var ratio = battery.TempMaxSupply / availableSupplySum;
+ battery.CurrentSupply = ratio * met;
+
+ battery.CurrentStorage = Math.Max(
+ 0,
+ battery.CurrentStorage - frameTime * battery.CurrentSupply);
+
+ battery.SupplyRampTarget = battery.CurrentSupply - battery.CurrentReceiving * battery.Efficiency;
+
+ /*var maxSupplyRatio = supply.MaxSupply / maxSupplySum;
+
+ supply.SupplyRampTarget = maxSupplyRatio * demand;*/
+ battery.SupplyingMarked = true;
+ }
+ }
+ }
+
+ // Clear supplying/loading on any batteries that haven't been marked by usage.
+ // Because we need this data while processing ramp-pegging, we can't clear it at the start.
+ foreach (var battery in state.Batteries.Values)
+ {
+ if (battery.Paused)
+ continue;
+
+ if (!battery.SupplyingMarked)
+ battery.CurrentSupply = 0;
+
+ if (!battery.LoadingMarked)
+ battery.CurrentReceiving = 0;
+
+ if (!battery.LoadingDemandMarked)
+ battery.LoadingNetworkDemand = 0;
+
+ battery.SupplyingMarked = false;
+ battery.LoadingMarked = false;
+ battery.LoadingDemandMarked = false;
+ }
+
+ PowerSolverShared.UpdateRampPositions(frameTime, state);
+ }
+
+ private static void EstimateNetworkDepth(PowerState state, Network network)
+ {
+ network.HeightTouched = true;
+
+ if (network.BatteriesCharging.Count == 0)
+ {
+ network.Height = 1;
+ return;
+ }
+
+ var max = 0;
+ foreach (var batteryId in network.BatteriesCharging)
+ {
+ var battery = state.Batteries[batteryId];
+
+ if (battery.LinkedNetworkDischarging == default)
+ continue;
+
+ var subNet = state.Networks[battery.LinkedNetworkDischarging];
+ if (!subNet.HeightTouched)
+ EstimateNetworkDepth(state, subNet);
+
+ max = Math.Max(subNet.Height, max);
+ }
+
+ network.Height = 1 + max;
+ }
+ }
+}
diff --git a/Content.Server/Power/Pow3r/GraphWalkSolver.cs b/Content.Server/Power/Pow3r/GraphWalkSolver.cs
new file mode 100644
index 0000000000..981aad8120
--- /dev/null
+++ b/Content.Server/Power/Pow3r/GraphWalkSolver.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using static Content.Server.Power.Pow3r.PowerState;
+
+namespace Content.Server.Power.Pow3r
+{
+ ///
+ /// Partial implementation of full-graph-walking power solving under pow3r.
+ /// Concept described at https://hackmd.io/@ss14/lowpower
+ ///
+ ///
+ /// Many features like batteries, cycle detection, join handling, etc... are not implemented at all.
+ /// Seriously, this implementation barely works. Ah well.
+ /// is better.
+ ///
+ public class GraphWalkSolver : IPowerSolver
+ {
+ public void Tick(float frameTime, PowerState state)
+ {
+ foreach (var load in state.Loads.Values)
+ {
+ load.ReceivingPower = 0;
+ }
+
+ foreach (var supply in state.Supplies.Values)
+ {
+ supply.CurrentSupply = 0;
+ }
+
+ foreach (var network in state.Networks.Values)
+ {
+ // Clear some stuff.
+ network.LocalDemandMet = 0;
+
+ // Add up demands in network.
+ network.LocalDemandTotal = network.Loads
+ .Select(l => state.Loads[l])
+ .Where(c => c.Enabled)
+ .Sum(c => c.DesiredPower);
+
+ // Add up supplies in network.
+ var availableSupplySum = 0f;
+ var maxSupplySum = 0f;
+ foreach (var supplyId in network.Supplies)
+ {
+ var supply = state.Supplies[supplyId];
+ if (!supply.Enabled)
+ continue;
+
+ var rampMax = supply.SupplyRampPosition + supply.SupplyRampTolerance;
+ var effectiveSupply = Math.Min(rampMax, supply.MaxSupply);
+ supply.EffectiveMaxSupply = effectiveSupply;
+ availableSupplySum += effectiveSupply;
+ maxSupplySum += supply.MaxSupply;
+ }
+
+ network.AvailableSupplyTotal = availableSupplySum;
+ network.TheoreticalSupplyTotal = maxSupplySum;
+ }
+
+ // Sort networks by tree height so that suppliers that have less possible loads go FIRST.
+ // Idea being that a backup generator on a small subnet should do more work
+ // so that a larger generator that covers more networks can put its power elsewhere.
+ var sortedByHeight = state.Networks.Values.OrderBy(v => TotalSubLoadCount(state, v)).ToArray();
+
+ // Go over every network with supply to send power.
+ foreach (var network in sortedByHeight)
+ {
+ // Find all loads recursively, and sum them up.
+ var subNets = new List();
+ var totalDemand = 0f;
+ GetLoadingNetworksRecursively(state, network, subNets, ref totalDemand);
+
+ if (totalDemand == 0)
+ continue;
+
+ // Calculate power delivered.
+ var power = Math.Min(totalDemand, network.AvailableSupplyTotal);
+
+ // Distribute load across supplies in network.
+ foreach (var supplyId in network.Supplies)
+ {
+ var supply = state.Supplies[supplyId];
+ if (!supply.Enabled)
+ continue;
+
+ if (supply.EffectiveMaxSupply != 0)
+ {
+ var ratio = supply.EffectiveMaxSupply / network.AvailableSupplyTotal;
+
+ supply.CurrentSupply = ratio * power;
+ }
+ else
+ {
+ supply.CurrentSupply = 0;
+ }
+
+ if (supply.MaxSupply != 0)
+ {
+ var ratio = supply.MaxSupply / network.TheoreticalSupplyTotal;
+
+ supply.SupplyRampTarget = ratio * totalDemand;
+ }
+ else
+ {
+ supply.SupplyRampTarget = 0;
+ }
+ }
+
+ // Distribute supply across subnet loads.
+ foreach (var subNet in subNets)
+ {
+ var rem = subNet.RemainingDemand;
+ var ratio = rem / totalDemand;
+
+ subNet.LocalDemandMet += ratio * power;
+ }
+ }
+
+ // Distribute power across loads in networks.
+ foreach (var network in state.Networks.Values)
+ {
+ if (network.LocalDemandMet == 0)
+ continue;
+
+ foreach (var loadId in network.Loads)
+ {
+ var load = state.Loads[loadId];
+ if (!load.Enabled)
+ continue;
+
+ var ratio = load.DesiredPower / network.LocalDemandTotal;
+ load.ReceivingPower = ratio * network.LocalDemandMet;
+ }
+ }
+
+ PowerSolverShared.UpdateRampPositions(frameTime, state);
+ }
+
+ private int TotalSubLoadCount(PowerState state, Network network)
+ {
+ // TODO: Cycle detection.
+ var height = network.Loads.Count;
+
+ foreach (var batteryId in network.BatteriesCharging)
+ {
+ var battery = state.Batteries[batteryId];
+ if (battery.LinkedNetworkDischarging != default)
+ {
+ height += TotalSubLoadCount(state, state.Networks[battery.LinkedNetworkDischarging]);
+ }
+ }
+
+ return height;
+ }
+
+ private void GetLoadingNetworksRecursively(
+ PowerState state,
+ Network network,
+ List networks,
+ ref float totalDemand)
+ {
+ networks.Add(network);
+ totalDemand += network.LocalDemandTotal - network.LocalDemandMet;
+
+ foreach (var batteryId in network.BatteriesCharging)
+ {
+ var battery = state.Batteries[batteryId];
+ if (battery.LinkedNetworkDischarging != default)
+ {
+ GetLoadingNetworksRecursively(
+ state,
+ state.Networks[battery.LinkedNetworkDischarging],
+ networks,
+ ref totalDemand);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/Power/Pow3r/IPowerSolver.cs b/Content.Server/Power/Pow3r/IPowerSolver.cs
new file mode 100644
index 0000000000..af1fe2d035
--- /dev/null
+++ b/Content.Server/Power/Pow3r/IPowerSolver.cs
@@ -0,0 +1,7 @@
+namespace Content.Server.Power.Pow3r
+{
+ public interface IPowerSolver
+ {
+ void Tick(float frameTime, PowerState state);
+ }
+}
diff --git a/Content.Server/Power/Pow3r/NoOpSolver.cs b/Content.Server/Power/Pow3r/NoOpSolver.cs
new file mode 100644
index 0000000000..fa54a8b80d
--- /dev/null
+++ b/Content.Server/Power/Pow3r/NoOpSolver.cs
@@ -0,0 +1,10 @@
+namespace Content.Server.Power.Pow3r
+{
+ public sealed class NoOpSolver : IPowerSolver
+ {
+ public void Tick(float frameTime, PowerState state)
+ {
+ // Literally nothing.
+ }
+ }
+}
diff --git a/Content.Server/Power/Pow3r/PowerSolverShared.cs b/Content.Server/Power/Pow3r/PowerSolverShared.cs
new file mode 100644
index 0000000000..f9ef3cead5
--- /dev/null
+++ b/Content.Server/Power/Pow3r/PowerSolverShared.cs
@@ -0,0 +1,90 @@
+using System;
+
+namespace Content.Server.Power.Pow3r
+{
+ public static class PowerSolverShared
+ {
+ public static void UpdateRampPositions(float frameTime, PowerState state)
+ {
+ // Update supplies to move their ramp position towards target, if necessary.
+ foreach (var supply in state.Supplies.Values)
+ {
+ if (supply.Paused)
+ continue;
+
+ if (!supply.Enabled)
+ {
+ // If disabled, set ramp to 0.
+ supply.SupplyRampPosition = 0;
+ continue;
+ }
+
+ var rampDev = supply.SupplyRampTarget - supply.SupplyRampPosition;
+ if (Math.Abs(rampDev) > 0.001f)
+ {
+ float newPos;
+ if (rampDev > 0)
+ {
+ // Position below target, go up.
+ newPos = Math.Min(
+ supply.SupplyRampTarget,
+ supply.SupplyRampPosition + supply.SupplyRampRate * frameTime);
+ }
+ else
+ {
+ // Other way around, go down
+ newPos = Math.Max(
+ supply.SupplyRampTarget,
+ supply.SupplyRampPosition - supply.SupplyRampRate * frameTime);
+ }
+
+ supply.SupplyRampPosition = Math.Clamp(newPos, 0, supply.MaxSupply);
+ }
+ else
+ {
+ supply.SupplyRampPosition = supply.SupplyRampTarget;
+ }
+ }
+
+ // Batteries too.
+ foreach (var battery in state.Batteries.Values)
+ {
+ if (battery.Paused)
+ continue;
+
+ if (!battery.Enabled)
+ {
+ // If disabled, set ramp to 0.
+ battery.SupplyRampPosition = 0;
+ continue;
+ }
+
+ var rampDev = battery.SupplyRampTarget - battery.SupplyRampPosition;
+ if (Math.Abs(rampDev) > 0.001f)
+ {
+ float newPos;
+ if (rampDev > 0)
+ {
+ // Position below target, go up.
+ newPos = Math.Min(
+ battery.SupplyRampTarget,
+ battery.SupplyRampPosition + battery.SupplyRampRate * frameTime);
+ }
+ else
+ {
+ // Other way around, go down
+ newPos = Math.Max(
+ battery.SupplyRampTarget,
+ battery.SupplyRampPosition - battery.SupplyRampRate * frameTime);
+ }
+
+ battery.SupplyRampPosition = Math.Clamp(newPos, 0, battery.MaxSupply);
+ }
+ else
+ {
+ battery.SupplyRampPosition = battery.SupplyRampTarget;
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/Power/Pow3r/PowerState.cs b/Content.Server/Power/Pow3r/PowerState.cs
new file mode 100644
index 0000000000..276fc785a9
--- /dev/null
+++ b/Content.Server/Power/Pow3r/PowerState.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Power.Pow3r
+{
+ public sealed class PowerState
+ {
+ public const int MaxTickData = 180;
+
+ public static readonly JsonSerializerOptions SerializerOptions = new()
+ {
+ IncludeFields = true,
+ Converters = {new NodeIdJsonConverter()}
+ };
+
+ public Dictionary Supplies = new();
+ public Dictionary Networks = new();
+ public Dictionary Loads = new();
+ public Dictionary Batteries = new();
+
+ public readonly struct NodeId : IEquatable
+ {
+ public readonly int Id;
+
+ public NodeId(int id)
+ {
+ Id = id;
+ }
+
+ public bool Equals(NodeId other)
+ {
+ return Id == other.Id;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is NodeId other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return Id;
+ }
+
+ public static bool operator ==(NodeId left, NodeId right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(NodeId left, NodeId right)
+ {
+ return !left.Equals(right);
+ }
+
+ public override string ToString()
+ {
+ return Id.ToString();
+ }
+ }
+
+ public sealed class NodeIdJsonConverter : JsonConverter
+ {
+ public override NodeId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return new(reader.GetInt32());
+ }
+
+ public override void Write(Utf8JsonWriter writer, NodeId value, JsonSerializerOptions options)
+ {
+ writer.WriteNumberValue(value.Id);
+ }
+ }
+
+ public sealed class Supply
+ {
+ [ViewVariables] public NodeId Id;
+
+ // == Static parameters ==
+ [ViewVariables(VVAccess.ReadWrite)] public bool Enabled = true;
+ [ViewVariables(VVAccess.ReadWrite)] public bool Paused;
+ [ViewVariables(VVAccess.ReadWrite)] public float MaxSupply;
+
+ [ViewVariables(VVAccess.ReadWrite)] public float SupplyRampRate;
+ [ViewVariables(VVAccess.ReadWrite)] public float SupplyRampTolerance;
+
+ // == Runtime parameters ==
+
+ // Actual power supplied last network update.
+ [ViewVariables(VVAccess.ReadWrite)] public float CurrentSupply;
+
+ // The amount of power we WANT to be supplying to match grid load.
+ [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore]
+ public float SupplyRampTarget;
+
+ // Position of the supply ramp.
+ [ViewVariables(VVAccess.ReadWrite)] public float SupplyRampPosition;
+
+ [ViewVariables] [JsonIgnore] public NodeId LinkedNetwork;
+
+ // In-tick max supply thanks to ramp. Used during calculations.
+ [JsonIgnore] public float EffectiveMaxSupply;
+ }
+
+ public sealed class Load
+ {
+ [ViewVariables] public NodeId Id;
+
+ // == Static parameters ==
+ [ViewVariables(VVAccess.ReadWrite)] public bool Enabled = true;
+ [ViewVariables(VVAccess.ReadWrite)] public bool Paused;
+ [ViewVariables(VVAccess.ReadWrite)] public float DesiredPower;
+
+ // == Runtime parameters ==
+ [ViewVariables(VVAccess.ReadWrite)] public float ReceivingPower;
+
+ [ViewVariables] [JsonIgnore] public NodeId LinkedNetwork;
+ }
+
+ public sealed class Battery
+ {
+ [ViewVariables] public NodeId Id;
+
+ // == Static parameters ==
+ [ViewVariables(VVAccess.ReadWrite)] public bool Enabled = true;
+ [ViewVariables(VVAccess.ReadWrite)] public bool Paused;
+ [ViewVariables(VVAccess.ReadWrite)] public bool CanDischarge = true;
+ [ViewVariables(VVAccess.ReadWrite)] public bool CanCharge = true;
+ [ViewVariables(VVAccess.ReadWrite)] public float Capacity;
+ [ViewVariables(VVAccess.ReadWrite)] public float MaxChargeRate;
+ [ViewVariables(VVAccess.ReadWrite)] public float MaxThroughput; // 0 = infinite cuz imgui
+ [ViewVariables(VVAccess.ReadWrite)] public float MaxSupply;
+ [ViewVariables(VVAccess.ReadWrite)] public float SupplyRampTolerance;
+ [ViewVariables(VVAccess.ReadWrite)] public float SupplyRampRate;
+ [ViewVariables(VVAccess.ReadWrite)] public float Efficiency = 1;
+
+ // == Runtime parameters ==
+ [ViewVariables(VVAccess.ReadWrite)] public float SupplyRampPosition;
+ [ViewVariables(VVAccess.ReadWrite)] public float CurrentSupply;
+ [ViewVariables(VVAccess.ReadWrite)] public float CurrentStorage;
+ [ViewVariables(VVAccess.ReadWrite)] public float CurrentReceiving;
+ [ViewVariables(VVAccess.ReadWrite)] public float LoadingNetworkDemand;
+
+ [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore]
+ public bool SupplyingMarked;
+
+ [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore]
+ public bool LoadingMarked;
+
+ [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore]
+ public bool LoadingDemandMarked;
+
+ [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore]
+ public float TempMaxSupply;
+
+ [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore]
+ public float DesiredPower;
+
+ [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore]
+ public float SupplyRampTarget;
+
+ [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore]
+ public NodeId LinkedNetworkCharging;
+
+ [ViewVariables(VVAccess.ReadWrite)] [JsonIgnore]
+ public NodeId LinkedNetworkDischarging;
+ }
+
+ // Readonly breaks json serialization.
+ [SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
+ public sealed class Network
+ {
+ [ViewVariables] public NodeId Id;
+
+ [ViewVariables] public List Supplies = new();
+
+ [ViewVariables] public List Loads = new();
+
+ // "Loading" means the network is connected to the INPUT port of the battery.
+ [ViewVariables] public List BatteriesCharging = new();
+
+ // "Supplying" means the network is connected to the OUTPUT port of the battery.
+ [ViewVariables] public List BatteriesDischarging = new();
+
+ // Calculation parameters used by GraphWalkSolver.
+ // Unused by BatteryRampPegSolver.
+ [JsonIgnore] public float LocalDemandTotal;
+ [JsonIgnore] public float LocalDemandMet;
+ [JsonIgnore] public float GroupDemandTotal;
+ [JsonIgnore] public float GroupDemandMet;
+
+ [ViewVariables] [JsonIgnore] public int Height;
+ [JsonIgnore] public bool HeightTouched;
+
+ // Supply available this tick.
+ [JsonIgnore] public float AvailableSupplyTotal;
+
+ // Max theoretical supply assuming max ramp.
+ [JsonIgnore] public float TheoreticalSupplyTotal;
+ public float RemainingDemand => LocalDemandTotal - LocalDemandMet;
+ }
+ }
+}
diff --git a/Content.Server/SMES/PowerSmesSystem.cs b/Content.Server/Power/SMES/PowerSmesSystem.cs
similarity index 91%
rename from Content.Server/SMES/PowerSmesSystem.cs
rename to Content.Server/Power/SMES/PowerSmesSystem.cs
index 1bccbecbcf..bf66d9daa0 100644
--- a/Content.Server/SMES/PowerSmesSystem.cs
+++ b/Content.Server/Power/SMES/PowerSmesSystem.cs
@@ -2,7 +2,7 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
-namespace Content.Server.SMES
+namespace Content.Server.Power.SMES
{
[UsedImplicitly]
internal class PowerSmesSystem : EntitySystem
diff --git a/Content.Server/SMES/SmesComponent.cs b/Content.Server/Power/SMES/SmesComponent.cs
similarity index 81%
rename from Content.Server/SMES/SmesComponent.cs
rename to Content.Server/Power/SMES/SmesComponent.cs
index c511f5bf11..903d616011 100644
--- a/Content.Server/SMES/SmesComponent.cs
+++ b/Content.Server/Power/SMES/SmesComponent.cs
@@ -1,6 +1,5 @@
#nullable enable
using System;
-using Content.Server.Battery.Components;
using Content.Server.Power.Components;
using Content.Shared.Power;
using Content.Shared.Rounding;
@@ -10,7 +9,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
-namespace Content.Server.SMES
+namespace Content.Server.Power.SMES
{
///
/// Handles the "user-facing" side of the actual SMES object.
@@ -80,20 +79,13 @@ namespace Content.Server.SMES
private ChargeState GetNewChargeState()
{
- var supplier = Owner.GetComponent();
- var consumer = Owner.GetComponent();
- if (supplier.SupplyRate > 0 && consumer.DrawRate != consumer.ReceivedPower)
+ var battery = Owner.GetComponent();
+ return (battery.CurrentSupply - battery.CurrentReceiving) switch
{
- return ChargeState.Discharging;
- }
- else if (supplier.SupplyRate == 0 && consumer.DrawRate > 0)
- {
- return ChargeState.Charging;
- }
- else
- {
- return ChargeState.Still;
- }
+ > 0 => ChargeState.Discharging,
+ < 0 => ChargeState.Charging,
+ _ => ChargeState.Still
+ };
}
}
}
diff --git a/Content.Server/PowerCell/Components/PowerCellChargerComponent.cs b/Content.Server/PowerCell/Components/PowerCellChargerComponent.cs
index 8548ff020b..80cfa844ac 100644
--- a/Content.Server/PowerCell/Components/PowerCellChargerComponent.cs
+++ b/Content.Server/PowerCell/Components/PowerCellChargerComponent.cs
@@ -1,5 +1,4 @@
#nullable enable
-using Content.Server.Battery.Components;
using Content.Server.Power.Components;
using Content.Shared.Interaction;
using Robust.Shared.GameObjects;
diff --git a/Content.Server/PowerCell/Components/PowerCellComponent.cs b/Content.Server/PowerCell/Components/PowerCellComponent.cs
index 05b951913a..2ba23bf120 100644
--- a/Content.Server/PowerCell/Components/PowerCellComponent.cs
+++ b/Content.Server/PowerCell/Components/PowerCellComponent.cs
@@ -1,8 +1,8 @@
#nullable enable
using System;
-using Content.Server.Battery.Components;
using Content.Server.Chemistry.Components;
using Content.Server.Explosion;
+using Content.Server.Power.Components;
using Content.Shared.Chemistry;
using Content.Shared.Examine;
using Content.Shared.PowerCell;
diff --git a/Content.Server/RCD/Components/RCDComponent.cs b/Content.Server/RCD/Components/RCDComponent.cs
index aa344649b8..1f6f229023 100644
--- a/Content.Server/RCD/Components/RCDComponent.cs
+++ b/Content.Server/RCD/Components/RCDComponent.cs
@@ -119,7 +119,7 @@ namespace Content.Server.RCD.Components
ExtraCheck = () => IsRCDStillValid(eventArgs, mapGrid, tile, snapPos, startingMode) //All of the sanity checks are here
};
- var result = await _doAfterSystem.DoAfter(doAfterEventArgs);
+ var result = await _doAfterSystem.WaitDoAfter(doAfterEventArgs);
if (result == DoAfterStatus.Cancelled)
{
return true;
diff --git a/Content.Server/Recycling/Components/RecyclerComponent.cs b/Content.Server/Recycling/Components/RecyclerComponent.cs
index 1c1073b0a1..bf0b04b219 100644
--- a/Content.Server/Recycling/Components/RecyclerComponent.cs
+++ b/Content.Server/Recycling/Components/RecyclerComponent.cs
@@ -47,7 +47,7 @@ namespace Content.Server.Recycling.Components
private float _efficiency = 0.25f;
private bool Powered =>
- !Owner.TryGetComponent(out PowerReceiverComponent? receiver) ||
+ !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) ||
receiver.Powered;
private void Bloodstain()
@@ -97,7 +97,7 @@ namespace Content.Server.Recycling.Components
public bool CanRun()
{
- if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) &&
+ if (Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) &&
!receiver.Powered)
{
return false;
diff --git a/Content.Server/Research/Components/ResearchConsoleComponent.cs b/Content.Server/Research/Components/ResearchConsoleComponent.cs
index b6cdea4c1a..da1dea4ff9 100644
--- a/Content.Server/Research/Components/ResearchConsoleComponent.cs
+++ b/Content.Server/Research/Components/ResearchConsoleComponent.cs
@@ -26,7 +26,7 @@ namespace Content.Server.Research.Components
private const string SoundCollectionName = "keyboard";
- [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(ResearchConsoleUiKey.Key);
diff --git a/Content.Server/Research/Components/ResearchPointSourceComponent.cs b/Content.Server/Research/Components/ResearchPointSourceComponent.cs
index 4eaeeff0b8..39d95fe36d 100644
--- a/Content.Server/Research/Components/ResearchPointSourceComponent.cs
+++ b/Content.Server/Research/Components/ResearchPointSourceComponent.cs
@@ -16,7 +16,7 @@ namespace Content.Server.Research.Components
private int _pointsPerSecond;
[DataField("active")]
private bool _active;
- private PowerReceiverComponent? _powerReceiver;
+ private ApcPowerReceiverComponent? _powerReceiver;
[ViewVariables(VVAccess.ReadWrite)]
public int PointsPerSecond
@@ -35,7 +35,7 @@ namespace Content.Server.Research.Components
///
/// Whether this can be used to produce research points.
///
- /// If no