--- /dev/null
+using Content.Shared.Power.EntitySystems;
+
+namespace Content.Client.Power.EntitySystems;
+
+/// <inheritdoc/>
+public sealed class PowerStateSystem : SharedPowerStateSystem;
--- /dev/null
+using System.Linq;
+using Content.Server.Power.Components;
+using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Power;
+
+[TestFixture, TestOf(typeof(SharedPowerStateSystem))]
+public sealed class PowerStatePrototypeTest
+{
+ /// <summary>
+ /// Asserts that the <see cref="SharedApcPowerReceiverComponent"/>'s load is the same
+ /// as the idle or working power draw from <see cref="PowerStateComponent"/>,
+ /// depending on the current power state.
+ /// </summary>
+ [Test]
+ public async Task AssertApcPowerMatchesPowerState()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var protoMan = server.ResolveDependency<IPrototypeManager>();
+ var entMan = server.ResolveDependency<IEntityManager>();
+
+ await server.WaitAssertion(() =>
+ {
+ Assert.Multiple(delegate
+ {
+ foreach (var prototype in protoMan.EnumeratePrototypes<EntityPrototype>()
+ .Where(p => !p.Abstract)
+ .Where(p => !pair.IsTestPrototype(p)))
+ {
+ if (!prototype.TryGetComponent<PowerStateComponent>(out var powerStateComp, entMan.ComponentFactory))
+ continue;
+
+ // LESSON LEARNED:
+ // ENSURE THAT THE COMPONENT YOU ARE TRYING TO GET IS THE SERVER-SIDE VARIANT
+ if (!prototype.TryGetComponent<ApcPowerReceiverComponent>(out var powerReceiverComp, entMan.ComponentFactory))
+ {
+ Assert.Fail(
+ $"Entity prototype '{prototype.ID}' has a PowerStateComponent but is missing the required ApcPowerReceiverComponent.");
+ }
+
+ var expectedLoad = powerStateComp.IsWorking
+ ? powerStateComp.WorkingPowerDraw
+ : powerStateComp.IdlePowerDraw;
+
+ Assert.That(powerReceiverComp.Load,
+ Is.EqualTo(expectedLoad),
+ $"Entity prototype '{prototype.ID}' has mismatched power draw between PowerStateComponent and SharedApcPowerReceiverComponent.");
+ }
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+}
Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
});
- var system = entManager.System<PowerStateSystem>();
+ var system = entManager.System<SharedPowerStateSystem>();
system.SetWorkingState((ent, powerState), true);
Assert.Multiple(() =>
var receiver = entManager.GetComponent<Server.Power.Components.ApcPowerReceiverComponent>(ent);
var powerState = entManager.GetComponent<PowerStateComponent>(ent);
- var system = entManager.System<PowerStateSystem>();
+ var system = entManager.System<SharedPowerStateSystem>();
Entity<PowerStateComponent> newEnt = (ent, powerState);
Assert.Multiple(() =>
var receiver = entManager.GetComponent<Server.Power.Components.ApcPowerReceiverComponent>(ent);
var powerState = entManager.GetComponent<PowerStateComponent>(ent);
- var system = entManager.System<PowerStateSystem>();
+ var system = entManager.System<SharedPowerStateSystem>();
Entity<PowerStateComponent> valueTuple = (ent, powerState);
Assert.Multiple(() =>
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Linq;
+using Content.Shared.Power.EntitySystems;
namespace Content.Server.Holopad;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
+ [Dependency] private readonly SharedPowerStateSystem _powerState = default!;
private float _updateTimer = 1.0f;
private const float UpdateTime = 1.0f;
{
_telephoneSystem.SetSpeakerForTelephone((entity, entityTelephone), (hologramUid, hologramSpeech));
}
+
+ _powerState.SetWorkingState(entity.Owner, true);
}
private void DeleteHologram(Entity<HolopadHologramComponent> hologram, Entity<HolopadComponent> attachedHolopad)
{
+ _powerState.SetWorkingState(attachedHolopad.Owner, false);
+
attachedHolopad.Comp.Hologram = null;
QueueDel(hologram);
using Content.Server.Construction.Components;
using Content.Shared.Chat;
using Content.Shared.Damage.Components;
+using Content.Shared.Power.EntitySystems;
using Content.Shared.Temperature.Components;
namespace Content.Server.Kitchen.EntitySystems
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
+ [Dependency] private readonly SharedPowerStateSystem _powerState = default!;
private static readonly EntProtoId MalfunctionSpark = "Spark";
microwaveComponent.PlayingStream =
_audio.PlayPvs(microwaveComponent.LoopingSound, ent, AudioParams.Default.WithLoop(true).WithMaxDistance(5))?.Entity;
+ _powerState.SetWorkingState(ent.Owner, true);
}
private void OnCookStop(Entity<ActiveMicrowaveComponent> ent, ref ComponentShutdown args)
SetAppearance(ent.Owner, MicrowaveVisualState.Idle, microwaveComponent);
microwaveComponent.PlayingStream = _audio.Stop(microwaveComponent.PlayingStream);
+ _powerState.SetWorkingState(ent.Owner, false);
}
private void OnActiveMicrowaveInsert(Entity<ActiveMicrowaveComponent> ent, ref EntInsertedIntoContainerMessage args)
using Content.Shared.Jittering;
using Content.Shared.Kitchen.EntitySystems;
using Content.Shared.Power;
+using Content.Shared.Power.EntitySystems;
namespace Content.Server.Kitchen.EntitySystems
{
[Dependency] private readonly SharedDestructibleSystem _destructible = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
[Dependency] private readonly JitteringSystem _jitter = default!;
+ [Dependency] private readonly SharedPowerStateSystem _powerState = default!;
public override void Initialize()
{
private void OnActiveGrinderStart(Entity<ActiveReagentGrinderComponent> ent, ref ComponentStartup args)
{
_jitter.AddJitter(ent, -10, 100);
+
+ // Not all grinders need power.
+ _powerState.TrySetWorkingState(ent.Owner, true);
}
private void OnActiveGrinderRemove(Entity<ActiveReagentGrinderComponent> ent, ref ComponentRemove args)
{
RemComp<JitteringComponent>(ent);
+ _powerState.TrySetWorkingState(ent.Owner, false);
}
private void OnEntRemoveAttempt(Entity<ReagentGrinderComponent> entity, ref ContainerIsRemovingAttemptEvent args)
--- /dev/null
+using Content.Server.Lathe.Components;
+using Content.Shared.Power.EntitySystems;
+
+namespace Content.Server.Lathe;
+
+/// <summary>
+/// System for handling lathes that are actively producing items.
+/// The component is used more so as a marker for EntityQueryEnumerator,
+/// however it's also used to set the power state of the lathe when producing.
+/// </summary>
+public sealed class LatheProducingSystem : EntitySystem
+{
+ [Dependency] private readonly SharedPowerStateSystem _powerState = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<LatheProducingComponent, ComponentStartup>(OnComponentStartup);
+ SubscribeLocalEvent<LatheProducingComponent, ComponentShutdown>(OnComponentShutdown);
+ }
+
+ private void OnComponentShutdown(Entity<LatheProducingComponent> ent, ref ComponentShutdown args)
+ {
+ // use the Try variant of this here
+ // or else you get trolled by AllComponentsOneToOneDeleteTest
+ _powerState.TrySetWorkingState(ent.Owner, false);
+ }
+
+ private void OnComponentStartup(Entity<LatheProducingComponent> ent, ref ComponentStartup args)
+ {
+ _powerState.TrySetWorkingState(ent.Owner, true);
+ }
+}
--- /dev/null
+using Content.Server.Power.Components;
+using Content.Shared.Power.Components;
+using Content.Shared.Power.EntitySystems;
+
+namespace Content.Server.Power.EntitySystems;
+
+public sealed class PowerStateSystem : SharedPowerStateSystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<PowerStateComponent, ComponentStartup>(OnComponentStartup);
+ }
+
+ private void OnComponentStartup(Entity<PowerStateComponent> ent, ref ComponentStartup args)
+ {
+ EnsureComp<ApcPowerReceiverComponent>(ent);
+ SetWorkingState(ent.Owner, ent.Comp.IsWorking);
+ }
+}
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Interaction;
using Content.Shared.Popups;
+using Content.Shared.Power.EntitySystems;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Network;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
+ [Dependency] private readonly SharedPowerStateSystem _powerState = default!;
/// <inheritdoc/>
public override void Initialize()
comp.MixingSoundEntity = _audio.PlayPvs(comp.MixingSound, entity, comp.MixingSound?.Params.WithLoop(true));
comp.MixTimeEnd = _timing.CurTime + comp.MixDuration;
_appearance.SetData(entity, SolutionContainerMixerVisuals.Mixing, true);
+ _powerState.SetWorkingState(entity.Owner, true);
Dirty(uid, comp);
}
_appearance.SetData(entity, SolutionContainerMixerVisuals.Mixing, false);
comp.Mixing = false;
comp.MixingSoundEntity = null;
+ _powerState.SetWorkingState(entity.Owner, false);
Dirty(uid, comp);
}
/// 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
+public abstract class SharedPowerStateSystem : EntitySystem
{
[Dependency] private readonly SharedPowerReceiverSystem _powerReceiverSystem = default!;
{
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>
_powerReceiverSystem.SetLoad(ent.Owner, working ? ent.Comp.WorkingPowerDraw : ent.Comp.IdlePowerDraw);
ent.Comp.IsWorking = working;
}
+
+ /// <summary>
+ /// Tries to set the working state of the entity, adjusting its power draw accordingly.
+ /// Use this for if you're not sure if the entity has a <see cref="PowerStateComponent"/>.
+ /// </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 TrySetWorkingState(Entity<PowerStateComponent?> ent, bool working)
+ {
+ // Sometimes systems calling this API handle generic objects that can or can't consume power,
+ // so to reduce boilerplate we don't log an error. Any entity that *should* have an ApcPowerRecieverComponent
+ // will log an error in tests if someone tries to add an entity that doesn't have one.
+ if (!_powerStateQuery.Resolve(ent, ref ent.Comp, false))
+ return;
+
+ SetWorkingState(ent, working);
+ }
}
public sealed class UIPowerStateSystem : EntitySystem
{
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
- [Dependency] private readonly PowerStateSystem _powerState = default!;
+ [Dependency] private readonly SharedPowerStateSystem _powerState = default!;
public override void Initialize()
{
- type: Godmode
missingComponents:
- ApcPowerReceiver
+ - PowerState
- Anchorable
- Construction
- Destructible
- type: Godmode
missingComponents:
- ApcPowerReceiver
+ - PowerState
- Anchorable
- Construction
- Destructible
- type: Godmode
missingComponents:
- ApcPowerReceiver
+ - PowerState
- Anchorable
- Construction
- Destructible
- type: Godmode
missingComponents:
- ApcPowerReceiver
+ - PowerState
- Anchorable
- Construction
- Destructible
name: arcade
parent: BaseComputer
components:
- - type: ApcPowerReceiver
- powerLoad: 350
- type: ExtensionCableReceiver
- type: PointLight
radius: 1.8
- board
- type: Computer
- type: ApcPowerReceiver
- powerLoad: 200
+ powerLoad: 50
+ - type: PowerState
+ idlePowerDraw: 50
+ workingPowerDraw: 500
+ - type: UIPowerState
- type: ExtensionCableReceiver
- type: ActivatableUIRequiresPower
- type: Sprite
enum.WiresUiKey.Key:
type: WiresBoundUserInterface
- type: ApcPowerReceiver
- powerLoad: 1000
+ powerLoad: 50
+ - type: PowerState
+ idlePowerDraw: 50
+ workingPowerDraw: 1000
- type: Computer
board: ResearchComputerCircuitboard
- type: AccessReader
enum.WiresUiKey.Key:
type: WiresBoundUserInterface
- type: ApcPowerReceiver
- powerLoad: 1000
+ powerLoad: 50
+ - type: PowerState
+ idlePowerDraw: 50
+ workingPowerDraw: 1000
- type: Computer
board: AnalysisComputerCircuitboard
- type: PointLight
name: body scanner computer
description: A body scanner.
components:
- - type: ApcPowerReceiver
- powerLoad: 500
- type: Computer
board: BodyScannerComputerCircuitboard
- type: PointLight
state: generic_keys
- map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
state: generic_panel_open
- - type: ApcPowerReceiver
- powerLoad: 3100 #We want this to fail first so I transferred most of the scanner and pod's power here. (3500 in total)
- type: Computer
board: CloningConsoleComputerCircuitboard
- type: PointLight
enum.WiresUiKey.Key:
type: WiresBoundUserInterface
- type: ApcPowerReceiver
- powerLoad: 1000
+ powerLoad: 5
+ - type: PowerState
+ idlePowerDraw: 5
+ workingPowerDraw: 1000
+ - type: UIPowerState
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: RoboticsConsole
channels:
- Xenoborg
- type: ApcPowerReceiver
- powerLoad: 1000
+ powerLoad: 50
+ - type: PowerState
+ idlePowerDraw: 50
+ workingPowerDraw: 1000
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: Mothership
- map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
state: generic_panel_open
- type: ApcPowerReceiver
- powerLoad: 1000
+ powerLoad: 50
+ - type: PowerState
+ idlePowerDraw: 50
+ workingPowerDraw: 1000
- type: Computer
board: StationAiUploadCircuitboard
- type: AccessReader
True: { visible: false }
False: { visible: true }
- type: ApcPowerReceiver
- powerLoad: 1000
+ powerLoad: 50
+ - type: PowerState
+ idlePowerDraw: 50
+ workingPowerDraw: 1000
- type: Computer
board: StationAiFixerCircuitboard
- type: AccessReader
False: { visible: False }
- type: Machine
board: ElectrolysisUnitMachineCircuitboard
+ - type: ApcPowerReceiver
+ powerLoad: 0
+ - type: PowerState
+ idlePowerDraw: 0
+ workingPowerDraw: 1000 # for a lab-grade machine
# TODO centrifuge should spill the vial if the lid is off
- type: entity
- CentrifugeCompatible
- type: Machine
board: CentrifugeMachineCircuitboard
+ - type: ApcPowerReceiver
+ powerLoad: 0
+ - type: PowerState
+ idlePowerDraw: 0
+ workingPowerDraw: 500
mask:
- Impassable
- type: ApcPowerReceiver
- powerLoad: 300
+ powerLoad: 5
+ - type: PowerState
+ idlePowerDraw: 5
+ workingPowerDraw: 300
- type: StationAiVision
range: 1
needsAnchoring: true
price: 800
- type: ResearchClient
- type: TechnologyDatabase
+ - type: ApcPowerReceiver
+ powerLoad: 150
+ - type: PowerState
+ idlePowerDraw: 150
+ workingPowerDraw: 1000
# a lathe that can be sped up with space lube / slowed down with glue
- type: entity
canCreateVacuum: false
deleteAfterExplosion: false
- type: ApcPowerReceiver
- powerLoad: 400
+ powerLoad: 5
+ - type: PowerState
+ idlePowerDraw: 5
+ workingPowerDraw: 1000
- type: Machine
board: MicrowaveMachineCircuitboard
- type: ContainerContainer
- map: [ "grinder" ]
state: "grinder_empty"
- type: ApcPowerReceiver
- powerLoad: 300
+ powerLoad: 0
+ - type: PowerState
+ idlePowerDraw: 0
+ workingPowerDraw: 750 # medium power blender
- type: ItemSlots
slots:
beakerSlot:
containers:
board: !type:Container
- type: ApcPowerReceiver
- powerLoad: 200
+ powerLoad: 50
+ - type: PowerState
+ idlePowerDraw: 50
+ workingPowerDraw: 200
+ - type: UIPowerState
- type: Construction
graph: StationMap
node: station_map