Pow3r: stage 1 (#4208)

Co-authored-by: 20kdc <asdd2808@gmail.com>
This commit is contained in:
Pieter-Jan Briers
2021-07-04 18:11:52 +02:00
committed by GitHub
parent ea60a81fdf
commit 103bc19508
212 changed files with 8584 additions and 4426 deletions

View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=power_005Cvisualizers/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -1,5 +1,6 @@
using System; using System;
using Content.Client.Wires; using Content.Client.Wires;
using Content.Client.Wires.Visualizers;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Doors; using Content.Shared.Doors;
using JetBrains.Annotations; using JetBrains.Annotations;

View File

@@ -55,7 +55,7 @@ namespace Content.Client.Entry
"AccessReader", "AccessReader",
"IdCardConsole", "IdCardConsole",
"Airlock", "Airlock",
"WirePlacer", "CablePlacer",
"Drink", "Drink",
"Food", "Food",
"FoodContainer", "FoodContainer",
@@ -73,6 +73,7 @@ namespace Content.Client.Entry
"StorageFill", "StorageFill",
"Mop", "Mop",
"Bucket", "Bucket",
"CableVis",
"Puddle", "Puddle",
"CanSpill", "CanSpill",
"SpeedLoader", "SpeedLoader",
@@ -105,12 +106,11 @@ namespace Content.Client.Entry
"PowerSupplier", "PowerSupplier",
"PowerConsumer", "PowerConsumer",
"Battery", "Battery",
"BatteryStorage",
"BatteryDischarger", "BatteryDischarger",
"Apc", "Apc",
"PowerProvider", "PowerProvider",
"PowerReceiver", "ApcPowerReceiver",
"Wire", "Cable",
"StressTestMovement", "StressTestMovement",
"Toys", "Toys",
"SurgeryTool", "SurgeryTool",
@@ -273,6 +273,8 @@ namespace Content.Client.Entry
"ExplosionLaunched", "ExplosionLaunched",
"BeingCloned", "BeingCloned",
"Advertise", "Advertise",
"PowerNetworkBattery",
"BatteryCharger",
}; };
} }
} }

View File

@@ -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");
}
}
}

View File

@@ -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<int, NodeVis.GroupData> Groups { get; } = new();
public HashSet<string> Filtered { get; } = new();
public Dictionary<EntityUid, (NodeVis.GroupData group, NodeVis.NodeDatum node)[]>
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<NodeVis.MsgData>(DataMsgHandler);
}
public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay<NodeVisualizationOverlay>();
}
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();
}
}
}
}

View File

@@ -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<IClientAdminManager>();
if (!adminMan.HasFlag(AdminFlags.Debug))
{
shell.WriteError("You need +DEBUG for this command");
return;
}
var sys = EntitySystem.Get<NodeGroupSystem>();
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<NodeGroupSystem>();
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);
}
}
}
}
}

View File

@@ -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<GridId, Dictionary<Vector2i, List<(GroupData, NodeDatum)>>> _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;
}
}
}
}

View File

@@ -9,7 +9,7 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Maths; using Robust.Shared.Maths;
namespace Content.Client.APC namespace Content.Client.Power.APC
{ {
[UsedImplicitly] [UsedImplicitly]
public class ApcBoundUserInterface : BoundUserInterface public class ApcBoundUserInterface : BoundUserInterface

View File

@@ -3,7 +3,7 @@ using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
namespace Content.Client.APC namespace Content.Client.Power.APC
{ {
public class ApcVisualizer : AppearanceVisualizer public class ApcVisualizer : AppearanceVisualizer
{ {

View File

@@ -4,7 +4,7 @@ using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
namespace Content.Client.SMES namespace Content.Client.Power.SMES
{ {
[UsedImplicitly] [UsedImplicitly]
public class SmesVisualizer : AppearanceVisualizer public class SmesVisualizer : AppearanceVisualizer

View File

@@ -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}");
}
}
}

View File

@@ -66,7 +66,7 @@ namespace Content.Client.Stack
/// ///
/// <list type="bullet"> /// <list type="bullet">
/// <item> /// <item>
/// <description>false: they are opaque and mutually exclusive (e.g. sprites in a wire coil). <b>Default value</b></description> /// <description>false: they are opaque and mutually exclusive (e.g. sprites in a cable coil). <b>Default value</b></description>
/// </item> /// </item>
/// <item> /// <item>
/// <description>true: they are transparent and thus layered one over another in ascending order first</description> /// <description>true: they are transparent and thus layered one over another in ascending order first</description>

View File

@@ -1,7 +1,7 @@
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using static Content.Shared.Wires.SharedWiresComponent; using static Content.Shared.Wires.SharedWiresComponent;
namespace Content.Client.Wires namespace Content.Client.Wires.Visualizers
{ {
public class WiresVisualizer : AppearanceVisualizer public class WiresVisualizer : AppearanceVisualizer
{ {

View File

@@ -85,7 +85,7 @@ namespace Content.IntegrationTests.Tests.Disposal
components: components:
- type: DisposalUnit - type: DisposalUnit
- type: Anchorable - type: Anchorable
- type: PowerReceiver - type: ApcPowerReceiver
- type: Physics - type: Physics
bodyType: Static bodyType: Static
@@ -155,7 +155,7 @@ namespace Content.IntegrationTests.Tests.Disposal
Flush(unit, false, human, wrench); Flush(unit, false, human, wrench);
// Remove power need // Remove power need
Assert.True(disposalUnit.TryGetComponent(out PowerReceiverComponent? power)); Assert.True(disposalUnit.TryGetComponent(out ApcPowerReceiverComponent? power));
power!.NeedsPower = false; power!.NeedsPower = false;
Assert.True(unit.Powered); Assert.True(unit.Powered);

View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Battery.Components; using Content.Server.Power.Components;
using Content.Server.PowerCell.Components; using Content.Server.PowerCell.Components;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Coordinates; using Content.Shared.Coordinates;

View File

@@ -22,7 +22,7 @@ namespace Content.IntegrationTests.Tests
id: GravityGeneratorDummy id: GravityGeneratorDummy
components: components:
- type: GravityGenerator - type: GravityGenerator
- type: PowerReceiver - type: ApcPowerReceiver
"; ";
[Test] [Test]
public async Task Test() public async Task Test()
@@ -48,9 +48,9 @@ namespace Content.IntegrationTests.Tests
generator = entityMan.SpawnEntity("GravityGeneratorDummy", grid2.ToCoordinates()); generator = entityMan.SpawnEntity("GravityGeneratorDummy", grid2.ToCoordinates());
Assert.That(generator.HasComponent<GravityGeneratorComponent>()); Assert.That(generator.HasComponent<GravityGeneratorComponent>());
Assert.That(generator.HasComponent<PowerReceiverComponent>()); Assert.That(generator.HasComponent<ApcPowerReceiverComponent>());
var generatorComponent = generator.GetComponent<GravityGeneratorComponent>(); var generatorComponent = generator.GetComponent<GravityGeneratorComponent>();
var powerComponent = generator.GetComponent<PowerReceiverComponent>(); var powerComponent = generator.GetComponent<ApcPowerReceiverComponent>();
Assert.That(generatorComponent.Status, Is.EqualTo(GravityGeneratorStatus.Unpowered)); Assert.That(generatorComponent.Status, Is.EqualTo(GravityGeneratorStatus.Unpowered));
powerComponent.NeedsPower = false; powerComponent.NeedsPower = false;
}); });

View File

@@ -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<IMapManager>();
_entityManager = _server.ResolveDependency<IEntityManager>();
_gameTiming = _server.ResolveDependency<IGameTiming>();
}
/// <summary>
/// Test small power net with a simple surplus of power over the loads.
/// </summary>
[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<PowerSupplierComponent>();
consumer1 = consumerEnt1.GetComponent<PowerConsumerComponent>();
consumer2 = consumerEnt2.GetComponent<PowerConsumerComponent>();
// 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();
}
/// <summary>
/// Test small power net with a simple deficit of power over the loads.
/// </summary>
[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<PowerSupplierComponent>();
consumer1 = consumerEnt1.GetComponent<PowerConsumerComponent>();
consumer2 = consumerEnt2.GetComponent<PowerConsumerComponent>();
// 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<PowerSupplierComponent>();
consumer = consumerEnt.GetComponent<PowerConsumerComponent>();
// 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<PowerNetworkBatteryComponent>();
battery = generatorEnt.GetComponent<BatteryComponent>();
consumer = consumerEnt.GetComponent<PowerConsumerComponent>();
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<PowerSupplierComponent>();
var netBattery = batteryEnt.GetComponent<PowerNetworkBatteryComponent>();
battery = batteryEnt.GetComponent<BatteryComponent>();
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<PowerConsumerComponent>();
supplier = supplyEnt.GetComponent<PowerSupplierComponent>();
netBattery = batteryEnt.GetComponent<PowerNetworkBatteryComponent>();
battery = batteryEnt.GetComponent<BatteryComponent>();
// 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<PowerConsumerComponent>();
supplier = supplyEnt.GetComponent<PowerSupplierComponent>();
netBattery = batteryEnt.GetComponent<PowerNetworkBatteryComponent>();
battery = batteryEnt.GetComponent<BatteryComponent>();
// 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<PowerConsumerComponent>();
consumer2 = consumerEnt2.GetComponent<PowerConsumerComponent>();
supplier = supplyEnt.GetComponent<PowerSupplierComponent>();
var netBattery1 = batteryEnt1.GetComponent<PowerNetworkBatteryComponent>();
var netBattery2 = batteryEnt2.GetComponent<PowerNetworkBatteryComponent>();
var battery1 = batteryEnt1.GetComponent<BatteryComponent>();
var battery2 = batteryEnt2.GetComponent<BatteryComponent>();
// 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();
}
/// <summary>
/// Test that power is distributed proportionally, even through batteries.
/// </summary>
[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<PowerConsumerComponent>();
consumer2 = consumerEnt2.GetComponent<PowerConsumerComponent>();
supplier = supplyEnt.GetComponent<PowerSupplierComponent>();
var netBattery1 = batteryEnt1.GetComponent<PowerNetworkBatteryComponent>();
var netBattery2 = batteryEnt2.GetComponent<PowerNetworkBatteryComponent>();
var battery1 = batteryEnt1.GetComponent<BatteryComponent>();
var battery2 = batteryEnt2.GetComponent<BatteryComponent>();
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<PowerConsumerComponent>();
supplier = supplyEnt.GetComponent<PowerSupplierComponent>();
netBattery = batteryEnt.GetComponent<PowerNetworkBatteryComponent>();
var battery = batteryEnt.GetComponent<BatteryComponent>();
// 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();
}
/// <summary>
/// Test that <see cref="CableTerminalNode"/> correctly isolates two networks.
/// </summary>
[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<NodeContainerComponent>();
leftNode = leftEnt.GetComponent<NodeContainerComponent>().GetNode<CableNode>("power");
rightNode = rightEnt.GetComponent<NodeContainerComponent>().GetNode<CableNode>("power");
batteryInput = batteryNodeContainer.GetNode<Node>("input");
batteryOutput = batteryNodeContainer.GetNode<Node>("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<PowerSupplierComponent>();
substationNetBattery = substationEnt.GetComponent<PowerNetworkBatteryComponent>();
apcBattery = apcEnt.GetComponent<BatteryComponent>();
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<ApcPowerProviderComponent>();
receiver = powerReceiverEnt.GetComponent<ApcPowerReceiverComponent>();
var battery = apcEnt.GetComponent<BatteryComponent>();
apcNetBattery = apcEnt.GetComponent<PowerNetworkBatteryComponent>();
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();
}
}
}

View File

@@ -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<IMapManager>();
var entityMan = IoCManager.Resolve<IEntityManager>();
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<PowerSupplierComponent>();
consumer1 = consumerEnt1.GetComponent<PowerConsumerComponent>();
consumer2 = consumerEnt2.GetComponent<PowerConsumerComponent>();
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<IMapManager>();
var entityMan = IoCManager.Resolve<IEntityManager>();
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<PowerSupplierComponent>();
substationSupplier = substationEnt.GetComponent<PowerSupplierComponent>();
var substationStorage = substationEnt.GetComponent<BatteryStorageComponent>();
var substationDischarger = substationEnt.GetComponent<BatteryDischargerComponent>();
apcBattery = apcEnt.GetComponent<BatteryComponent>();
var apcStorage = apcEnt.GetComponent<BatteryStorageComponent>();
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<IMapManager>();
var entityMan = IoCManager.Resolve<IEntityManager>();
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<ApcComponent>();
var provider = apcExtensionEnt.GetComponent<PowerProviderComponent>();
receiver = powerReceiverEnt.GetComponent<PowerReceiverComponent>();
var battery = apcEnt.GetComponent<BatteryComponent>();
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();
}
}
}

