diff --git a/Content.Client/Power/APC/ApcVisualizer.cs b/Content.Client/Power/APC/ApcVisualizer.cs index b2f57624ff..e68a91ea1d 100644 --- a/Content.Client/Power/APC/ApcVisualizer.cs +++ b/Content.Client/Power/APC/ApcVisualizer.cs @@ -3,11 +3,16 @@ using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Maths; namespace Content.Client.Power.APC { public class ApcVisualizer : AppearanceVisualizer { + public static readonly Color LackColor = Color.FromHex("#d1332e"); + public static readonly Color ChargingColor = Color.FromHex("#2e8ad1"); + public static readonly Color FullColor = Color.FromHex("#3db83b"); + [UsedImplicitly] public override void InitializeEntity(EntityUid entity) { @@ -35,7 +40,8 @@ namespace Content.Client.Power.APC { base.OnChangeData(component); - var sprite = IoCManager.Resolve().GetComponent(component.Owner); + var ent = IoCManager.Resolve(); + var sprite = ent.GetComponent(component.Owner); if (component.TryGetData(ApcVisuals.ChargeState, out var state)) { switch (state) @@ -50,6 +56,17 @@ namespace Content.Client.Power.APC sprite.LayerSetState(Layers.ChargeState, "apco3-2"); break; } + + if (ent.TryGetComponent(component.Owner, out SharedPointLightComponent? light)) + { + light.Color = state switch + { + ApcChargeState.Lack => LackColor, + ApcChargeState.Charging => ChargingColor, + ApcChargeState.Full => FullColor, + _ => LackColor + }; + } } else { diff --git a/Content.Server/Power/Components/ApcComponent.cs b/Content.Server/Power/Components/ApcComponent.cs index 97752804bb..3e5953b45d 100644 --- a/Content.Server/Power/Components/ApcComponent.cs +++ b/Content.Server/Power/Components/ApcComponent.cs @@ -1,209 +1,46 @@ using System; +using Content.Server.Power.EntitySystems; using Content.Server.Power.NodeGroups; -using Content.Server.UserInterface; -using Content.Shared.Access.Components; -using Content.Shared.Access.Systems; using Content.Shared.APC; -using Content.Shared.Interaction; -using Content.Shared.Popups; using Content.Shared.Sound; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Maths; -using Robust.Shared.Player; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Timing; using Robust.Shared.ViewVariables; -namespace Content.Server.Power.Components +namespace Content.Server.Power.Components; + +[RegisterComponent] +[Friend(typeof(ApcSystem))] +[ComponentProtoName("Apc")] +public class ApcComponent : BaseApcNetComponent { - [RegisterComponent] - [ComponentReference(typeof(IActivate))] - public class ApcComponent : BaseApcNetComponent, IActivate + [DataField("onReceiveMessageSound")] + public SoundSpecifier OnReceiveMessageSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg"); + + [ViewVariables] + public ApcChargeState LastChargeState; + public TimeSpan LastChargeStateTime; + + [ViewVariables] + public ApcExternalPowerState LastExternalState; + public TimeSpan LastUiUpdate; + + [ViewVariables] + public bool MainBreakerEnabled = true; + + public const float HighPowerThreshold = 0.9f; + public static TimeSpan VisualsChangeDelay = TimeSpan.FromSeconds(1); + + // TODO ECS power a little better! + protected override void AddSelfToNet(IApcNet apcNet) { - [Dependency] private readonly IEntityManager _entMan = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; + apcNet.AddApc(this); + } - public override string Name => "Apc"; - - public bool MainBreakerEnabled { get; private set; } = true; - - [DataField("onReceiveMessageSound")] private SoundSpecifier _onReceiveMessageSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg"); - - private ApcChargeState _lastChargeState; - - private TimeSpan _lastChargeStateChange; - - private ApcExternalPowerState _lastExternalPowerState; - - private TimeSpan _lastExternalPowerStateChange; - - private float _lastCharge; - - private TimeSpan _lastChargeChange; - - private bool _uiDirty = true; - - private const float HighPowerThreshold = 0.9f; - - private const int VisualsChangeDelay = 1; - - private static readonly Color LackColor = Color.FromHex("#d1332e"); - private static readonly Color ChargingColor = Color.FromHex("#2e8ad1"); - private static readonly Color FullColor = Color.FromHex("#3db83b"); - - [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ApcUiKey.Key); - - public BatteryComponent? Battery => _entMan.TryGetComponent(Owner, out BatteryComponent? batteryComponent) ? batteryComponent : null; - - protected override void Initialize() - { - base.Initialize(); - - Owner.EnsureComponentWarn(); - Owner.EnsureComponentWarn(); - - if (UserInterface != null) - { - UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage; - } - - Update(); - } - - protected override void AddSelfToNet(IApcNet apcNet) - { - apcNet.AddApc(this); - } - - protected override void RemoveSelfFromNet(IApcNet apcNet) - { - apcNet.RemoveApc(this); - } - - private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg) - { - if (serverMsg.Message is not ApcToggleMainBreakerMessage || serverMsg.Session.AttachedEntity is not {} attached) - return; - - var accessSystem = EntitySystem.Get(); - if (!_entMan.TryGetComponent(Owner, out var accessReaderComponent) || accessSystem.IsAllowed(accessReaderComponent, attached)) - { - MainBreakerEnabled = !MainBreakerEnabled; - _entMan.GetComponent(Owner).CanDischarge = MainBreakerEnabled; - - _uiDirty = true; - SoundSystem.Play(Filter.Pvs(Owner), _onReceiveMessageSound.GetSound(), Owner, AudioParams.Default.WithVolume(-2f)); - } - else - { - attached.PopupMessageCursor(Loc.GetString("apc-component-insufficient-access")); - } - } - - public void Update() - { - var newState = CalcChargeState(); - if (newState != _lastChargeState && _lastChargeStateChange + TimeSpan.FromSeconds(VisualsChangeDelay) < _gameTiming.CurTime) - { - _lastChargeState = newState; - _lastChargeStateChange = _gameTiming.CurTime; - - if (_entMan.TryGetComponent(Owner, out AppearanceComponent? appearance)) - { - appearance.SetData(ApcVisuals.ChargeState, newState); - } - - if (_entMan.TryGetComponent(Owner, out SharedPointLightComponent? light)) - { - light.Color = newState switch - { - ApcChargeState.Lack => LackColor, - ApcChargeState.Charging => ChargingColor, - ApcChargeState.Full => FullColor, - _ => LackColor - }; - light.Dirty(); - } - } - - _entMan.TryGetComponent(Owner, out BatteryComponent? battery); - - var newCharge = battery?.CurrentCharge; - if (newCharge != null && newCharge != _lastCharge && _lastChargeChange + TimeSpan.FromSeconds(VisualsChangeDelay) < _gameTiming.CurTime) - { - _lastCharge = newCharge.Value; - _lastChargeChange = _gameTiming.CurTime; - _uiDirty = true; - } - - var extPowerState = CalcExtPowerState(); - if (extPowerState != _lastExternalPowerState && _lastExternalPowerStateChange + TimeSpan.FromSeconds(VisualsChangeDelay) < _gameTiming.CurTime) - { - _lastExternalPowerState = extPowerState; - _lastExternalPowerStateChange = _gameTiming.CurTime; - _uiDirty = true; - } - - if (_uiDirty && battery != null && newCharge != null) - { - UserInterface?.SetState(new ApcBoundInterfaceState(MainBreakerEnabled, extPowerState, newCharge.Value / battery.MaxCharge)); - _uiDirty = false; - } - } - - private ApcChargeState CalcChargeState() - { - if (!_entMan.TryGetComponent(Owner, out BatteryComponent? battery)) - { - return ApcChargeState.Lack; - } - - var chargeFraction = battery.CurrentCharge / battery.MaxCharge; - - if (chargeFraction > HighPowerThreshold) - { - return ApcChargeState.Full; - } - - var netBattery = _entMan.GetComponent(Owner); - var delta = netBattery.CurrentSupply - netBattery.CurrentReceiving; - - return delta < 0 ? ApcChargeState.Charging : ApcChargeState.Lack; - } - - private ApcExternalPowerState CalcExtPowerState() - { - var bat = Battery; - if (bat == null) - return ApcExternalPowerState.None; - - var netBat = _entMan.GetComponent(Owner); - if (netBat.CurrentReceiving == 0 && netBat.LoadingNetworkDemand != 0) - { - return ApcExternalPowerState.None; - } - - var delta = netBat.CurrentReceiving - netBat.LoadingNetworkDemand; - if (!MathHelper.CloseToPercent(delta, 0, 0.1f) && delta < 0) - { - return ApcExternalPowerState.Low; - } - - return ApcExternalPowerState.Good; - } - - void IActivate.Activate(ActivateEventArgs eventArgs) - { - if (!_entMan.TryGetComponent(eventArgs.User, out ActorComponent? actor)) - { - return; - } - - UserInterface?.Open(actor.PlayerSession); - } + protected override void RemoveSelfFromNet(IApcNet apcNet) + { + apcNet.RemoveApc(this); } } diff --git a/Content.Server/Power/EntitySystems/ApcSystem.cs b/Content.Server/Power/EntitySystems/ApcSystem.cs new file mode 100644 index 0000000000..5b77405433 --- /dev/null +++ b/Content.Server/Power/EntitySystems/ApcSystem.cs @@ -0,0 +1,156 @@ +using System; +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 JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +namespace Content.Server.Power.EntitySystems +{ + [UsedImplicitly] + internal sealed class ApcSystem : EntitySystem + { + [Dependency] private readonly AccessReaderSystem _accessReader = default!; + [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + public override void Initialize() + { + base.Initialize(); + + UpdatesAfter.Add(typeof(PowerNetSystem)); + + SubscribeLocalEvent(OnApcInit); + SubscribeLocalEvent(OnBatteryChargeChanged); + SubscribeLocalEvent(OnToggleMainBreaker); + } + + // Change the APC's state only when the battery state changes, or when it's first created. + private void OnBatteryChargeChanged(EntityUid uid, ApcComponent component, ChargeChangedEvent args) + { + UpdateApcState(uid, component); + } + + private void OnApcInit(EntityUid uid, ApcComponent component, MapInitEvent args) + { + UpdateApcState(uid, component); + } + + private void OnToggleMainBreaker(EntityUid uid, ApcComponent component, ApcToggleMainBreakerMessage args) + { + TryComp(uid, out var access); + if (args.Session.AttachedEntity == null) + return; + + if (access == null || _accessReader.IsAllowed(access, args.Session.AttachedEntity.Value)) + { + component.MainBreakerEnabled = !component.MainBreakerEnabled; + Comp(uid).CanDischarge = component.MainBreakerEnabled; + + UpdateUIState(uid, component); + SoundSystem.Play(Filter.Pvs(uid), component.OnReceiveMessageSound.GetSound(), uid, AudioParams.Default.WithVolume(-2f)); + } + else + { + _popupSystem.PopupCursor(Loc.GetString("apc-component-insufficient-access"), + Filter.Entities(args.Session.AttachedEntity.Value)); + } + } + + public void UpdateApcState(EntityUid uid, + ApcComponent? apc=null, + BatteryComponent? battery=null) + { + if (!Resolve(uid, ref apc, ref battery)) + return; + + var newState = CalcChargeState(uid, apc, battery); + 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(ApcVisuals.ChargeState, newState); + } + } + + var extPowerState = CalcExtPowerState(uid, apc, battery); + 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, + BatteryComponent? battery = null, + ServerUserInterfaceComponent? ui = null) + { + if (!Resolve(uid, ref apc, ref battery, ref ui)) + return; + + if (_userInterfaceSystem.GetUiOrNull(uid, ApcUiKey.Key, ui) is { } bui) + { + bui.SetState(new ApcBoundInterfaceState(apc.MainBreakerEnabled, apc.LastExternalState, battery.CurrentCharge / battery.MaxCharge)); + } + } + + public ApcChargeState CalcChargeState(EntityUid uid, + ApcComponent? apc=null, + BatteryComponent? battery=null) + { + if (!Resolve(uid, ref apc, ref battery)) + return ApcChargeState.Lack; + + var chargeFraction = battery.CurrentCharge / battery.MaxCharge; + + if (chargeFraction > ApcComponent.HighPowerThreshold) + { + return ApcChargeState.Full; + } + + var netBattery = Comp(uid); + var delta = netBattery.CurrentSupply - netBattery.CurrentReceiving; + + return delta < 0 ? ApcChargeState.Charging : ApcChargeState.Lack; + } + + public ApcExternalPowerState CalcExtPowerState(EntityUid uid, + ApcComponent? apc=null, + BatteryComponent? battery=null) + { + if (!Resolve(uid, ref apc, ref battery)) + return ApcExternalPowerState.None; + + var netBat = Comp(uid); + if (netBat.CurrentReceiving == 0 && !MathHelper.CloseTo(battery.CurrentCharge / battery.MaxCharge, 1)) + { + return ApcExternalPowerState.None; + } + + var delta = netBat.CurrentReceiving - netBat.LoadingNetworkDemand; + if (!MathHelper.CloseToPercent(delta, 0, 0.1f) && delta < 0) + { + return ApcExternalPowerState.Low; + } + + return ApcExternalPowerState.Good; + } + } +} diff --git a/Content.Server/Power/EntitySystems/PowerApcSystem.cs b/Content.Server/Power/EntitySystems/PowerApcSystem.cs deleted file mode 100644 index 744bde6b41..0000000000 --- a/Content.Server/Power/EntitySystems/PowerApcSystem.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Server.Power.Components; -using JetBrains.Annotations; -using Robust.Shared.GameObjects; - -namespace Content.Server.Power.EntitySystems -{ - [UsedImplicitly] - internal sealed class PowerApcSystem : EntitySystem - { - public override void Initialize() - { - base.Initialize(); - - UpdatesAfter.Add(typeof(PowerNetSystem)); - } - - public override void Update(float frameTime) - { - foreach (var apc in EntityManager.EntityQuery()) - { - apc.Update(); - } - } - } -} diff --git a/Resources/Prototypes/Entities/Structures/Power/apc.yml b/Resources/Prototypes/Entities/Structures/Power/apc.yml index 35dbb33df6..facf36ff18 100644 --- a/Resources/Prototypes/Entities/Structures/Power/apc.yml +++ b/Resources/Prototypes/Entities/Structures/Power/apc.yml @@ -13,6 +13,7 @@ sound: path: /Audio/Ambience/Objects/hdd_buzz.ogg - type: PointLight + netsync: false radius: 1.5 energy: 1.6 color: "#3db83b" @@ -62,6 +63,10 @@ interfaces: - key: enum.ApcUiKey.Key type: ApcBoundUserInterface + - type: ActivatableUI + inHandsOnly: false + singleUser: true + key: enum.ApcUiKey.Key - type: Construction graph: apc node: apc