From dde01f746f81e3381f3a16f2eae560d699ffe3f7 Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Mon, 22 Dec 2025 08:43:02 -0800 Subject: [PATCH] Basic Dynamic Power Consumption Systems (#41885) * init commit * Addr reviews --- .../Tests/Power/PowerStateTest.cs | 186 ++++++++++++++++++ .../Power/Components/PowerStateComponent.cs | 32 +++ .../Power/Components/UIPowerStateComponent.cs | 16 ++ .../Power/EntitySystems/PowerStateSystem.cs | 44 +++++ .../Power/EntitySystems/UIPowerStateSystem.cs | 46 +++++ 5 files changed, 324 insertions(+) create mode 100644 Content.IntegrationTests/Tests/Power/PowerStateTest.cs create mode 100644 Content.Shared/Power/Components/PowerStateComponent.cs create mode 100644 Content.Shared/Power/Components/UIPowerStateComponent.cs create mode 100644 Content.Shared/Power/EntitySystems/PowerStateSystem.cs create mode 100644 Content.Shared/Power/EntitySystems/UIPowerStateSystem.cs 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); + } +} -- 2.52.0