View File

@@ -122,7 +122,24 @@ namespace Content.IntegrationTests.Tests
two = reader.ReadToEnd(); 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);
}
});
} }
} }
} }

View File

@@ -8,6 +8,7 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes; using Content.Server.NodeContainer.Nodes;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
@@ -36,19 +37,23 @@ namespace Content.Server.AME
public int CoreCount => _cores.Count; public int CoreCount => _cores.Count;
protected override void OnAddNode(Node node) public override void LoadNodes(List<Node> groupNodes)
{ {
base.OnAddNode(node); base.LoadNodes(groupNodes);
if (_masterController == null)
foreach (var node in groupNodes)
{ {
node.Owner.TryGetComponent<AMEControllerComponent>(out var controller); if (node.Owner.TryGetComponent(out AMEControllerComponent? controller))
_masterController = controller; {
_masterController = controller;
}
} }
} }
protected override void OnRemoveNode(Node node) public override void RemoveNode(Node node)
{ {
base.OnRemoveNode(node); base.RemoveNode(node);
RefreshAMENodes(_masterController); RefreshAMENodes(_masterController);
if (_masterController != null && _masterController?.Owner == node.Owner) { _masterController = null; } if (_masterController != null && _masterController?.Owner == node.Owner) { _masterController = null; }
} }

View File

@@ -33,7 +33,7 @@ namespace Content.Server.AME.Components
private AppearanceComponent? _appearance; private AppearanceComponent? _appearance;
private PowerSupplierComponent? _powerSupplier; private PowerSupplierComponent? _powerSupplier;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered; private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] [ViewVariables]
private int _stability = 100; private int _stability = 100;
@@ -92,7 +92,7 @@ namespace Content.Server.AME.Components
if(fuelJar != null && _powerSupplier != null) if(fuelJar != null && _powerSupplier != null)
{ {
var availableInject = fuelJar.FuelAmount >= InjectionAmount ? InjectionAmount : fuelJar.FuelAmount; 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; fuelJar.FuelAmount -= availableInject;
InjectSound(overloading); InjectSound(overloading);
UpdateUserInterface(); UpdateUserInterface();
@@ -252,7 +252,7 @@ namespace Content.Server.AME.Components
_appearance?.SetData(AMEControllerVisuals.DisplayState, "off"); _appearance?.SetData(AMEControllerVisuals.DisplayState, "off");
if (_powerSupplier != null) if (_powerSupplier != null)
{ {
_powerSupplier.SupplyRate = 0; _powerSupplier.MaxSupply = 0;
} }
} }
_injecting = !_injecting; _injecting = !_injecting;

View File

@@ -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<BaseApcNetComponent, IApcNet>, IApcNet
{
[ViewVariables]
private readonly Dictionary<ApcComponent, BatteryComponent> _apcBatteries = new();
[ViewVariables]
private readonly List<PowerProviderComponent> _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<ApcNetSystem>().AddApcNet(this);
}
protected override void AfterRemake(IEnumerable<INodeGroup> 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<ApcNetSystem>().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
{
/// <summary>
/// It is important that this returns false, so <see cref="PowerProviderComponent"/>s with a <see cref="NullApcNet"/> have no power.
/// </summary>
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) { }
}
}
}

View File

@@ -1,52 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
namespace Content.Server.APC
{
[UsedImplicitly]
internal sealed class ApcNetSystem : EntitySystem
{
[Dependency] private readonly IPauseManager _pauseManager = default!;
private HashSet<IApcNet> _apcNets = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
}
public override void Update(float frameTime)
{
foreach (var apcNet in _apcNets)
{
var gridId = apcNet.GridId;
if (gridId != null && !_pauseManager.IsGridPaused(gridId.Value))
apcNet.Update(frameTime);
}
}
public void AddApcNet(ApcNetNodeGroup apcNet)
{
_apcNets.Add(apcNet);
}
public void RemoveApcNet(ApcNetNodeGroup apcNet)
{
_apcNets.Remove(apcNet);
}
public void Reset(RoundRestartCleanupEvent ev)
{
// NodeGroupSystem does not remake ApcNets affected during restarting until a frame later,
// when their grid is invalid. So, we are clearing them on round restart.
_apcNets.Clear();
}
}
}

View File

@@ -1,10 +0,0 @@
#nullable enable
using Content.Server.Power.Components;
namespace Content.Server.APC.Components
{
public abstract class BaseApcNetComponent : BaseNetConnectorComponent<IApcNet>
{
protected override IApcNet NullNet => ApcNetNodeGroup.NullNet;
}
}

View File

@@ -34,7 +34,7 @@ namespace Content.Server.Access.Components
private ContainerSlot _targetIdContainer = default!; private ContainerSlot _targetIdContainer = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(IdCardConsoleUiKey.Key); [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 PrivilegedIDEmpty => _privilegedIdContainer.ContainedEntities.Count < 1;
private bool TargetIDEmpty => _targetIdContainer.ContainedEntities.Count < 1; private bool TargetIDEmpty => _targetIdContainer.ContainedEntities.Count < 1;

View File

@@ -27,7 +27,7 @@ namespace Content.Server.Arcade.Components
public override string Name => "BlockGameArcade"; public override string Name => "BlockGameArcade";
public override uint? NetID => ContentNetIDs.BLOCKGAME_ARCADE; 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 bool Powered => _powerReceiverComponent?.Powered ?? false;
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BlockGameUiKey.Key); private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BlockGameUiKey.Key);

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.UserInterface; using Content.Server.UserInterface;
using Content.Server.VendingMachines; using Content.Server.VendingMachines;
using Content.Server.Wires.Components; using Content.Server.WireHacking;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Arcade; using Content.Shared.Arcade;
using Content.Shared.Interaction; using Content.Shared.Interaction;
@@ -28,7 +28,7 @@ namespace Content.Server.Arcade.Components
{ {
[Dependency] private readonly IRobustRandom _random = null!; [Dependency] private readonly IRobustRandom _random = null!;
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiverComponent = default!; [ComponentDependency] private readonly ApcPowerReceiverComponent? _powerReceiverComponent = default!;
[ComponentDependency] private readonly WiresComponent? _wiresComponent = default!; [ComponentDependency] private readonly WiresComponent? _wiresComponent = default!;
private bool Powered => _powerReceiverComponent != null && _powerReceiverComponent.Powered; private bool Powered => _powerReceiverComponent != null && _powerReceiverComponent.Powered;

View File

@@ -20,7 +20,7 @@ namespace Content.Server.GameObjects.Components
protected readonly object UserInterfaceKey; protected readonly object UserInterfaceKey;
[ViewVariables] protected BoundUserInterface? UserInterface => Owner.GetUIOrNull(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) public BaseComputerUserInterfaceComponent(object key)
{ {

View File

@@ -4,9 +4,9 @@ using System;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Respiratory; using Content.Server.Body.Respiratory;
using Content.Server.Explosion; using Content.Server.Explosion;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.Interfaces; using Content.Server.Interfaces;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.UserInterface; using Content.Server.UserInterface;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Actions; using Content.Shared.Actions;

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Atmos.EntitySystems; using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Maps; using Content.Shared.Maps;
using JetBrains.Annotations; using JetBrains.Annotations;
@@ -33,6 +34,8 @@ namespace Content.Server.Atmos.EntitySystems
{ {
base.Initialize(); base.Initialize();
UpdatesAfter.Add(typeof(NodeGroupSystem));
InitializeGases(); InitializeGases();
InitializeCVars(); InitializeCVars();

View File

@@ -1,5 +1,5 @@
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers; using Content.Shared.Interaction.Helpers;

View File

@@ -1,8 +1,8 @@
using System; using System;
using Content.Server.Atmos.Piping.Binary.Components; using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;

View File

@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Binary.Components; using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping; using Content.Shared.Atmos.Piping;
using JetBrains.Annotations; using JetBrains.Annotations;

View File

@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Binary.Components; using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;

View File

@@ -1,8 +1,8 @@
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Construction.Components; using Content.Server.Construction.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Notification.Managers; using Content.Shared.Notification.Managers;
using JetBrains.Annotations; using JetBrains.Annotations;

View File

@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Trinary.Components; using Content.Server.Atmos.Piping.Trinary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;

View File

@@ -1,8 +1,8 @@
using System; using System;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Trinary.Components; using Content.Server.Atmos.Piping.Trinary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;

View File

@@ -4,9 +4,9 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Binary.Components; using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.Hands.Components; using Content.Server.Hands.Components;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.UserInterface; using Content.Server.UserInterface;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Atmos; using Content.Shared.Atmos;
@@ -62,7 +62,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
return; return;
// Create a pipenet if we don't have one already. // Create a pipenet if we don't have one already.
portNode.TryAssignGroupIfNeeded(); portNode.CreateSingleNetImmediate();
Get<AtmosphereSystem>().Merge(portNode.Air, canister.InitialMixture); Get<AtmosphereSystem>().Merge(portNode.Air, canister.InitialMixture);
portNode.Air.Temperature = canister.InitialMixture.Temperature; portNode.Air.Temperature = canister.InitialMixture.Temperature;
portNode.Volume = canister.InitialMixture.Volume; 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, 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.ReleasePressure, canister.ReleaseValve,
canister.MinReleasePressure, canister.MaxReleasePressure)); canister.MinReleasePressure, canister.MaxReleasePressure));
} }

