]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Basic Dynamic Power Consumption Systems (#41885)
authorArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Mon, 22 Dec 2025 16:43:02 +0000 (08:43 -0800)
committerGitHub <noreply@github.com>
Mon, 22 Dec 2025 16:43:02 +0000 (16:43 +0000)
* init commit

* Addr reviews

Content.IntegrationTests/Tests/Power/PowerStateTest.cs [new file with mode: 0644]
Content.Shared/Power/Components/PowerStateComponent.cs [new file with mode: 0644]
Content.Shared/Power/Components/UIPowerStateComponent.cs [new file with mode: 0644]
Content.Shared/Power/EntitySystems/PowerStateSystem.cs [new file with mode: 0644]
Content.Shared/Power/EntitySystems/UIPowerStateSystem.cs [new file with mode: 0644]

diff --git a/Content.IntegrationTests/Tests/Power/PowerStateTest.cs b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs
new file mode 100644 (file)
index 0000000..dec3982
--- /dev/null
@@ -0,0 +1,186 @@
+using Content.Shared.Coordinates;
+using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+
+namespace Content.IntegrationTests.Tests.Power;
+
+[TestFixture]
+public sealed class PowerStateTest
+{
+    [TestPrototypes]
+    private const string Prototypes = @"
+- type: entity
+  id: PowerStateApcReceiverDummy
+  components:
+  - type: ApcPowerReceiver
+  - type: ExtensionCableReceiver
+  - type: Transform
+    anchored: true
+  - type: PowerState
+    isWorking: false
+    idlePowerDraw: 10
+    workingPowerDraw: 50
+";
+
+    /// <summary>
+    /// Asserts that switching from idle to working updates the power receiver load to the working draw.
+    /// </summary>
+    [Test]
+    public async Task SetWorkingState_IdleToWorking_UpdatesLoad()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        var mapManager = server.ResolveDependency<IMapManager>();
+        var entManager = server.ResolveDependency<IEntityManager>();
+        var mapSys = entManager.System<SharedMapSystem>();
+
+        await server.WaitAssertion(() =>
+        {
+            mapSys.CreateMap(out var mapId);
+            var grid = mapManager.CreateGridEntity(mapId);
+
+            mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
+
+            var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
+
+            var receiver = entManager.GetComponent<Server.Power.Components.ApcPowerReceiverComponent>(ent);
+            var powerState = entManager.GetComponent<PowerStateComponent>(ent);
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(powerState.IsWorking, Is.False);
+                Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+            });
+
+            var system = entManager.System<PowerStateSystem>();
+            system.SetWorkingState((ent, powerState), true);
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(powerState.IsWorking, Is.True);
+                Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+            });
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    /// <summary>
+    /// Asserts that switching from working to idle updates the power receiver load to the idle draw.
+    /// </summary>
+    [Test]
+    public async Task SetWorkingState_WorkingToIdle_UpdatesLoad()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        var mapManager = server.ResolveDependency<IMapManager>();
+        var entManager = server.ResolveDependency<IEntityManager>();
+        var mapSys = entManager.System<SharedMapSystem>();
+
+        await server.WaitAssertion(() =>
+        {
+            mapSys.CreateMap(out var mapId);
+            var grid = mapManager.CreateGridEntity(mapId);
+
+            mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
+
+            var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
+
+            var receiver = entManager.GetComponent<Server.Power.Components.ApcPowerReceiverComponent>(ent);
+            var powerState = entManager.GetComponent<PowerStateComponent>(ent);
+            var system = entManager.System<PowerStateSystem>();
+            Entity<PowerStateComponent> newEnt = (ent, powerState);
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(powerState.IsWorking, Is.False);
+                Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+            });
+
+            system.SetWorkingState(newEnt, true);
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(powerState.IsWorking, Is.True);
+                Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+            });
+
+            system.SetWorkingState(newEnt, false);
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(powerState.IsWorking, Is.False);
+                Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+            });
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    /// <summary>
+    /// Asserts that setting the working state to the current state does not change the power receiver load.
+    /// </summary>
+    [Test]
+    public async Task SetWorkingState_AlreadyInState_NoChange()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        var mapManager = server.ResolveDependency<IMapManager>();
+        var entManager = server.ResolveDependency<IEntityManager>();
+        var mapSys = entManager.System<SharedMapSystem>();
+
+        await server.WaitAssertion(() =>
+        {
+            mapSys.CreateMap(out var mapId);
+            var grid = mapManager.CreateGridEntity(mapId);
+
+            mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
+
+            var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
+
+            var receiver = entManager.GetComponent<Server.Power.Components.ApcPowerReceiverComponent>(ent);
+            var powerState = entManager.GetComponent<PowerStateComponent>(ent);
+            var system = entManager.System<PowerStateSystem>();
+            Entity<PowerStateComponent> valueTuple = (ent, powerState);
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(powerState.IsWorking, Is.False);
+                Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+            });
+
+            system.SetWorkingState(valueTuple, false);
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(powerState.IsWorking, Is.False);
+                Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+            });
+
+            system.SetWorkingState(valueTuple, true);
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(powerState.IsWorking, Is.True);
+                Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+            });
+
+            system.SetWorkingState(valueTuple, true);
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(powerState.IsWorking, Is.True);
+                Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
+            });
+        });
+
+        await pair.CleanReturnAsync();
+    }
+}
+
diff --git a/Content.Shared/Power/Components/PowerStateComponent.cs b/Content.Shared/Power/Components/PowerStateComponent.cs
new file mode 100644 (file)
index 0000000..d1fc0d2
--- /dev/null
@@ -0,0 +1,32 @@
+namespace Content.Shared.Power.Components;
+
+/// <summary>
+/// Generic component for giving entities "idle" and "working" power states.
+/// </summary>
+/// <remarks><para>Entities that have more complex power draw
+/// (ex. a thermomachine whose heating power is directly tied to its power consumption)
+/// should just directly set their load on the <see cref="SharedApcPowerReceiverComponent"/>.</para>
+///
+/// <para>This is also applicable if you would like to add
+/// more complex power behavior that is tied to a generic component.</para></remarks>
+[RegisterComponent]
+public sealed partial class PowerStateComponent : Component
+{
+    /// <summary>
+    /// Whether the entity is currently in the working state.
+    /// </summary>
+    [DataField]
+    public bool IsWorking;
+
+    /// <summary>
+    /// The idle power draw of this entity when not working, in watts.
+    /// </summary>
+    [DataField]
+    public float IdlePowerDraw = 20f;
+
+    /// <summary>
+    /// The working power draw of this entity when working, in watts.
+    /// </summary>
+    [DataField]
+    public float WorkingPowerDraw = 350f;
+}
diff --git a/Content.Shared/Power/Components/UIPowerStateComponent.cs b/Content.Shared/Power/Components/UIPowerStateComponent.cs
new file mode 100644 (file)
index 0000000..9210f63
--- /dev/null
@@ -0,0 +1,16 @@
+namespace Content.Shared.Power.Components;
+
+/// <summary>
+/// Component for entities that want to increase their power usage to a working state when
+/// a UI on the machine is open. Requires <see cref="PowerStateComponent"/>.
+/// </summary>
+[RegisterComponent]
+public sealed partial class UIPowerStateComponent : Component
+{
+    /// <summary>
+    /// List of UI keys that will trigger the working state.
+    /// If null, any UI open will trigger the working state.
+    /// </summary>
+    [DataField]
+    public List<Enum>? Keys;
+}
diff --git a/Content.Shared/Power/EntitySystems/PowerStateSystem.cs b/Content.Shared/Power/EntitySystems/PowerStateSystem.cs
new file mode 100644 (file)
index 0000000..dd47708
--- /dev/null
@@ -0,0 +1,44 @@
+using Content.Shared.Power.Components;
+using JetBrains.Annotations;
+
+namespace Content.Shared.Power.EntitySystems;
+
+/// <summary>
+/// Generic system that handles entities with <see cref="PowerStateComponent"/>.
+/// Used for simple machines that only need to switch between "idle" and "working" power states.
+/// </summary>
+public sealed class PowerStateSystem : EntitySystem
+{
+    [Dependency] private readonly SharedPowerReceiverSystem _powerReceiverSystem = default!;
+
+    private EntityQuery<PowerStateComponent> _powerStateQuery;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<PowerStateComponent, ComponentStartup>(OnComponentStartup);
+
+        _powerStateQuery = GetEntityQuery<PowerStateComponent>();
+    }
+
+    private void OnComponentStartup(Entity<PowerStateComponent> ent, ref ComponentStartup args)
+    {
+        SetWorkingState(ent.Owner, ent.Comp.IsWorking);
+    }
+
+    /// <summary>
+    /// Sets the working state of the entity, adjusting its power draw accordingly.
+    /// </summary>
+    /// <param name="ent">The entity to set the working state for.</param>
+    /// <param name="working">Whether the entity should be in the working state.</param>
+    [PublicAPI]
+    public void SetWorkingState(Entity<PowerStateComponent?> ent, bool working)
+    {
+        if (!_powerStateQuery.Resolve(ent, ref ent.Comp))
+            return;
+
+        _powerReceiverSystem.SetLoad(ent.Owner, working ? ent.Comp.WorkingPowerDraw : ent.Comp.IdlePowerDraw);
+        ent.Comp.IsWorking = working;
+    }
+}
diff --git a/Content.Shared/Power/EntitySystems/UIPowerStateSystem.cs b/Content.Shared/Power/EntitySystems/UIPowerStateSystem.cs
new file mode 100644 (file)
index 0000000..bf2d08a
--- /dev/null
@@ -0,0 +1,46 @@
+using Content.Shared.Power.Components;
+
+namespace Content.Shared.Power.EntitySystems;
+
+/// <summary>
+/// System for entities with <see cref="UIPowerStateComponent"/>.
+/// Entities with this component will increase their power usage to a working state
+/// when a UI on the entity is open.
+/// </summary>
+public sealed class UIPowerStateSystem : EntitySystem
+{
+    [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+    [Dependency] private readonly PowerStateSystem _powerState = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<UIPowerStateComponent, BoundUIOpenedEvent>(OnUiOpened);
+        SubscribeLocalEvent<UIPowerStateComponent, BoundUIClosedEvent>(OnUiClosed);
+    }
+
+    private void OnUiClosed(Entity<UIPowerStateComponent> ent, ref BoundUIClosedEvent args)
+    {
+        if (ent.Comp.Keys is null)
+        {
+            if (_ui.IsAnyUiOpen(ent.Owner))
+                return;
+        }
+        else
+        {
+            if (_ui.IsUiOpen(ent.Owner, ent.Comp.Keys))
+                return;
+        }
+
+        _powerState.SetWorkingState(ent.Owner, false);
+    }
+
+    private void OnUiOpened(Entity<UIPowerStateComponent> ent, ref BoundUIOpenedEvent args)
+    {
+        if (ent.Comp.Keys is not null && !ent.Comp.Keys.Contains(args.UiKey))
+            return;
+
+        _powerState.SetWorkingState(ent.Owner, true);
+    }
+}