diff --git a/Content.Server/Power/Components/ApcComponent.cs b/Content.Server/Power/Components/ApcComponent.cs
index 1729dcd10a..2a31a6ba4d 100644
--- a/Content.Server/Power/Components/ApcComponent.cs
+++ b/Content.Server/Power/Components/ApcComponent.cs
@@ -1,6 +1,7 @@
using Content.Server.Power.NodeGroups;
using Content.Shared.APC;
using Robust.Shared.Audio;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Power.Components;
@@ -10,16 +11,26 @@ public sealed partial class ApcComponent : BaseApcNetComponent
[DataField("onReceiveMessageSound")]
public SoundSpecifier OnReceiveMessageSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
- [ViewVariables]
+ [DataField("lastChargeState")]
public ApcChargeState LastChargeState;
+ [DataField("lastChargeStateTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastChargeStateTime;
- [ViewVariables]
+ [DataField("lastExternalState")]
public ApcExternalPowerState LastExternalState;
+
+ ///
+ /// Time the ui was last updated automatically.
+ /// Done after every to show the latest load.
+ /// If charge state changes it will be instantly updated.
+ ///
+ [DataField("lastUiUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastUiUpdate;
- [ViewVariables]
+ [DataField("enabled")]
public bool MainBreakerEnabled = true;
+ // TODO: remove this since it probably breaks when 2 people use it
+ [DataField("hasAccess")]
public bool HasAccess = false;
public const float HighPowerThreshold = 0.9f;
diff --git a/Content.Server/Power/EntitySystems/ApcSystem.cs b/Content.Server/Power/EntitySystems/ApcSystem.cs
index 3f5da2129a..d477fa4360 100644
--- a/Content.Server/Power/EntitySystems/ApcSystem.cs
+++ b/Content.Server/Power/EntitySystems/ApcSystem.cs
@@ -8,196 +8,198 @@ using Content.Shared.APC;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Popups;
-using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Timing;
-namespace Content.Server.Power.EntitySystems
+namespace Content.Server.Power.EntitySystems;
+
+public sealed class ApcSystem : EntitySystem
{
- [UsedImplicitly]
- internal sealed class ApcSystem : EntitySystem
+ [Dependency] private readonly AccessReaderSystem _accessReader = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+ public override void Initialize()
{
- [Dependency] private readonly AccessReaderSystem _accessReader = default!;
- [Dependency] private readonly UserInterfaceSystem _ui = default!;
- [Dependency] private readonly PopupSystem _popup = default!;
- [Dependency] private readonly IGameTiming _gameTiming = default!;
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
+ base.Initialize();
- public override void Initialize()
+ UpdatesAfter.Add(typeof(PowerNetSystem));
+
+ SubscribeLocalEvent(OnBoundUiOpen);
+ SubscribeLocalEvent(OnApcInit);
+ SubscribeLocalEvent(OnBatteryChargeChanged);
+ SubscribeLocalEvent(OnToggleMainBreaker);
+ SubscribeLocalEvent(OnEmagged);
+
+ SubscribeLocalEvent(OnEmpPulse);
+ }
+
+ public override void Update(float deltaTime)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var apc, out var battery, out var ui))
{
- base.Initialize();
-
- UpdatesAfter.Add(typeof(PowerNetSystem));
-
- SubscribeLocalEvent(OnBoundUiOpen);
- SubscribeLocalEvent(OnApcInit);
- SubscribeLocalEvent(OnBatteryChargeChanged);
- SubscribeLocalEvent(OnToggleMainBreaker);
- SubscribeLocalEvent(OnEmagged);
-
- SubscribeLocalEvent(OnEmpPulse);
- }
-
- // Change the APC's state only when the battery state changes, or when it's first created.
- private void OnBatteryChargeChanged(EntityUid uid, ApcComponent component, ref ChargeChangedEvent args)
- {
- UpdateApcState(uid, component);
- }
-
- private void OnApcInit(EntityUid uid, ApcComponent component, MapInitEvent args)
- {
- UpdateApcState(uid, component);
- }
- //Update the HasAccess var for UI to read
- private void OnBoundUiOpen(EntityUid uid, ApcComponent component, BoundUIOpenedEvent args)
- {
- TryComp(uid, out var access);
- if (args.Session.AttachedEntity == null)
- return;
-
- if (access == null || _accessReader.IsAllowed(args.Session.AttachedEntity.Value, uid, access))
+ if (apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime)
{
- component.HasAccess = true;
- }
- else
- {
- component.HasAccess = false;
- }
- UpdateApcState(uid, component);
- }
- private void OnToggleMainBreaker(EntityUid uid, ApcComponent component, ApcToggleMainBreakerMessage args)
- {
- var attemptEv = new ApcToggleMainBreakerAttemptEvent();
- RaiseLocalEvent(uid, ref attemptEv);
- if (attemptEv.Cancelled)
- {
- _popup.PopupCursor(Loc.GetString("apc-component-on-toggle-cancel"),
- args.Session, PopupType.Medium);
- return;
- }
-
- TryComp(uid, out var access);
- if (args.Session.AttachedEntity == null)
- return;
-
- if (access == null || _accessReader.IsAllowed(args.Session.AttachedEntity.Value, uid, access))
- {
- ApcToggleBreaker(uid, component);
- }
- else
- {
- _popup.PopupCursor(Loc.GetString("apc-component-insufficient-access"),
- args.Session, PopupType.Medium);
- }
- }
-
- public void ApcToggleBreaker(EntityUid uid, ApcComponent? apc = null, PowerNetworkBatteryComponent? battery = null)
- {
- if (!Resolve(uid, ref apc, ref battery))
- return;
-
- apc.MainBreakerEnabled = !apc.MainBreakerEnabled;
- battery.CanDischarge = apc.MainBreakerEnabled;
-
- UpdateUIState(uid, apc);
- _audio.PlayPvs(apc.OnReceiveMessageSound, uid, AudioParams.Default.WithVolume(-2f));
- }
-
- private void OnEmagged(EntityUid uid, ApcComponent comp, ref GotEmaggedEvent args)
- {
- // no fancy conditions
- args.Handled = true;
- }
-
- public void UpdateApcState(EntityUid uid,
- ApcComponent? apc=null,
- PowerNetworkBatteryComponent? battery = null)
- {
- if (!Resolve(uid, ref apc, ref battery))
- return;
-
- var newState = CalcChargeState(uid, battery.NetworkBattery);
- if (newState != apc.LastChargeState && apc.LastChargeStateTime + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime)
- {
- apc.LastChargeState = newState;
- apc.LastChargeStateTime = _gameTiming.CurTime;
-
- if (TryComp(uid, out AppearanceComponent? appearance))
- {
- _appearance.SetData(uid, ApcVisuals.ChargeState, newState, appearance);
- }
- }
-
- var extPowerState = CalcExtPowerState(uid, battery.NetworkBattery);
- if (extPowerState != apc.LastExternalState
- || apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime)
- {
- apc.LastExternalState = extPowerState;
apc.LastUiUpdate = _gameTiming.CurTime;
- UpdateUIState(uid, apc, battery);
- }
- }
-
- public void UpdateUIState(EntityUid uid,
- ApcComponent? apc = null,
- PowerNetworkBatteryComponent? netBat = null,
- ServerUserInterfaceComponent? ui = null)
- {
- if (!Resolve(uid, ref apc, ref netBat, ref ui))
- return;
-
- var battery = netBat.NetworkBattery;
-
- var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled, apc.HasAccess,
- (int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState,
- battery.CurrentStorage / battery.Capacity);
-
- _ui.TrySetUiState(uid, ApcUiKey.Key, state, ui: ui);
- }
-
- private ApcChargeState CalcChargeState(EntityUid uid, PowerState.Battery battery)
- {
- if (HasComp(uid))
- return ApcChargeState.Emag;
-
- if (battery.CurrentStorage / battery.Capacity > ApcComponent.HighPowerThreshold)
- {
- return ApcChargeState.Full;
- }
-
- var delta = battery.CurrentSupply - battery.CurrentReceiving;
- return delta < 0 ? ApcChargeState.Charging : ApcChargeState.Lack;
- }
-
- private ApcExternalPowerState CalcExtPowerState(EntityUid uid, PowerState.Battery battery)
- {
- if (battery.CurrentReceiving == 0 && !MathHelper.CloseTo(battery.CurrentStorage / battery.Capacity, 1))
- {
- return ApcExternalPowerState.None;
- }
-
- var delta = battery.CurrentSupply - battery.CurrentReceiving;
- if (!MathHelper.CloseToPercent(delta, 0, 0.1f) && delta < 0)
- {
- return ApcExternalPowerState.Low;
- }
-
- return ApcExternalPowerState.Good;
- }
-
- private void OnEmpPulse(EntityUid uid, ApcComponent component, ref EmpPulseEvent args)
- {
- if (component.MainBreakerEnabled)
- {
- args.Affected = true;
- args.Disabled = true;
- ApcToggleBreaker(uid, component);
+ UpdateUIState(uid, apc, battery, ui);
}
}
}
- [ByRefEvent]
- public record struct ApcToggleMainBreakerAttemptEvent(bool Cancelled);
+ // Change the APC's state only when the battery state changes, or when it's first created.
+ private void OnBatteryChargeChanged(EntityUid uid, ApcComponent component, ref ChargeChangedEvent args)
+ {
+ UpdateApcState(uid, component);
+ }
+
+ private void OnApcInit(EntityUid uid, ApcComponent component, MapInitEvent args)
+ {
+ UpdateApcState(uid, component);
+ }
+
+ //Update the HasAccess var for UI to read
+ private void OnBoundUiOpen(EntityUid uid, ApcComponent component, BoundUIOpenedEvent args)
+ {
+ if (args.Session.AttachedEntity == null)
+ return;
+
+ // TODO: this should be per-player not stored on the apc
+ component.HasAccess = _accessReader.IsAllowed(args.Session.AttachedEntity.Value, uid);
+ UpdateApcState(uid, component);
+ }
+
+ private void OnToggleMainBreaker(EntityUid uid, ApcComponent component, ApcToggleMainBreakerMessage args)
+ {
+ var attemptEv = new ApcToggleMainBreakerAttemptEvent();
+ RaiseLocalEvent(uid, ref attemptEv);
+ if (attemptEv.Cancelled)
+ {
+ _popup.PopupCursor(Loc.GetString("apc-component-on-toggle-cancel"),
+ args.Session, PopupType.Medium);
+ return;
+ }
+
+ if (args.Session.AttachedEntity == null)
+ return;
+
+ if (_accessReader.IsAllowed(args.Session.AttachedEntity.Value, uid))
+ {
+ ApcToggleBreaker(uid, component);
+ }
+ else
+ {
+ _popup.PopupCursor(Loc.GetString("apc-component-insufficient-access"),
+ args.Session, PopupType.Medium);
+ }
+ }
+
+ public void ApcToggleBreaker(EntityUid uid, ApcComponent? apc = null, PowerNetworkBatteryComponent? battery = null)
+ {
+ if (!Resolve(uid, ref apc, ref battery))
+ return;
+
+ apc.MainBreakerEnabled = !apc.MainBreakerEnabled;
+ battery.CanDischarge = apc.MainBreakerEnabled;
+
+ UpdateUIState(uid, apc);
+ _audio.PlayPvs(apc.OnReceiveMessageSound, uid, AudioParams.Default.WithVolume(-2f));
+ }
+
+ private void OnEmagged(EntityUid uid, ApcComponent comp, ref GotEmaggedEvent args)
+ {
+ // no fancy conditions
+ args.Handled = true;
+ }
+
+ public void UpdateApcState(EntityUid uid,
+ ApcComponent? apc=null,
+ PowerNetworkBatteryComponent? battery = null)
+ {
+ if (!Resolve(uid, ref apc, ref battery))
+ return;
+
+ var newState = CalcChargeState(uid, battery.NetworkBattery);
+ if (newState != apc.LastChargeState && apc.LastChargeStateTime + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime)
+ {
+ apc.LastChargeState = newState;
+ apc.LastChargeStateTime = _gameTiming.CurTime;
+
+ if (TryComp(uid, out AppearanceComponent? appearance))
+ {
+ _appearance.SetData(uid, ApcVisuals.ChargeState, newState, appearance);
+ }
+ }
+
+ var extPowerState = CalcExtPowerState(uid, battery.NetworkBattery);
+ if (extPowerState != apc.LastExternalState)
+ {
+ apc.LastExternalState = extPowerState;
+ UpdateUIState(uid, apc, battery);
+ }
+ }
+
+ public void UpdateUIState(EntityUid uid,
+ ApcComponent? apc = null,
+ PowerNetworkBatteryComponent? netBat = null,
+ ServerUserInterfaceComponent? ui = null)
+ {
+ if (!Resolve(uid, ref apc, ref netBat, ref ui))
+ return;
+
+ var battery = netBat.NetworkBattery;
+
+ var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled, apc.HasAccess,
+ (int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState,
+ battery.CurrentStorage / battery.Capacity);
+
+ _ui.TrySetUiState(uid, ApcUiKey.Key, state, ui: ui);
+ }
+
+ private ApcChargeState CalcChargeState(EntityUid uid, PowerState.Battery battery)
+ {
+ if (HasComp(uid))
+ return ApcChargeState.Emag;
+
+ if (battery.CurrentStorage / battery.Capacity > ApcComponent.HighPowerThreshold)
+ {
+ return ApcChargeState.Full;
+ }
+
+ var delta = battery.CurrentSupply - battery.CurrentReceiving;
+ return delta < 0 ? ApcChargeState.Charging : ApcChargeState.Lack;
+ }
+
+ private ApcExternalPowerState CalcExtPowerState(EntityUid uid, PowerState.Battery battery)
+ {
+ if (battery.CurrentReceiving == 0 && !MathHelper.CloseTo(battery.CurrentStorage / battery.Capacity, 1))
+ {
+ return ApcExternalPowerState.None;
+ }
+
+ var delta = battery.CurrentSupply - battery.CurrentReceiving;
+ if (!MathHelper.CloseToPercent(delta, 0, 0.1f) && delta < 0)
+ {
+ return ApcExternalPowerState.Low;
+ }
+
+ return ApcExternalPowerState.Good;
+ }
+
+ private void OnEmpPulse(EntityUid uid, ApcComponent component, ref EmpPulseEvent args)
+ {
+ if (component.MainBreakerEnabled)
+ {
+ args.Affected = true;
+ args.Disabled = true;
+ ApcToggleBreaker(uid, component);
+ }
+ }
}
+
+[ByRefEvent]
+public record struct ApcToggleMainBreakerAttemptEvent(bool Cancelled);