From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Date: Mon, 22 Dec 2025 16:43:02 +0000 (-0800)
Subject: Basic Dynamic Power Consumption Systems (#41885)
X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=dde01f746f81e3381f3a16f2eae560d699ffe3f7;p=space-station-14.git
Basic Dynamic Power Consumption Systems (#41885)
* init commit
* Addr reviews
---
diff --git a/Content.IntegrationTests/Tests/Power/PowerStateTest.cs b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs
new file mode 100644
index 0000000000..dec398212d
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Power/PowerStateTest.cs
@@ -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
+";
+
+ ///
+ /// Asserts that switching from idle to working updates the power receiver load to the working draw.
+ ///
+ [Test]
+ public async Task SetWorkingState_IdleToWorking_UpdatesLoad()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var mapManager = server.ResolveDependency();
+ var entManager = server.ResolveDependency();
+ var mapSys = entManager.System();
+
+ 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(ent);
+ var powerState = entManager.GetComponent(ent);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(powerState.IsWorking, Is.False);
+ Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
+ });
+
+ var system = entManager.System();
+ 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();
+ }
+
+ ///
+ /// Asserts that switching from working to idle updates the power receiver load to the idle draw.
+ ///
+ [Test]
+ public async Task SetWorkingState_WorkingToIdle_UpdatesLoad()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var mapManager = server.ResolveDependency();
+ var entManager = server.ResolveDependency();
+ var mapSys = entManager.System();
+
+ 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(ent);
+ var powerState = entManager.GetComponent(ent);
+ var system = entManager.System();
+ Entity 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();
+ }
+
+ ///
+ /// Asserts that setting the working state to the current state does not change the power receiver load.
+ ///
+ [Test]
+ public async Task SetWorkingState_AlreadyInState_NoChange()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var mapManager = server.ResolveDependency();
+ var entManager = server.ResolveDependency();
+ var mapSys = entManager.System();
+
+ 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(ent);
+ var powerState = entManager.GetComponent(ent);
+ var system = entManager.System();
+ Entity 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
index 0000000000..d1fc0d2aeb
--- /dev/null
+++ b/Content.Shared/Power/Components/PowerStateComponent.cs
@@ -0,0 +1,32 @@
+namespace Content.Shared.Power.Components;
+
+///
+/// Generic component for giving entities "idle" and "working" power states.
+///
+/// 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 .
+///
+/// This is also applicable if you would like to add
+/// more complex power behavior that is tied to a generic component.
+[RegisterComponent]
+public sealed partial class PowerStateComponent : Component
+{
+ ///
+ /// Whether the entity is currently in the working state.
+ ///
+ [DataField]
+ public bool IsWorking;
+
+ ///
+ /// The idle power draw of this entity when not working, in watts.
+ ///
+ [DataField]
+ public float IdlePowerDraw = 20f;
+
+ ///
+ /// The working power draw of this entity when working, in watts.
+ ///
+ [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
index 0000000000..9210f63c55
--- /dev/null
+++ b/Content.Shared/Power/Components/UIPowerStateComponent.cs
@@ -0,0 +1,16 @@
+namespace Content.Shared.Power.Components;
+
+///
+/// Component for entities that want to increase their power usage to a working state when
+/// a UI on the machine is open. Requires .
+///
+[RegisterComponent]
+public sealed partial class UIPowerStateComponent : Component
+{
+ ///
+ /// List of UI keys that will trigger the working state.
+ /// If null, any UI open will trigger the working state.
+ ///
+ [DataField]
+ public List? Keys;
+}
diff --git a/Content.Shared/Power/EntitySystems/PowerStateSystem.cs b/Content.Shared/Power/EntitySystems/PowerStateSystem.cs
new file mode 100644
index 0000000000..dd47708d2d
--- /dev/null
+++ b/Content.Shared/Power/EntitySystems/PowerStateSystem.cs
@@ -0,0 +1,44 @@
+using Content.Shared.Power.Components;
+using JetBrains.Annotations;
+
+namespace Content.Shared.Power.EntitySystems;
+
+///
+/// Generic system that handles entities with .
+/// Used for simple machines that only need to switch between "idle" and "working" power states.
+///
+public sealed class PowerStateSystem : EntitySystem
+{
+ [Dependency] private readonly SharedPowerReceiverSystem _powerReceiverSystem = default!;
+
+ private EntityQuery _powerStateQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnComponentStartup);
+
+ _powerStateQuery = GetEntityQuery();
+ }
+
+ private void OnComponentStartup(Entity ent, ref ComponentStartup args)
+ {
+ SetWorkingState(ent.Owner, ent.Comp.IsWorking);
+ }
+
+ ///
+ /// Sets the working state of the entity, adjusting its power draw accordingly.
+ ///
+ /// The entity to set the working state for.
+ /// Whether the entity should be in the working state.
+ [PublicAPI]
+ public void SetWorkingState(Entity 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
index 0000000000..bf2d08adbf
--- /dev/null
+++ b/Content.Shared/Power/EntitySystems/UIPowerStateSystem.cs
@@ -0,0 +1,46 @@
+using Content.Shared.Power.Components;
+
+namespace Content.Shared.Power.EntitySystems;
+
+///
+/// System for entities with .
+/// Entities with this component will increase their power usage to a working state
+/// when a UI on the entity is open.
+///
+public sealed class UIPowerStateSystem : EntitySystem
+{
+ [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+ [Dependency] private readonly PowerStateSystem _powerState = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnUiOpened);
+ SubscribeLocalEvent(OnUiClosed);
+ }
+
+ private void OnUiClosed(Entity 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 ent, ref BoundUIOpenedEvent args)
+ {
+ if (ent.Comp.Keys is not null && !ent.Comp.Keys.Contains(args.UiKey))
+ return;
+
+ _powerState.SetWorkingState(ent.Owner, true);
+ }
+}