View File

@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;

View File

@@ -1,8 +1,8 @@
using System; using System;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;

View File

@@ -2,8 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.Piping.Binary.Components; using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.Construction.Components; using Content.Server.Construction.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping.Unary.Components; using Content.Shared.Atmos.Piping.Unary.Components;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;

View File

@@ -1,8 +1,8 @@
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping; using Content.Shared.Atmos.Piping;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;

View File

@@ -1,8 +1,8 @@
using System; using System;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Atmos.Visuals; using Content.Shared.Atmos.Visuals;
using JetBrains.Annotations; using JetBrains.Annotations;

View File

@@ -2,8 +2,8 @@ using System;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components; using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Unary.Visuals; using Content.Shared.Atmos.Piping.Unary.Visuals;
using JetBrains.Annotations; using JetBrains.Annotations;

View File

@@ -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() private void UpdateSignInfo()
{ {

View File

@@ -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<BatteryDischargerComponent>(false))
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -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<BatteryStorageComponent>(false))
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -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<BatteryComponent>(true))
{
comp.OnUpdate(frameTime);
}
}
}
}

View File

@@ -14,7 +14,7 @@ namespace Content.Server.Botany.Components
[RegisterComponent] [RegisterComponent]
public class SeedExtractorComponent : Component, IInteractUsing public class SeedExtractorComponent : Component, IInteractUsing
{ {
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiver = default!; [ComponentDependency] private readonly ApcPowerReceiverComponent? _powerReceiver = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;

View File

@@ -59,7 +59,7 @@ namespace Content.Server.Cargo.Components
[DataField("requestOnly")] [DataField("requestOnly")]
private bool _requestOnly = false; 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!; private CargoConsoleSystem _cargoConsoleSystem = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CargoConsoleUiKey.Key); [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CargoConsoleUiKey.Key);
@@ -171,7 +171,7 @@ namespace Content.Server.Cargo.Components
{ {
foreach (IEntity entity in enumerator) foreach (IEntity entity in enumerator)
{ {
if (entity.HasComponent<CargoTelepadComponent>() && entity.TryGetComponent<PowerReceiverComponent>(out var powerReceiver) && powerReceiver.Powered) if (entity.HasComponent<CargoTelepadComponent>() && entity.TryGetComponent<ApcPowerReceiverComponent>(out var powerReceiver) && powerReceiver.Powered)
{ {
cargoTelepad = entity; cargoTelepad = entity;
break; break;

View File

@@ -43,14 +43,14 @@ namespace Content.Server.Cargo.Components
{ {
if (args.Powered && _currentState == CargoTelepadState.Unpowered) { if (args.Powered && _currentState == CargoTelepadState.Unpowered) {
_currentState = CargoTelepadState.Idle; _currentState = CargoTelepadState.Idle;
if(Owner.TryGetComponent<SpriteComponent>(out var spriteComponent)) if(Owner.TryGetComponent<SpriteComponent>(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "idle"); spriteComponent.LayerSetState(0, "idle");
TeleportLoop(); TeleportLoop();
} }
else if (!args.Powered) else if (!args.Powered)
{ {
_currentState = CargoTelepadState.Unpowered; _currentState = CargoTelepadState.Unpowered;
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent)) if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "offline"); spriteComponent.LayerSetState(0, "offline");
} }
} }
@@ -59,14 +59,14 @@ namespace Content.Server.Cargo.Components
if (_currentState == CargoTelepadState.Idle && _teleportQueue.Count > 0) if (_currentState == CargoTelepadState.Idle && _teleportQueue.Count > 0)
{ {
_currentState = CargoTelepadState.Charging; _currentState = CargoTelepadState.Charging;
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent)) if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "idle"); spriteComponent.LayerSetState(0, "idle");
Owner.SpawnTimer((int) (TeleportDelay * 1000), () => Owner.SpawnTimer((int) (TeleportDelay * 1000), () =>
{ {
if (!Deleted && !Owner.Deleted && _currentState == CargoTelepadState.Charging && _teleportQueue.Count > 0) if (!Deleted && !Owner.Deleted && _currentState == CargoTelepadState.Charging && _teleportQueue.Count > 0)
{ {
_currentState = CargoTelepadState.Teleporting; _currentState = CargoTelepadState.Teleporting;
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent)) if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "beam"); spriteComponent.LayerSetState(0, "beam");
Owner.SpawnTimer((int) (TeleportDuration * 1000), () => 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)); SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/phasein.ogg", Owner, AudioParams.Default.WithVolume(-8f));
Owner.EntityManager.SpawnEntity(_teleportQueue[0].Product, Owner.Transform.Coordinates); Owner.EntityManager.SpawnEntity(_teleportQueue[0].Product, Owner.Transform.Coordinates);
_teleportQueue.RemoveAt(0); _teleportQueue.RemoveAt(0);
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent)) if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "idle"); spriteComponent.LayerSetState(0, "idle");
_currentState = CargoTelepadState.Idle; _currentState = CargoTelepadState.Idle;
TeleportLoop(); TeleportLoop();

View File

@@ -40,7 +40,7 @@ namespace Content.Server.Chemistry.Components
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null; [ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private bool _bufferModeTransfer = true; [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(); [ViewVariables] private readonly Solution BufferSolution = new();

View File

@@ -51,7 +51,7 @@ namespace Content.Server.Chemistry.Components
[ViewVariables] private ReagentUnit _dispenseAmount = ReagentUnit.New(10); [ViewVariables] private ReagentUnit _dispenseAmount = ReagentUnit.New(10);
[UsedImplicitly] [ViewVariables] private SolutionContainerComponent? Solution => _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>(); [UsedImplicitly] [ViewVariables] private SolutionContainerComponent? Solution => _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
[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); [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ReagentDispenserUiKey.Key);

View File

@@ -74,7 +74,7 @@ namespace Content.Server.Cloning
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
foreach (var (cloning, power) in ComponentManager.EntityQuery<CloningPodComponent, PowerReceiverComponent>(true)) foreach (var (cloning, power) in ComponentManager.EntityQuery<CloningPodComponent, ApcPowerReceiverComponent>(true))
{ {
if (cloning.UiKnownPowerState != power.Powered) if (cloning.UiKnownPowerState != power.Powered)
{ {

View File

@@ -26,7 +26,7 @@ namespace Content.Server.Cloning.Components
[Dependency] private readonly EuiManager _euiManager = null!; [Dependency] private readonly EuiManager _euiManager = null!;
[ViewVariables] [ViewVariables]
public bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered; public bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] [ViewVariables]
public BoundUserInterface? UserInterface => public BoundUserInterface? UserInterface =>

View File

@@ -25,7 +25,7 @@ namespace Content.Server.Communications
{ {
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IChatManager _chatManager = 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<RoundEndSystem>(); private RoundEndSystem RoundEndSystem => EntitySystem.Get<RoundEndSystem>();

View File

@@ -24,7 +24,7 @@ namespace Content.Server.Computer
// Let's ensure the container manager and container are here. // Let's ensure the container manager and container are here.
Owner.EnsureContainer<Container>("board", out var _); Owner.EnsureContainer<Container>("board", out var _);
if (Owner.TryGetComponent(out PowerReceiverComponent? powerReceiver) && if (Owner.TryGetComponent(out ApcPowerReceiverComponent? powerReceiver) &&
Owner.TryGetComponent(out AppearanceComponent? appearance)) Owner.TryGetComponent(out AppearanceComponent? appearance))
{ {
appearance.SetData(ComputerVisuals.Powered, powerReceiver.Powered); appearance.SetData(ComputerVisuals.Powered, powerReceiver.Powered);

View File

@@ -1,6 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.GameObjects.Components; using Content.Server.GameObjects.Components;
using Content.Server.Wires.Components; using Content.Server.WireHacking;
using Content.Shared.Construction; using Content.Shared.Construction;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;

View File

@@ -1,6 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.GameObjects.Components; using Content.Server.GameObjects.Components;
using Content.Server.Wires.Components; using Content.Server.WireHacking;
using Content.Shared.Construction; using Content.Shared.Construction;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;

View File

@@ -19,7 +19,7 @@ namespace Content.Server.Conveyor
{ {
public override string Name => "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;
/// <summary> /// <summary>
/// The angle to move entities by in relation to the owner's rotation. /// The angle to move entities by in relation to the owner's rotation.
@@ -105,7 +105,7 @@ namespace Content.Server.Conveyor
return false; return false;
} }
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) && if (Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) &&
!receiver.Powered) !receiver.Powered)
{ {
return false; return false;

View File

@@ -26,7 +26,7 @@ namespace Content.Server.DeviceNetwork.Connections
return false; return false;
} }
if (_owner.TryGetComponent<PowerReceiverComponent>(out var powerReceiver) if (_owner.TryGetComponent<ApcPowerReceiverComponent>(out var powerReceiver)
&& TryGetWireNet(powerReceiver, out var ownNet) && TryGetWireNet(powerReceiver, out var ownNet)
&& metadata.TryParseMetadata<INodeGroup>(WIRENET, out var senderNet)) && metadata.TryParseMetadata<INodeGroup>(WIRENET, out var senderNet))
{ {
@@ -44,7 +44,7 @@ namespace Content.Server.DeviceNetwork.Connections
return new Metadata(); return new Metadata();
} }
if (_owner.TryGetComponent<PowerReceiverComponent>(out var powerReceiver) if (_owner.TryGetComponent<ApcPowerReceiverComponent>(out var powerReceiver)
&& TryGetWireNet(powerReceiver, out var net)) && TryGetWireNet(powerReceiver, out var net))
{ {
var metadata = new Metadata var metadata = new Metadata
@@ -63,16 +63,16 @@ namespace Content.Server.DeviceNetwork.Connections
return payload; 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 && var provider = apcPowerReceiver.Provider;
provider.ProviderOwner.TryGetComponent<NodeContainerComponent>(out var nodeContainer)) if (provider != null && provider.ProviderOwner.TryGetComponent<NodeContainerComponent>(out var nodeContainer))
{ {
var nodes = nodeContainer.Nodes; var nodes = nodeContainer.Nodes;
foreach (var node in nodes.Values) foreach (var node in nodes.Values)
{ {
if (node.NodeGroupID == NodeGroupID.WireNet) if (node.NodeGroupID == NodeGroupID.WireNet && node.NodeGroup != null)
{ {
net = node.NodeGroup; net = node.NodeGroup;
return true; return true;

View File

@@ -117,7 +117,7 @@ namespace Content.Server.Disposal.Mailing
[ViewVariables] [ViewVariables]
public bool Powered => public bool Powered =>
!Owner.TryGetComponent(out PowerReceiverComponent? receiver) || !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) ||
receiver.Powered; receiver.Powered;
[ViewVariables] [ViewVariables]
@@ -372,7 +372,7 @@ namespace Content.Server.Disposal.Mailing
private void TogglePower() private void TogglePower()
{ {
if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver)) if (!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{ {
return; return;
} }

View File

@@ -107,7 +107,7 @@ namespace Content.Server.Disposal.Unit.Components
[ViewVariables] [ViewVariables]
public bool Powered => public bool Powered =>
!Owner.TryGetComponent(out PowerReceiverComponent? receiver) || !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) ||
receiver.Powered; receiver.Powered;
[ViewVariables] [ViewVariables]
@@ -314,7 +314,7 @@ namespace Content.Server.Disposal.Unit.Components
private void TogglePower() private void TogglePower()
{ {
if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver)) if (!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{ {
return; return;
} }

View File

@@ -3,7 +3,7 @@ using System;
using System.Threading; using System.Threading;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.VendingMachines; using Content.Server.VendingMachines;
using Content.Server.Wires.Components; using Content.Server.WireHacking;
using Content.Shared.Doors; using Content.Shared.Doors;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Notification; using Content.Shared.Notification;
@@ -36,7 +36,7 @@ namespace Content.Server.Doors.Components
private readonly SharedAppearanceComponent? _appearanceComponent = null; private readonly SharedAppearanceComponent? _appearanceComponent = null;
[ComponentDependency] [ComponentDependency]
private readonly PowerReceiverComponent? _receiverComponent = null; private readonly ApcPowerReceiverComponent? _receiverComponent = null;
[ComponentDependency] [ComponentDependency]
private readonly WiresComponent? _wiresComponent = null; private readonly WiresComponent? _wiresComponent = null;

View File

@@ -26,7 +26,7 @@ namespace Content.Server.Gravity
private GravityGeneratorStatus _status; 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; public bool SwitchedOn => _switchedOn;

View File

@@ -50,7 +50,6 @@ namespace Content.Server.IoC
IoCManager.Register<ActionManager, ActionManager>(); IoCManager.Register<ActionManager, ActionManager>();
IoCManager.Register<IPDAUplinkManager,PDAUplinkManager>(); IoCManager.Register<IPDAUplinkManager,PDAUplinkManager>();
IoCManager.Register<INodeGroupFactory, NodeGroupFactory>(); IoCManager.Register<INodeGroupFactory, NodeGroupFactory>();
IoCManager.Register<IPowerNetManager, PowerNetManager>();
IoCManager.Register<BlackboardManager, BlackboardManager>(); IoCManager.Register<BlackboardManager, BlackboardManager>();
IoCManager.Register<ConsiderationsManager, ConsiderationsManager>(); IoCManager.Register<ConsiderationsManager, ConsiderationsManager>();
IoCManager.Register<IAccentManager, AccentManager>(); IoCManager.Register<IAccentManager, AccentManager>();

View File

@@ -67,7 +67,7 @@ namespace Content.Server.Kitchen.Components
[ViewVariables] [ViewVariables]
private uint _currentCookTimerTime = 1; 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 _hasContents => Owner.TryGetComponent(out SolutionContainerComponent? solution) && (solution.ReagentList.Count > 0 || _storage.ContainedEntities.Count > 0);
private bool _uiDirty = true; private bool _uiDirty = true;
private bool _lostPower = false; private bool _lostPower = false;

View File

@@ -53,7 +53,7 @@ namespace Content.Server.Kitchen.Components
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null; [ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ReagentGrinderUiKey.Key); [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;
/// <summary> /// <summary>
/// Should the BoundUI be told to update? /// Should the BoundUI be told to update?

View File

@@ -42,7 +42,7 @@ namespace Content.Server.Lathe.Components
[ViewVariables] [ViewVariables]
private LatheRecipePrototype? _producingRecipe; private LatheRecipePrototype? _producingRecipe;
[ViewVariables] [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); private static readonly TimeSpan InsertionTime = TimeSpan.FromSeconds(0.9f);

View File

@@ -1,9 +1,9 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.Battery.Components;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Light.Component;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Localization; using Robust.Shared.Localization;
@@ -60,7 +60,7 @@ namespace Content.Server.Light.Components
/// </summary> /// </summary>
public void UpdateState() public void UpdateState()
{ {
if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver)) if (!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{ {
return; return;
} }
@@ -80,7 +80,7 @@ namespace Content.Server.Light.Components
public void OnUpdate(float frameTime) public void OnUpdate(float frameTime)
{ {
if (Owner.Deleted || !Owner.TryGetComponent(out BatteryComponent? battery)) if (Owner.Deleted || !Owner.TryGetComponent(out BatteryComponent? battery) || Owner.Paused)
{ {
return; return;
} }
@@ -96,9 +96,9 @@ namespace Content.Server.Light.Components
else else
{ {
battery.CurrentCharge += _chargingWattage * frameTime * _chargingEfficiency; 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; receiver.Load = 1;
} }
@@ -110,28 +110,24 @@ namespace Content.Server.Light.Components
private void TurnOff() private void TurnOff()
{ {
if (Owner.TryGetComponent(out SpriteComponent? sprite))
{
sprite.LayerSetState(0, "emergency_light_off");
}
if (Owner.TryGetComponent(out PointLightComponent? light)) if (Owner.TryGetComponent(out PointLightComponent? light))
{ {
light.Enabled = false; light.Enabled = false;
} }
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
appearance.SetData(EmergencyLightVisuals.On, false);
} }
private void TurnOn() private void TurnOn()
{ {
if (Owner.TryGetComponent(out SpriteComponent? sprite))
{
sprite.LayerSetState(0, "emergency_light_on");
}
if (Owner.TryGetComponent(out PointLightComponent? light)) if (Owner.TryGetComponent(out PointLightComponent? light))
{ {
light.Enabled = true; light.Enabled = true;
} }
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
appearance.SetData(EmergencyLightVisuals.On, true);
} }
public override void HandleMessage(ComponentMessage message, IComponent? component) public override void HandleMessage(ComponentMessage message, IComponent? component)

View File

@@ -198,7 +198,7 @@ namespace Content.Server.Light.Components
/// </summary> /// </summary>
public void UpdateLight() public void UpdateLight()
{ {
var powerReceiver = Owner.GetComponent<PowerReceiverComponent>(); var powerReceiver = Owner.GetComponent<ApcPowerReceiverComponent>();
if (LightBulb == null) // No light bulb. if (LightBulb == null) // No light bulb.
{ {

View File

@@ -48,7 +48,7 @@ namespace Content.Server.Medical.Components
private readonly Vector2 _ejectOffset = new(0f, 0f); private readonly Vector2 _ejectOffset = new(0f, 0f);
[ViewVariables] [ViewVariables]
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered; private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] [ViewVariables]
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(MedicalScannerUiKey.Key); private BoundUserInterface? UserInterface => Owner.GetUIOrNull(MedicalScannerUiKey.Key);

View File

@@ -11,16 +11,51 @@ namespace Content.Server.NodeContainer.EntitySystems
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<NodeContainerComponent, ComponentInit>(OnInitEvent);
SubscribeLocalEvent<NodeContainerComponent, ComponentStartup>(OnStartupEvent);
SubscribeLocalEvent<NodeContainerComponent, ComponentShutdown>(OnShutdownEvent);
SubscribeLocalEvent<NodeContainerComponent, AnchorStateChangedEvent>(OnAnchorStateChanged); SubscribeLocalEvent<NodeContainerComponent, AnchorStateChangedEvent>(OnAnchorStateChanged);
SubscribeLocalEvent<NodeContainerComponent, RotateEvent>(OnRotateEvent); SubscribeLocalEvent<NodeContainerComponent, RotateEvent>(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) if (ev.NewRotation == ev.OldRotation)
{ {

View File

@@ -1,30 +1,385 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Content.Server.Administration.Managers;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Administration;
using Content.Shared.NodeContainer;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects; 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 namespace Content.Server.NodeContainer.EntitySystems
{ {
[UsedImplicitly] [UsedImplicitly]
public class NodeGroupSystem : EntitySystem public class NodeGroupSystem : EntitySystem
{ {
private readonly HashSet<INodeGroup> _dirtyNodeGroups = new(); [Dependency] private readonly IEntityManager _entityManager = default!;
public void AddDirtyNodeGroup(INodeGroup nodeGroup) [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly INodeGroupFactory _nodeGroupFactory = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly List<int> _visDeletes = new();
private readonly List<BaseNodeGroup> _visSends = new();
private readonly HashSet<IPlayerSession> _visPlayers = new();
private readonly HashSet<BaseNodeGroup> _toRemake = new();
private readonly HashSet<Node> _toRemove = new();
private readonly List<Node> _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<NodeVis.MsgEnable>(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> {node});
} }
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(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<BaseNodeGroup>();
// 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<Node> 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<Node> FloodFillNode(Node rootNode)
{
// All nodes we're filling into that currently have NO network.
var allNodes = new List<Node>();
var stack = new Stack<Node>();
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<Node> 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<NodeContainerComponent>()
.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
};
} }
} }
} }

View File

@@ -20,60 +20,18 @@ namespace Content.Server.NodeContainer
{ {
public override string Name => "NodeContainer"; public override string Name => "NodeContainer";
[ViewVariables] [DataField("nodes")] [ViewVariables] public Dictionary<string, Node> Nodes { get; } = new();
public IReadOnlyDictionary<string, Node> Nodes => _nodes;
[DataField("nodes")] [DataField("examinable")] private bool _examinable = false;
private readonly Dictionary<string, Node> _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();
}
}
public T GetNode<T>(string identifier) where T : Node public T GetNode<T>(string identifier) where T : Node
{ {
return (T)_nodes[identifier]; return (T) Nodes[identifier];
} }
public bool TryGetNode<T>(string identifier, [NotNullWhen(true)] out T? node) where T : Node public bool TryGetNode<T>(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; node = t;
return true; return true;

View File

@@ -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<TNetConnector, TNetType> : BaseNodeGroup where TNetConnector : BaseNetConnectorComponent<TNetType>
{
private readonly Dictionary<Node, List<TNetConnector>> _netConnectorComponents = new();
protected override void OnAddNode(Node node)
{
var newNetConnectorComponents = node.Owner
.GetAllComponents<TNetConnector>()
.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);
}
}
}

View File

@@ -0,0 +1,91 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.ViewVariables;
namespace Content.Server.NodeContainer.NodeGroups
{
/// <summary>
/// Maintains a collection of <see cref="Node"/>s, and performs operations requiring a list of
/// all connected <see cref="Node"/>s.
/// </summary>
public interface INodeGroup
{
bool Remaking { get; }
IReadOnlyList<Node> Nodes { get; }
void Create(NodeGroupID groupId);
void Initialize(Node sourceNode);
void RemoveNode(Node node);
void LoadNodes(List<Node> groupNodes);
// In theory, the SS13 curse ensures this method will never be called.
void AfterRemake(IEnumerable<IGrouping<INodeGroup?, Node>> newGroups);
// TODO: Why is this method needed?
void QueueRemake();
}
[NodeGroup(NodeGroupID.Default, NodeGroupID.WireNet)]
public class BaseNodeGroup : INodeGroup
{
public bool Remaking { get; set; }
IReadOnlyList<Node> INodeGroup.Nodes => Nodes;
[ViewVariables] public readonly List<Node> Nodes = new();
[ViewVariables] public int NodeCount => Nodes.Count;
/// <summary>
/// Debug variable to indicate that this NodeGroup should not be being used by anything.
/// </summary>
[ViewVariables]
public bool Removed { get; set; } = false;
[ViewVariables]
protected GridId GridId { get; private set; }
[ViewVariables]
public int NetId;
[ViewVariables]
public NodeGroupID GroupId { get; private set; }
public void Create(NodeGroupID groupId)
{
GroupId = groupId;
}
public virtual void Initialize(Node sourceNode)
{
// TODO: Can we get rid of this GridId?
GridId = sourceNode.Owner.Transform.GridID;
}
public virtual void RemoveNode(Node node)
{
}
public virtual void LoadNodes(
List<Node> groupNodes)
{
Nodes.AddRange(groupNodes);
}
public virtual void AfterRemake(IEnumerable<IGrouping<INodeGroup?, Node>> newGroups) { }
public void QueueRemake()
{
EntitySystem.Get<NodeGroupSystem>().QueueRemakeGroup(this);
}
}
}

View File

@@ -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
{
/// <summary>
/// Maintains a collection of <see cref="Node"/>s, and performs operations requiring a list of
/// all connected <see cref="Node"/>s.
/// </summary>
public interface INodeGroup
{
IReadOnlyList<Node> 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<Node> Nodes => _nodes;
private readonly List<Node> _nodes = new();
[ViewVariables]
public int NodeCount => Nodes.Count;
/// <summary>
/// Debug variable to indicate that this NodeGroup should not be being used by anything.
/// </summary>
[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<NodeGroupSystem>().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;
}
/// <summary>
/// Causes all <see cref="Node"/>s to remake their groups. Called when a <see cref="Node"/> is removed
/// and may have split a group in two, so multiple new groups may need to be formed.
/// </summary>
public void RemakeGroup()
{
foreach (var node in Nodes)
{
node.ClearNodeGroup();
}
var newGroups = new HashSet<INodeGroup>();
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<INodeGroup> newGroups) { }
protected class NullNodeGroup : INodeGroup
{
public IReadOnlyList<Node> Nodes => _nodes;
private readonly List<Node> _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() { }
}
}
}

View File

@@ -1,5 +1,6 @@
#nullable enable #nullable enable
using System; using System;
using JetBrains.Annotations;
namespace Content.Server.NodeContainer.NodeGroups namespace Content.Server.NodeContainer.NodeGroups
{ {
@@ -9,6 +10,7 @@ namespace Content.Server.NodeContainer.NodeGroups
/// have the same type of <see cref="INodeGroup"/>. Used by <see cref="INodeGroupFactory"/>. /// have the same type of <see cref="INodeGroup"/>. Used by <see cref="INodeGroupFactory"/>.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
[MeansImplicitUse]
public class NodeGroupAttribute : Attribute public class NodeGroupAttribute : Attribute
{ {
public NodeGroupID[] NodeGroupIDs { get; } public NodeGroupID[] NodeGroupIDs { get; }

View File

@@ -19,7 +19,7 @@ namespace Content.Server.NodeContainer.NodeGroups
/// <summary> /// <summary>
/// Returns a new <see cref="INodeGroup"/> instance. /// Returns a new <see cref="INodeGroup"/> instance.
/// </summary> /// </summary>
INodeGroup MakeNodeGroup(Node sourceNode); INodeGroup MakeNodeGroup(NodeGroupID id);
} }
public class NodeGroupFactory : INodeGroupFactory 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)) if (!_groupTypes.TryGetValue(id, out var type))
{ throw new ArgumentException($"{id} did not have an associated {nameof(INodeGroup)} implementation.");
var nodeGroup = _typeFactory.CreateInstance<INodeGroup>(type);
nodeGroup.Initialize(sourceNode); var instance = _typeFactory.CreateInstance<INodeGroup>(type);
return nodeGroup; instance.Create(id);
} return instance;
throw new ArgumentException($"{sourceNode.NodeGroupID} did not have an associated {nameof(INodeGroup)}.");
} }
} }

View File

@@ -1,14 +1,15 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Content.Server.Atmos; using Content.Server.Atmos;
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.Interfaces; using Content.Server.Interfaces;
using Content.Server.NodeContainer.Nodes; using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Server.NodeContainer.NodeGroups namespace Content.Server.NodeContainer.NodeGroups
@@ -24,17 +25,15 @@ namespace Content.Server.NodeContainer.NodeGroups
[NodeGroup(NodeGroupID.Pipe)] [NodeGroup(NodeGroupID.Pipe)]
public class PipeNet : BaseNodeGroup, IPipeNet public class PipeNet : BaseNodeGroup, IPipeNet
{ {
[ViewVariables] [ViewVariables] public GasMixture Air { get; set; } = new() {Temperature = Atmospherics.T20C};
public GasMixture Air { get; set; } = new() {Temperature = Atmospherics.T20C};
public static readonly IPipeNet NullNet = new NullPipeNet(); [ViewVariables] private readonly List<PipeNode> _pipes = new();
[ViewVariables]
private readonly List<PipeNode> _pipes = new();
[ViewVariables] private AtmosphereSystem? _atmosphereSystem; [ViewVariables] private AtmosphereSystem? _atmosphereSystem;
[ViewVariables] private IGridAtmosphereComponent? GridAtmos => _atmosphereSystem?.GetGridAtmosphere(GridId); [ViewVariables]
private IGridAtmosphereComponent? GridAtmos =>
_atmosphereSystem?.GetGridAtmosphere(GridId);
public override void Initialize(Node sourceNode) public override void Initialize(Node sourceNode)
{ {
@@ -49,35 +48,30 @@ namespace Content.Server.NodeContainer.NodeGroups
_atmosphereSystem?.React(Air, this); _atmosphereSystem?.React(Air, this);
} }
protected override void OnAddNode(Node node) public override void LoadNodes(List<Node> groupNodes)
{ {
if (node is not PipeNode pipeNode) base.LoadNodes(groupNodes);
return;
_pipes.Add(pipeNode); foreach (var node in groupNodes)
pipeNode.JoinPipeNet(this); {
Air.Volume += pipeNode.Volume; 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(); base.RemoveNode(node);
if (node is not PipeNode pipeNode)
return;
pipeNode.ClearPipeNet(); var pipeNode = (PipeNode) node;
Air.Volume -= pipeNode.Volume;
// TODO: Bad O(n^2)
_pipes.Remove(pipeNode); _pipes.Remove(pipeNode);
} }
protected override void OnGivingNodesForCombine(INodeGroup newGroup) public override void AfterRemake(IEnumerable<IGrouping<INodeGroup?, Node>> newGroups)
{
if (newGroup is not IPipeNet newPipeNet)
return;
EntitySystem.Get<AtmosphereSystem>().Merge(newPipeNet.Air, Air);
}
protected override void AfterRemake(IEnumerable<INodeGroup> newGroups)
{ {
RemoveFromGridAtmos(); RemoveFromGridAtmos();
@@ -86,14 +80,15 @@ namespace Content.Server.NodeContainer.NodeGroups
foreach (var newGroup in newGroups) foreach (var newGroup in newGroups)
{ {
if (newGroup is not IPipeNet newPipeNet) if (newGroup.Key is not IPipeNet newPipeNet)
continue; continue;
var newAir = newPipeNet.Air; var newAir = newPipeNet.Air;
var newVolume = newGroup.Cast<PipeNode>().Sum(n => n.Volume);
buffer.Clear(); buffer.Clear();
atmosphereSystem.Merge(buffer, Air); 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); atmosphereSystem.Merge(newAir, buffer);
} }
} }
@@ -102,20 +97,5 @@ namespace Content.Server.NodeContainer.NodeGroups
{ {
GridAtmos?.RemovePipeNet(this); 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() { }
}
} }
} }

View File

@@ -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<BasePowerNetComponent, IPowerNet>, IPowerNet
{
private static readonly Priority[] CachedPriorities = (Priority[]) Enum.GetValues(typeof(Priority));
[Dependency] private readonly IPowerNetManager _powerNetManager = default!;
[ViewVariables]
private readonly List<PowerSupplierComponent> _suppliers = new();
[ViewVariables]
private int _totalSupply = 0;
[ViewVariables]
private readonly Dictionary<Priority, List<PowerConsumerComponent>> _consumersByPriority = new();
[ViewVariables]
private readonly Dictionary<Priority, int> _drawByPriority = new();
public static readonly IPowerNet NullNet = new NullPowerNet();
public PowerNetNodeGroup()
{
foreach (Priority priority in Enum.GetValues(typeof(Priority)))
{
_consumersByPriority.Add(priority, new List<PowerConsumerComponent>());
_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() { }
}
}
}

View File

@@ -1,5 +1,6 @@
#nullable enable #nullable enable
using System.Collections.Generic; using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
@@ -12,29 +13,19 @@ namespace Content.Server.NodeContainer.Nodes
[DataDefinition] [DataDefinition]
public class AdjacentNode : Node public class AdjacentNode : Node
{ {
protected override IEnumerable<Node> GetReachableNodes() public override IEnumerable<Node> GetReachableNodes()
{ {
if (!Owner.Transform.Anchored) if (!Owner.Transform.Anchored)
yield break; yield break;
var compMgr = IoCManager.Resolve<IComponentManager>();
var grid = IoCManager.Resolve<IMapManager>().GetGrid(Owner.Transform.GridID); var grid = IoCManager.Resolve<IMapManager>().GetGrid(Owner.Transform.GridID);
var coords = Owner.Transform.Coordinates; var gridIndex = grid.TileIndicesFor(Owner.Transform.Coordinates);
foreach (var cell in grid.GetCardinalNeighborCells(coords))
foreach (var (_, node) in NodeHelpers.GetCardinalNeighborNodes(compMgr, grid, gridIndex))
{ {
foreach (var entity in grid.GetLocal(Owner.EntityManager.GetEntity(cell).Transform.Coordinates)) if (node != this)
{ yield return node;
if (!Owner.EntityManager.GetEntity(entity).TryGetComponent<NodeContainerComponent>(out var container))
continue;
foreach (var node in container.Nodes.Values)
{
if (node != null && node != this)
{
yield return node;
}
}
}
} }
} }
} }

View File

@@ -1,11 +1,8 @@
#nullable enable #nullable enable
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using Content.Server.NodeContainer.EntitySystems;
using System.Linq;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
@@ -26,20 +23,14 @@ namespace Content.Server.NodeContainer.Nodes
[DataField("nodeGroupID")] [DataField("nodeGroupID")]
public NodeGroupID NodeGroupID { get; private set; } = NodeGroupID.Default; public NodeGroupID NodeGroupID { get; private set; } = NodeGroupID.Default;
[ViewVariables] [ViewVariables] public INodeGroup? NodeGroup;
public INodeGroup NodeGroup { get => _nodeGroup; set => SetNodeGroup(value); }
private INodeGroup _nodeGroup = BaseNodeGroup.NullGroup;
[ViewVariables] [ViewVariables] public IEntity Owner { get; private set; } = default!;
public IEntity Owner { get; private set; } = default!;
[ViewVariables]
private bool _needsGroup = true;
/// <summary> /// <summary>
/// If this node should be considered for connection by other nodes. /// If this node should be considered for connection by other nodes.
/// </summary> /// </summary>
public bool Connectable => !_deleting && Anchored; public bool Connectable => !Deleting && Anchored;
protected bool Anchored => !NeedAnchored || Owner.Transform.Anchored; protected bool Anchored => !NeedAnchored || Owner.Transform.Anchored;
@@ -50,7 +41,15 @@ namespace Content.Server.NodeContainer.Nodes
/// <summary> /// <summary>
/// Prevents a node from being used by other nodes while midway through removal. /// Prevents a node from being used by other nodes while midway through removal.
/// </summary> /// </summary>
private bool _deleting; public bool Deleting;
public readonly HashSet<Node> ReachableNodes = new();
internal int FloodGen;
internal int UndirectGen;
internal bool FlaggedForFlood;
internal int NetId;
public string Name = default!;
public virtual void Initialize(IEntity owner) public virtual void Initialize(IEntity owner)
{ {
@@ -59,20 +58,23 @@ namespace Content.Server.NodeContainer.Nodes
public virtual void OnContainerStartup() public virtual void OnContainerStartup()
{ {
TryAssignGroupIfNeeded(); EntitySystem.Get<NodeGroupSystem>().QueueReflood(this);
CombineGroupWithReachable(); }
public void CreateSingleNetImmediate()
{
EntitySystem.Get<NodeGroupSystem>().CreateSingleNetImmediate(this);
} }
public void AnchorUpdate() public void AnchorUpdate()
{ {
if (Anchored) if (Anchored)
{ {
TryAssignGroupIfNeeded(); EntitySystem.Get<NodeGroupSystem>().QueueReflood(this);
CombineGroupWithReachable();
} }
else else
{ {
RemoveSelfFromGroup(); EntitySystem.Get<NodeGroupSystem>().QueueNodeRemove(this);
} }
} }
@@ -80,107 +82,21 @@ namespace Content.Server.NodeContainer.Nodes
{ {
} }
public virtual void OnPostRebuild()
{
}
public virtual void OnContainerShutdown() public virtual void OnContainerShutdown()
{ {
_deleting = true; Deleting = true;
NodeGroup.RemoveNode(this); EntitySystem.Get<NodeGroupSystem>().QueueNodeRemove(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();
} }
/// <summary> /// <summary>
/// How this node will attempt to find other reachable <see cref="Node"/>s to group with. /// How this node will attempt to find other reachable <see cref="Node"/>s to group with.
/// Returns a set of <see cref="Node"/>s to consider grouping with. Should not return this current <see cref="Node"/>. /// Returns a set of <see cref="Node"/>s to consider grouping with. Should not return this current <see cref="Node"/>.
/// </summary> /// </summary>
protected abstract IEnumerable<Node> GetReachableNodes(); public abstract IEnumerable<Node> GetReachableNodes();
private IEnumerable<Node> GetReachableCompatibleNodes()
{
foreach (var node in GetReachableNodes())
{
if (node.NodeGroupID == NodeGroupID && node.Connectable)
{
yield return node;
}
}
}
private IEnumerable<INodeGroup> 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<INodeGroupFactory>().MakeNodeGroup(this);
}
private void RemoveSelfFromGroup()
{
NodeGroup.RemoveNode(this);
ClearNodeGroup();
}
} }
} }

View File

@@ -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
{
/// <summary>
/// Helper utilities for implementing <see cref="Node"/>.
/// </summary>
public static class NodeHelpers
{
public static IEnumerable<Node> 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)
};
}
}
}

View File

@@ -3,9 +3,8 @@ using System.Collections.Generic;
using Content.Server.Atmos; using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Interfaces; using Content.Server.Interfaces;
using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Containers; using Robust.Shared.Containers;
@@ -13,9 +12,10 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.NodeContainer.Nodes namespace Content.Server.NodeContainer.Nodes
{ {
/// <summary> /// <summary>
/// Connects with other <see cref="PipeNode"/>s whose <see cref="PipeDirection"/> /// Connects with other <see cref="PipeNode"/>s whose <see cref="PipeDirection"/>
@@ -63,21 +63,23 @@ namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
set set
{ {
_connectionsEnabled = value; _connectionsEnabled = value;
RefreshNodeGroup();
if (NodeGroup != null)
EntitySystem.Get<NodeGroupSystem>().QueueRemakeGroup((BaseNodeGroup) NodeGroup);
} }
} }
[DataField("connectionsEnabled")]
private bool _connectionsEnabled = true;
[DataField("rotationsEnabled")] [DataField("rotationsEnabled")]
public bool RotationsEnabled { get; set; } = true; public bool RotationsEnabled { get; set; } = true;
/// <summary> /// <summary>
/// The <see cref="IPipeNet"/> this pipe is a part of. Set to <see cref="PipeNet.NullNet"/> when not in an <see cref="IPipeNet"/>. /// The <see cref="IPipeNet"/> this pipe is a part of.
/// </summary> /// </summary>
[ViewVariables] [ViewVariables]
private IPipeNet _pipeNet = PipeNet.NullNet; private IPipeNet? PipeNet => (IPipeNet?) NodeGroup;
[DataField("connectionsEnabled")]
private bool _connectionsEnabled = true;
/// <summary> /// <summary>
/// Whether to ignore the pipenet and return the environment's air. /// Whether to ignore the pipenet and return the environment's air.
@@ -92,8 +94,12 @@ namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
[ViewVariables] [ViewVariables]
public GasMixture Air public GasMixture Air
{ {
get => !EnvironmentalAir ? _pipeNet.Air : Owner.Transform.Coordinates.GetTileAir() ?? GasMixture.SpaceGas; get => (!EnvironmentalAir ? PipeNet?.Air : Owner.Transform.Coordinates.GetTileAir()) ?? GasMixture.SpaceGas;
set => _pipeNet.Air = value; set
{
DebugTools.Assert(PipeNet != null);
PipeNet!.Air = value;
}
} }
public void AssumeAir(GasMixture giver) public void AssumeAir(GasMixture giver)
@@ -105,7 +111,7 @@ namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
return; return;
} }
EntitySystem.Get<AtmosphereSystem>().Merge(_pipeNet.Air, giver); EntitySystem.Get<AtmosphereSystem>().Merge(PipeNet!.Air, giver);
} }
[ViewVariables] [ViewVariables]
@@ -128,13 +134,6 @@ namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
public void JoinPipeNet(IPipeNet pipeNet) public void JoinPipeNet(IPipeNet pipeNet)
{ {
_pipeNet = pipeNet;
OnConnectedDirectionsNeedsUpdating();
}
public void ClearPipeNet()
{
_pipeNet = PipeNet.NullNet;
OnConnectedDirectionsNeedsUpdating(); OnConnectedDirectionsNeedsUpdating();
} }
@@ -146,12 +145,11 @@ namespace Content.Server.GameObjects.Components.NodeContainer.Nodes
if (!RotationsEnabled) return; if (!RotationsEnabled) return;
var diff = ev.NewRotation - ev.OldRotation; var diff = ev.NewRotation - ev.OldRotation;
PipeDirection = PipeDirection.RotatePipeDirection(diff); PipeDirection = PipeDirection.RotatePipeDirection(diff);
RefreshNodeGroup();
OnConnectedDirectionsNeedsUpdating(); OnConnectedDirectionsNeedsUpdating();
UpdateAppearance(); UpdateAppearance();
} }
protected override IEnumerable<Node> GetReachableNodes() public override IEnumerable<Node> GetReachableNodes()
{ {
for (var i = 0; i < PipeDirectionHelpers.AllPipeDirections; i++) for (var i = 0; i < PipeDirectionHelpers.AllPipeDirections; i++)
{ {

View File

@@ -6,9 +6,10 @@ using System.Diagnostics.CodeAnalysis;
using System.Threading; using System.Threading;
using Content.Server.Notification; using Content.Server.Notification;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.UserInterface; using Content.Server.UserInterface;
using Content.Server.VendingMachines; using Content.Server.VendingMachines;
using Content.Server.Wires.Components; using Content.Server.WireHacking;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
@@ -47,7 +48,7 @@ namespace Content.Server.ParticleAccelerator.Components
/// <summary> /// <summary>
/// Power receiver for the control console itself. /// Power receiver for the control console itself.
/// </summary> /// </summary>
[ViewVariables] private PowerReceiverComponent _powerReceiverComponent = default!; [ViewVariables] private ApcPowerReceiverComponent _apcPowerReceiverComponent = default!;
[ViewVariables] private ParticleAcceleratorFuelChamberComponent? _partFuelChamber; [ViewVariables] private ParticleAcceleratorFuelChamberComponent? _partFuelChamber;
[ViewVariables] private ParticleAcceleratorEndCapComponent? _partEndCap; [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("powerDrawBase")] private int _powerDrawBase = 500;
[ViewVariables(VVAccess.ReadWrite)] [DataField("powerDrawMult")] private int _powerDrawMult = 1500; [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() public ParticleAcceleratorControlBoxComponent()
{ {
@@ -107,9 +108,9 @@ namespace Content.Server.ParticleAccelerator.Components
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage; 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) public override void HandleMessage(ComponentMessage message, IComponent? component)
@@ -189,8 +190,8 @@ namespace Content.Server.ParticleAccelerator.Components
public void UpdateUI() public void UpdateUI()
{ {
var draw = 0; var draw = 0f;
var receive = 0; var receive = 0f;
if (_isEnabled) if (_isEnabled)
{ {
@@ -202,8 +203,8 @@ namespace Content.Server.ParticleAccelerator.Components
_isAssembled, _isAssembled,
_isEnabled, _isEnabled,
_selectedStrength, _selectedStrength,
draw, (int) draw,
receive, (int) receive,
_partEmitterLeft != null, _partEmitterLeft != null,
_partEmitterCenter != null, _partEmitterCenter != null,
_partEmitterRight != null, _partEmitterRight != null,
@@ -577,7 +578,7 @@ namespace Content.Server.ParticleAccelerator.Components
if (Owner.TryGetComponent(out AppearanceComponent? appearance)) if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{ {
appearance.SetData(ParticleAcceleratorVisuals.VisualState, appearance.SetData(ParticleAcceleratorVisuals.VisualState,
_powerReceiverComponent!.Powered _apcPowerReceiverComponent!.Powered
? (ParticleAcceleratorVisualState) _selectedStrength ? (ParticleAcceleratorVisualState) _selectedStrength
: ParticleAcceleratorVisualState.Unpowered); : ParticleAcceleratorVisualState.Unpowered);
} }
@@ -645,7 +646,7 @@ namespace Content.Server.ParticleAccelerator.Components
} * _powerDrawMult + _powerDrawBase; } * _powerDrawMult + _powerDrawBase;
} }
public void PowerBoxReceivedChanged(object? sender, ReceivedPowerChangedEventArgs eventArgs) public void PowerBoxReceivedChanged(PowerConsumerReceivedChanged eventArgs)
{ {
DebugTools.Assert(_isAssembled); DebugTools.Assert(_isAssembled);

View File

@@ -17,12 +17,6 @@ namespace Content.Server.ParticleAccelerator.Components
base.Initialize(); base.Initialize();
PowerConsumerComponent = Owner.EnsureComponentWarn<PowerConsumerComponent>(); PowerConsumerComponent = Owner.EnsureComponentWarn<PowerConsumerComponent>();
PowerConsumerComponent.OnReceivedPowerChanged += PowerReceivedChanged;
}
private void PowerReceivedChanged(object? sender, ReceivedPowerChangedEventArgs e)
{
Master?.PowerBoxReceivedChanged(sender, e);
} }
} }
} }

View File

@@ -3,7 +3,7 @@ using Content.Server.ParticleAccelerator.Components;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
namespace Content.Server.ParticleAccelerator namespace Content.Server.ParticleAccelerator.EntitySystems
{ {
[UsedImplicitly] [UsedImplicitly]
public class ParticleAcceleratorPartSystem : EntitySystem public class ParticleAcceleratorPartSystem : EntitySystem

View File

@@ -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<ParticleAcceleratorPowerBoxComponent, PowerConsumerReceivedChanged>(
PowerBoxReceivedChanged);
}
private static void PowerBoxReceivedChanged(
EntityUid uid,
ParticleAcceleratorPowerBoxComponent component,
PowerConsumerReceivedChanged args)
{
component.Master!.PowerBoxReceivedChanged(args);
}
}
}

View File

@@ -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<PowerNetSystem>().GetStatistics();
shell.WriteLine($"networks: {stats.CountNetworks}");
shell.WriteLine($"loads: {stats.CountLoads}");
shell.WriteLine($"supplies: {stats.CountSupplies}");
shell.WriteLine($"batteries: {stats.CountBatteries}");
}
}
}

View File

@@ -1,23 +1,22 @@
#nullable enable #nullable enable
using System; using System;
using Content.Server.Access.Components; using Content.Server.Access.Components;
using Content.Server.Battery.Components; using Content.Server.Power.NodeGroups;
using Content.Server.Power.Components;
using Content.Server.UserInterface; using Content.Server.UserInterface;
using Content.Shared.APC; using Content.Shared.APC;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Notification.Managers; using Content.Shared.Notification.Managers;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Server.APC.Components namespace Content.Server.Power.Components
{ {
[RegisterComponent] [RegisterComponent]
[ComponentReference(typeof(IActivate))] [ComponentReference(typeof(IActivate))]
@@ -57,7 +56,6 @@ namespace Content.Server.APC.Components
{ {
base.Initialize(); base.Initialize();
Owner.EnsureComponent<PowerConsumerComponent>();
Owner.EnsureComponentWarn<ServerUserInterfaceComponent>(); Owner.EnsureComponentWarn<ServerUserInterfaceComponent>();
Owner.EnsureComponentWarn<AccessReader>(); Owner.EnsureComponentWarn<AccessReader>();
@@ -89,6 +87,8 @@ namespace Content.Server.APC.Components
if (_accessReader == null || _accessReader.IsAllowed(user)) if (_accessReader == null || _accessReader.IsAllowed(user))
{ {
MainBreakerEnabled = !MainBreakerEnabled; MainBreakerEnabled = !MainBreakerEnabled;
Owner.GetComponent<PowerNetworkBatteryComponent>().CanDischarge = MainBreakerEnabled;
_uiDirty = true; _uiDirty = true;
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f)); 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; return ApcChargeState.Full;
} }
if (!Owner.TryGetComponent(out PowerConsumerComponent? consumer)) var netBattery = Owner.GetComponent<PowerNetworkBatteryComponent>();
{ var delta = netBattery.CurrentSupply - netBattery.CurrentReceiving;
return ApcChargeState.Full;
}
if (consumer.DrawRate == consumer.ReceivedPower) return delta < 0 ? ApcChargeState.Charging : ApcChargeState.Lack;
{
return ApcChargeState.Charging;
}
else
{
return ApcChargeState.Lack;
}
} }
private ApcExternalPowerState CalcExtPowerState() private ApcExternalPowerState CalcExtPowerState()
{ {
if (!Owner.TryGetComponent(out BatteryStorageComponent? batteryStorage)) var bat = Battery;
if (bat == null)
return ApcExternalPowerState.None;
var netBat = Owner.GetComponent<PowerNetworkBatteryComponent>();
if (netBat.CurrentReceiving == 0 && netBat.LoadingNetworkDemand != 0)
{ {
return ApcExternalPowerState.None; return ApcExternalPowerState.None;
} }
var consumer = batteryStorage.Consumer;
if (consumer == null) var delta = netBat.CurrentReceiving - netBat.LoadingNetworkDemand;
return ApcExternalPowerState.None; if (!MathHelper.CloseTo(delta, 0, 0.1f) && delta < 0)
if (consumer.ReceivedPower == 0 && consumer.DrawRate != 0)
{
return ApcExternalPowerState.None;
}
else if (consumer.ReceivedPower < consumer.DrawRate)
{ {
return ApcExternalPowerState.Low; return ApcExternalPowerState.Low;
} }
else
{ return ApcExternalPowerState.Good;
return ApcExternalPowerState.Good;
}
} }
void IActivate.Activate(ActivateEventArgs eventArgs) void IActivate.Activate(ActivateEventArgs eventArgs)

View File

@@ -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;
/// <summary>
/// The max distance this can transmit power to <see cref="ApcPowerReceiverComponent"/>s from.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int PowerTransferRange { get => _powerTransferRange; set => SetPowerTransferRange(value); }
[DataField("powerTransferRange")]
private int _powerTransferRange = 3;
[ViewVariables] public List<ApcPowerReceiverComponent> LinkedReceivers { get; } = new();
/// <summary>
/// If <see cref="ApcPowerReceiverComponent"/>s should consider connecting to this.
/// </summary>
[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<ApcPowerReceiverComponent> FindAvailableReceivers()
{
var nearbyEntities = IoCManager.Resolve<IEntityLookup>()
.GetEntitiesInRange(Owner, PowerTransferRange);
foreach (var entity in nearbyEntities)
{
if (entity.TryGetComponent<ApcPowerReceiverComponent>(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();
}
}
}
}

View File

@@ -1,12 +1,15 @@
#nullable enable #nullable enable
using System; 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.Examine;
using Content.Shared.Power; using Content.Shared.Power;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -15,26 +18,21 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Power.Components namespace Content.Server.Power.Components
{ {
/// <summary> /// <summary>
/// Attempts to link with a nearby <see cref="IPowerProvider"/>s so that it can receive power from a <see cref="IApcNet"/>. /// Attempts to link with a nearby <see cref="ApcPowerProviderComponent"/>s
/// so that it can receive power from a <see cref="IApcNet"/>.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent]
public class PowerReceiverComponent : Component, IExamine public class ApcPowerReceiverComponent : Component, IExamine
{ {
[ViewVariables] [ComponentDependency] private readonly IPhysBody? _physicsComponent = null; [ViewVariables] [ComponentDependency] private readonly IPhysBody? _physicsComponent = null;
public override string Name => "PowerReceiver"; public override string Name => "ApcPowerReceiver";
[ViewVariables] [ViewVariables]
public bool Powered => (HasApcPower || !NeedsPower) && !PowerDisabled; public bool Powered => (MathHelper.CloseTo(NetworkLoad.ReceivingPower, Load) || !NeedsPower) && !PowerDisabled;
/// <summary> /// <summary>
/// If this is being powered by an Apc. /// The max distance from a <see cref="ApcPowerProviderComponent"/> that this can receive power from.
/// </summary>
[ViewVariables]
public bool HasApcPower { get; private set; }
/// <summary>
/// The max distance from a <see cref="PowerProviderComponent"/> that this can receive power from.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public int PowerReceptionRange { get => _powerReceptionRange; set => SetPowerReceptionRange(value); } public int PowerReceptionRange { get => _powerReceptionRange; set => SetPowerReceptionRange(value); }
@@ -42,32 +40,53 @@ namespace Content.Server.Power.Components
private int _powerReceptionRange = 3; private int _powerReceptionRange = 3;
[ViewVariables] [ViewVariables]
public IPowerProvider Provider { get => _provider; set => SetProvider(value); } public ApcPowerProviderComponent? Provider
private IPowerProvider _provider = PowerProviderComponent.NullProvider; {
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;
/// <summary> /// <summary>
/// If this should be considered for connection by <see cref="PowerProviderComponent"/>s. /// If this should be considered for connection by <see cref="ApcPowerProviderComponent"/>s.
/// </summary> /// </summary>
public bool Connectable => Anchored; public bool Connectable => Anchored;
private bool Anchored => _physicsComponent == null || _physicsComponent.BodyType == BodyType.Static; private bool Anchored => _physicsComponent == null || _physicsComponent.BodyType == BodyType.Static;
[ViewVariables] [ViewVariables] public bool NeedsProvider => Provider == null;
public bool NeedsProvider { get; private set; } = true;
/// <summary> /// <summary>
/// Amount of charge this needs from an APC per second to function. /// Amount of charge this needs from an APC per second to function.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public int Load { get => _load; set => SetLoad(value); }
[DataField("powerLoad")] [DataField("powerLoad")]
private int _load = 5; public float Load { get => NetworkLoad.DesiredPower; set => NetworkLoad.DesiredPower = value; }
/// <summary> /// <summary>
/// When false, causes this to appear powered even if not receiving power from an Apc. /// When false, causes this to appear powered even if not receiving power from an Apc.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [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")] [DataField("needsPower")]
private bool _needsPower = true; private bool _needsPower = true;
@@ -75,9 +94,16 @@ namespace Content.Server.Power.Components
/// When true, causes this to never appear powered. /// When true, causes this to never appear powered.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public bool PowerDisabled { get => _powerDisabled; set => SetPowerDisabled(value); }
[DataField("powerDisabled")] [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() protected override void Startup()
{ {
@@ -94,7 +120,8 @@ namespace Content.Server.Power.Components
protected override void OnRemove() protected override void OnRemove()
{ {
_provider.RemoveReceiver(this); _provider?.RemoveReceiver(this);
base.OnRemove(); base.OnRemove();
} }
@@ -108,20 +135,17 @@ namespace Content.Server.Power.Components
public void ApcPowerChanged() public void ApcPowerChanged()
{ {
var oldPowered = Powered; OnNewPowerState();
HasApcPower = Provider.HasApcPower;
if (Powered != oldPowered)
OnNewPowerState();
} }
private bool TryFindAvailableProvider(out IPowerProvider foundProvider) private bool TryFindAvailableProvider([NotNullWhen(true)] out ApcPowerProviderComponent? foundProvider)
{ {
var nearbyEntities = IoCManager.Resolve<IEntityLookup>() var nearbyEntities = IoCManager.Resolve<IEntityLookup>()
.GetEntitiesInRange(Owner, PowerReceptionRange); .GetEntitiesInRange(Owner, PowerReceptionRange);
foreach (var entity in nearbyEntities) foreach (var entity in nearbyEntities)
{ {
if (entity.TryGetComponent<PowerProviderComponent>(out var provider)) if (entity.TryGetComponent<ApcPowerProviderComponent>(out var provider))
{ {
if (provider.Connectable) if (provider.Connectable)
{ {
@@ -136,60 +160,18 @@ namespace Content.Server.Power.Components
} }
} }
} }
foundProvider = default!;
foundProvider = default;
return false; 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) private void SetPowerReceptionRange(int newPowerReceptionRange)
{ {
ClearProvider(); Provider = null;
_powerReceptionRange = newPowerReceptionRange; _powerReceptionRange = newPowerReceptionRange;
TryFindAndSetProvider(); 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() private void OnNewPowerState()
{ {
SendMessage(new PowerChangedMessage(Powered)); SendMessage(new PowerChangedMessage(Powered));
@@ -211,7 +193,7 @@ namespace Content.Server.Power.Components
} }
else else
{ {
ClearProvider(); Provider = null;
} }
} }

View File

@@ -0,0 +1,9 @@
#nullable enable
using Content.Server.Power.NodeGroups;
namespace Content.Server.Power.Components
{
public abstract class BaseApcNetComponent : BaseNetConnectorComponent<IApcNet>
{
}
}

View File

@@ -1,7 +1,6 @@
#nullable enable #nullable enable
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Battery.Components;
using Content.Server.Hands.Components; using Content.Server.Hands.Components;
using Content.Server.Items; using Content.Server.Items;
using Content.Server.Weapon.Ranged.Barrels.Components; using Content.Server.Weapon.Ranged.Barrels.Components;
@@ -45,7 +44,7 @@ namespace Content.Server.Power.Components
{ {
base.Initialize(); base.Initialize();
Owner.EnsureComponent<PowerReceiverComponent>(); Owner.EnsureComponent<ApcPowerReceiverComponent>();
_container = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-powerCellContainer"); _container = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-powerCellContainer");
// Default state in the visualizer is OFF, so when this gets powered on during initialization it will generally show empty // 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() private CellChargerStatus GetStatus()
{ {
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) && if (Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) &&
!receiver.Powered) !receiver.Powered)
{ {
return CellChargerStatus.Off; return CellChargerStatus.Off;
@@ -234,7 +233,7 @@ namespace Content.Server.Power.Components
// Not called UpdateAppearance just because it messes with the load // Not called UpdateAppearance just because it messes with the load
var status = GetStatus(); var status = GetStatus();
if (_status == status || if (_status == status ||
!Owner.TryGetComponent(out PowerReceiverComponent? receiver)) !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{ {
return; return;
} }
@@ -279,7 +278,7 @@ namespace Content.Server.Power.Components
private void TransferPower(float frameTime) private void TransferPower(float frameTime)
{ {
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) && if (Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) &&
!receiver.Powered) !receiver.Powered)
{ {
return; return;

View File

@@ -17,23 +17,18 @@ namespace Content.Server.Power.Components
private Voltage _voltage = Voltage.High; private Voltage _voltage = Voltage.High;
[ViewVariables] [ViewVariables]
public TNetType Net { get => _net; set => SetNet(value); } public TNetType? Net { get => _net; set => SetNet(value); }
private TNetType _net = default!; //set in OnAdd() private TNetType? _net;
protected abstract TNetType NullNet { get; }
[ViewVariables] [ViewVariables]
private bool _needsNet = true; private bool _needsNet => _net != null;
protected override void OnAdd() [DataField("node")] [ViewVariables] public string? NodeId;
{
base.OnAdd();
_net = NullNet;
}
protected override void Initialize() protected override void Initialize()
{ {
base.Initialize(); base.Initialize();
if (_needsNet) if (_needsNet)
{ {
TryFindAndSetNet(); TryFindAndSetNet();
@@ -56,9 +51,8 @@ namespace Content.Server.Power.Components
public void ClearNet() public void ClearNet()
{ {
RemoveSelfFromNet(_net); if (_net != null)
_net = NullNet; RemoveSelfFromNet(_net);
_needsNet = true;
} }
protected abstract void AddSelfToNet(TNetType net); protected abstract void AddSelfToNet(TNetType net);
@@ -70,7 +64,7 @@ namespace Content.Server.Power.Components
if (Owner.TryGetComponent<NodeContainerComponent>(out var container)) if (Owner.TryGetComponent<NodeContainerComponent>(out var container))
{ {
var compatibleNet = container.Nodes.Values 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) .Select(node => node.NodeGroup)
.OfType<TNetType>() .OfType<TNetType>()
.FirstOrDefault(); .FirstOrDefault();
@@ -85,12 +79,15 @@ namespace Content.Server.Power.Components
return false; return false;
} }
private void SetNet(TNetType newNet) private void SetNet(TNetType? newNet)
{ {
RemoveSelfFromNet(_net); if (_net != null)
AddSelfToNet(newNet); RemoveSelfFromNet(_net);
if (newNet != null)
AddSelfToNet(newNet);
_net = newNet; _net = newNet;
_needsNet = false;
} }
private void SetVoltage(Voltage newVoltage) private void SetVoltage(Voltage newVoltage)

View File

@@ -1,10 +1,10 @@
#nullable enable #nullable enable
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Power.NodeGroups;
namespace Content.Server.Power.Components namespace Content.Server.Power.Components
{ {
public abstract class BasePowerNetComponent : BaseNetConnectorComponent<IPowerNet> public abstract class BasePowerNetComponent : BaseNetConnectorComponent<IPowerNet>
{ {
protected override IPowerNet NullNet => PowerNetNodeGroup.NullNet;
} }
} }

View File

@@ -0,0 +1,24 @@
using Content.Server.Power.NodeGroups;
using Robust.Shared.GameObjects;
namespace Content.Server.Power.Components
{
/// <summary>
/// Connects the loading side of a <see cref="BatteryComponent"/> to a non-APC power network.
/// </summary>
[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);
}
}
}

Some files were not shown because too many files have changed in this diff Show More