Files
tbd-station-14/Content.Server/Power/EntitySystems/ApcSystem.cs
Pieter-Jan Briers 797aebb161 Fix bugs resulting in quantum APC visual states (#29475)
There were TWO bugs here

FIRST, APCs *did* update their visual state on initialization, but at that point the relevant power state hasn't been initialized yet, so it always returns a bogus result. There aren't guaranteed to be subsequent power updates that actually trigger the APC to update so this can get it stuck.

Fixed by just deferring the on-init update to be after the first update tick, which is itself ordered to be after power update.

SECOND: Once I fixed that, I ran into the issue that APCs created at *server startup* also fail to update, because the throttling system (to prevent frequent APC updates) thinks the LastChargeStateTime was at server startup.

Fixed by making that variable nullable so it defaults to null.

Also removed the useless datafields on the "last update" fields. These are all just used to cache and throttle updates, something that should not be persisted to a map file.
2024-06-28 22:07:00 -04:00

218 lines
7.6 KiB
C#

using Content.Server.Emp;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.Pow3r;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.APC;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Popups;
using Content.Shared.Rounding;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
namespace Content.Server.Power.EntitySystems;
public 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()
{
base.Initialize();
UpdatesAfter.Add(typeof(PowerNetSystem));
SubscribeLocalEvent<ApcComponent, BoundUIOpenedEvent>(OnBoundUiOpen);
SubscribeLocalEvent<ApcComponent, ComponentStartup>(OnApcStartup);
SubscribeLocalEvent<ApcComponent, ChargeChangedEvent>(OnBatteryChargeChanged);
SubscribeLocalEvent<ApcComponent, ApcToggleMainBreakerMessage>(OnToggleMainBreaker);
SubscribeLocalEvent<ApcComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<ApcComponent, EmpPulseEvent>(OnEmpPulse);
}
public override void Update(float deltaTime)
{
var query = EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent, UserInterfaceComponent>();
while (query.MoveNext(out var uid, out var apc, out var battery, out var ui))
{
if (apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime && _ui.IsUiOpen((uid, ui), ApcUiKey.Key))
{
apc.LastUiUpdate = _gameTiming.CurTime;
UpdateUIState(uid, apc, battery);
}
if (apc.NeedStateUpdate)
{
UpdateApcState(uid, apc, battery);
}
}
}
// 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 static void OnApcStartup(EntityUid uid, ApcComponent component, ComponentStartup args)
{
// We cannot update immediately, as various network/battery state is not valid yet.
// Defer until the next tick.
component.NeedStateUpdate = true;
}
//Update the HasAccess var for UI to read
private void OnBoundUiOpen(EntityUid uid, ApcComponent component, BoundUIOpenedEvent args)
{
// TODO: this should be per-player not stored on the apc
component.HasAccess = _accessReader.IsAllowed(args.Actor, 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.Actor, PopupType.Medium);
return;
}
if (_accessReader.IsAllowed(args.Actor, uid))
{
ApcToggleBreaker(uid, component);
}
else
{
_popup.PopupCursor(Loc.GetString("apc-component-insufficient-access"),
args.Actor, 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, false))
return;
if (apc.LastChargeStateTime == null || apc.LastChargeStateTime + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime)
{
var newState = CalcChargeState(uid, battery.NetworkBattery);
if (newState != apc.LastChargeState)
{
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);
}
apc.NeedStateUpdate = false;
}
public void UpdateUIState(EntityUid uid,
ApcComponent? apc = null,
PowerNetworkBatteryComponent? netBat = null,
UserInterfaceComponent? ui = null)
{
if (!Resolve(uid, ref apc, ref netBat, ref ui))
return;
var battery = netBat.NetworkBattery;
const int ChargeAccuracy = 5;
// TODO: Fix ContentHelpers or make a new one coz this is cooked.
var charge = ContentHelpers.RoundToNearestLevels(battery.CurrentStorage / battery.Capacity, 1.0, 100 / ChargeAccuracy) / 100f * ChargeAccuracy;
var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled, apc.HasAccess,
(int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState,
charge);
_ui.SetUiState((uid, ui), ApcUiKey.Key, state);
}
private ApcChargeState CalcChargeState(EntityUid uid, PowerState.Battery battery)
{
if (HasComp<EmaggedComponent>(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);