diff --git a/Content.Client/Light/Components/HandheldLightComponent.cs b/Content.Client/Light/Components/HandheldLightComponent.cs index 1b2b194652..f357d17ae1 100644 --- a/Content.Client/Light/Components/HandheldLightComponent.cs +++ b/Content.Client/Light/Components/HandheldLightComponent.cs @@ -3,6 +3,7 @@ using Content.Shared.Light.Component; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.Maths; using Robust.Shared.Timing; @@ -12,27 +13,18 @@ using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Light.Components { [RegisterComponent] + [Friend(typeof(HandheldLightSystem))] public sealed class HandheldLightComponent : SharedHandheldLightComponent, IItemStatus { - [ViewVariables] protected override bool HasCell => _level != null; + [ViewVariables] protected override bool HasCell => Level != null; - private byte? _level; + public byte? Level; public Control MakeControl() { return new StatusControl(this); } - public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) - { - base.HandleComponentState(curState, nextState); - - if (curState is not HandheldLightComponentState cast) - return; - - _level = cast.Charge; - } - private sealed class StatusControl : Control { private const float TimerCycle = 1; @@ -83,7 +75,7 @@ namespace Content.Client.Light.Components _timer += args.DeltaSeconds; _timer %= TimerCycle; - var level = _parent._level; + var level = _parent.Level; for (var i = 0; i < _sections.Length; i++) { diff --git a/Content.Client/Light/HandheldLightSystem.cs b/Content.Client/Light/HandheldLightSystem.cs new file mode 100644 index 0000000000..ee1b3b6bc4 --- /dev/null +++ b/Content.Client/Light/HandheldLightSystem.cs @@ -0,0 +1,23 @@ +using Content.Client.Light.Components; +using Content.Shared.Light.Component; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; + +namespace Content.Client.Light; + +public sealed class HandheldLightSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(EntityUid uid, HandheldLightComponent component, ref ComponentHandleState args) + { + if (args.Current is not SharedHandheldLightComponent.HandheldLightComponentState state) + return; + + component.Level = state.Charge; + } +} diff --git a/Content.Server/Light/Components/HandheldLightComponent.cs b/Content.Server/Light/Components/HandheldLightComponent.cs index dc8b6d7560..2c979d26a7 100644 --- a/Content.Server/Light/Components/HandheldLightComponent.cs +++ b/Content.Server/Light/Components/HandheldLightComponent.cs @@ -1,27 +1,13 @@ -using System.Threading.Tasks; -using Content.Server.Clothing.Components; -using Content.Server.Items; +using Content.Server.Light.EntitySystems; using Content.Server.PowerCell.Components; -using Content.Shared.ActionBlocker; -using Content.Shared.Actions; using Content.Shared.Actions.Behaviors.Item; -using Content.Shared.Actions.Components; -using Content.Shared.Examine; -using Content.Shared.Interaction; using Content.Shared.Light.Component; -using Content.Shared.Popups; -using Content.Shared.Rounding; using Content.Shared.Sound; using JetBrains.Annotations; -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.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.Light.Components @@ -30,222 +16,29 @@ namespace Content.Server.Light.Components /// Component that represents a powered handheld light source which can be toggled on and off. /// [RegisterComponent] -#pragma warning disable 618 - internal sealed class HandheldLightComponent : SharedHandheldLightComponent, IUse, IExamine, IInteractUsing -#pragma warning restore 618 + [Friend(typeof(HandheldLightSystem))] + public sealed class HandheldLightComponent : SharedHandheldLightComponent { - [Dependency] private readonly IEntityManager _entMan = default!; - [ViewVariables(VVAccess.ReadWrite)] [DataField("wattage")] public float Wattage { get; set; } = 3f; - [ViewVariables] private PowerCellSlotComponent _cellSlot = default!; - private PowerCellComponent? Cell => _cellSlot.Cell; + [ViewVariables] public PowerCellSlotComponent CellSlot = default!; + public PowerCellComponent? Cell => CellSlot.Cell; /// /// Status of light, whether or not it is emitting light. /// [ViewVariables] - public bool Activated { get; private set; } + public bool Activated { get; set; } - [ViewVariables] protected override bool HasCell => _cellSlot.HasCell; + [ViewVariables] protected override bool HasCell => CellSlot.HasCell; [ViewVariables(VVAccess.ReadWrite)] [DataField("turnOnSound")] public SoundSpecifier TurnOnSound = new SoundPathSpecifier("/Audio/Items/flashlight_on.ogg"); [ViewVariables(VVAccess.ReadWrite)] [DataField("turnOnFailSound")] public SoundSpecifier TurnOnFailSound = new SoundPathSpecifier("/Audio/Machines/button.ogg"); [ViewVariables(VVAccess.ReadWrite)] [DataField("turnOffSound")] public SoundSpecifier TurnOffSound = new SoundPathSpecifier("/Audio/Items/flashlight_off.ogg"); - [ComponentDependency] private readonly ItemActionsComponent? _itemActions = null; - /// /// Client-side ItemStatus level /// - private byte? _lastLevel; - - protected override void Initialize() - { - base.Initialize(); - - Owner.EnsureComponent(); - _cellSlot = Owner.EnsureComponent(); - - Dirty(); - } - - protected override void OnRemove() - { - base.OnRemove(); - _entMan.EventBus.QueueEvent(EventSource.Local, new DeactivateHandheldLightMessage(this)); - } - - async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) - { - if (!EntitySystem.Get().CanInteract(eventArgs.User)) return false; - if (!_cellSlot.InsertCell(eventArgs.Using)) return false; - Dirty(); - return true; - } - - void IExamine.Examine(FormattedMessage message, bool inDetailsRange) - { - if (Activated) - { - message.AddMarkup(Loc.GetString("handheld-light-component-on-examine-is-on-message")); - } - else - { - message.AddMarkup(Loc.GetString("handheld-light-component-on-examine-is-off-message")); - } - } - - bool IUse.UseEntity(UseEntityEventArgs eventArgs) - { - return ToggleStatus(eventArgs.User); - } - - /// - /// Illuminates the light if it is not active, extinguishes it if it is active. - /// - /// True if the light's status was toggled, false otherwise. - public bool ToggleStatus(EntityUid user) - { - if (!EntitySystem.Get().CanUse(user)) return false; - return Activated ? TurnOff() : TurnOn(user); - } - - public bool TurnOff(bool makeNoise = true) - { - if (!Activated) - { - return false; - } - - SetState(false); - Activated = false; - UpdateLightAction(); - _entMan.EventBus.QueueEvent(EventSource.Local, new DeactivateHandheldLightMessage(this)); - - if (makeNoise) - { - SoundSystem.Play(Filter.Pvs(Owner), TurnOffSound.GetSound(), Owner); - } - - return true; - } - - public bool TurnOn(EntityUid user) - { - if (Activated) - { - return false; - } - - if (Cell == null) - { - SoundSystem.Play(Filter.Pvs(Owner), TurnOnFailSound.GetSound(), Owner); - Owner.PopupMessage(user, Loc.GetString("handheld-light-component-cell-missing-message")); - UpdateLightAction(); - return false; - } - - // To prevent having to worry about frame time in here. - // Let's just say you need a whole second of charge before you can turn it on. - // Simple enough. - if (Wattage > Cell.CurrentCharge) - { - SoundSystem.Play(Filter.Pvs(Owner), TurnOnFailSound.GetSound(), Owner); - Owner.PopupMessage(user, Loc.GetString("handheld-light-component-cell-dead-message")); - UpdateLightAction(); - return false; - } - - Activated = true; - UpdateLightAction(); - SetState(true); - _entMan.EventBus.QueueEvent(EventSource.Local, new ActivateHandheldLightMessage(this)); - - SoundSystem.Play(Filter.Pvs(Owner), TurnOnSound.GetSound(), Owner); - return true; - } - - private void SetState(bool on) - { - if (_entMan.TryGetComponent(Owner, out SpriteComponent? sprite)) - { - sprite.LayerSetVisible(1, on); - } - - if (_entMan.TryGetComponent(Owner, out PointLightComponent? light)) - { - light.Enabled = on; - } - - if (_entMan.TryGetComponent(Owner, out ClothingComponent? clothing)) - { - clothing.ClothingEquippedPrefix = Loc.GetString(on ? "on" : "off"); - } - - if (_entMan.TryGetComponent(Owner, out ItemComponent? item)) - { - item.EquippedPrefix = Loc.GetString(on ? "on" : "off"); - } - } - - private void UpdateLightAction() - { - _itemActions?.Toggle(ItemActionType.ToggleLight, Activated); - } - - public void OnUpdate(float frameTime) - { - if (Cell == null) - { - TurnOff(false); - return; - } - - var appearanceComponent = _entMan.GetComponent(Owner); - - if (Cell.MaxCharge - Cell.CurrentCharge < Cell.MaxCharge * 0.70) - { - appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower); - } - else if (Cell.MaxCharge - Cell.CurrentCharge < Cell.MaxCharge * 0.90) - { - appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.LowPower); - } - else - { - appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.Dying); - } - - if (Activated && !Cell.TryUseCharge(Wattage * frameTime)) TurnOff(false); - - var level = GetLevel(); - - if (level != _lastLevel) - { - _lastLevel = level; - Dirty(); - } - } - - // Curently every single flashlight has the same number of levels for status and that's all it uses the charge for - // Thus we'll just check if the level changes. - private byte? GetLevel() - { - if (Cell == null) - return null; - - var currentCharge = Cell.CurrentCharge; - - if (MathHelper.CloseToPercent(currentCharge, 0) || Wattage > currentCharge) - return 0; - - return (byte?) ContentHelpers.RoundToNearestLevels(currentCharge / Cell.MaxCharge * 255, 255, StatusLevels); - } - - public override ComponentState GetComponentState() - { - return new HandheldLightComponentState(GetLevel()); - } + public byte? LastLevel; } [UsedImplicitly] @@ -256,27 +49,7 @@ namespace Content.Server.Light.Components { if (!IoCManager.Resolve().TryGetComponent(args.Item, out var lightComponent)) return false; if (lightComponent.Activated == args.ToggledOn) return false; - return lightComponent.ToggleStatus(args.Performer); - } - } - - internal sealed class ActivateHandheldLightMessage : EntityEventArgs - { - public HandheldLightComponent Component { get; } - - public ActivateHandheldLightMessage(HandheldLightComponent component) - { - Component = component; - } - } - - internal sealed class DeactivateHandheldLightMessage : EntityEventArgs - { - public HandheldLightComponent Component { get; } - - public DeactivateHandheldLightMessage(HandheldLightComponent component) - { - Component = component; + return EntitySystem.Get().ToggleStatus(args.Performer, lightComponent); } } } diff --git a/Content.Server/Light/EntitySystems/HandHeldLightSystem.cs b/Content.Server/Light/EntitySystems/HandHeldLightSystem.cs deleted file mode 100644 index a044d7a59a..0000000000 --- a/Content.Server/Light/EntitySystems/HandHeldLightSystem.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Content.Server.Light.Components; -using Content.Shared.Verbs; -using JetBrains.Annotations; -using Robust.Shared.GameObjects; -using Robust.Shared.Localization; - -namespace Content.Server.Light.EntitySystems -{ - [UsedImplicitly] - internal sealed class HandHeldLightSystem : EntitySystem - { - // TODO: Ideally you'd be able to subscribe to power stuff to get events at certain percentages.. or something? - // But for now this will be better anyway. - private HashSet _activeLights = new(); - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(HandleActivate); - SubscribeLocalEvent(HandleDeactivate); - SubscribeLocalEvent(AddToggleLightVerb); - } - - public override void Shutdown() - { - base.Shutdown(); - _activeLights.Clear(); - } - - private void HandleActivate(ActivateHandheldLightMessage message) - { - _activeLights.Add(message.Component); - } - - private void HandleDeactivate(DeactivateHandheldLightMessage message) - { - _activeLights.Remove(message.Component); - } - - public override void Update(float frameTime) - { - foreach (var handheld in _activeLights.ToArray()) - { - if (handheld.Deleted || handheld.Paused) continue; - handheld.OnUpdate(frameTime); - } - } - - private void AddToggleLightVerb(EntityUid uid, HandheldLightComponent component, GetActivationVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract) - return; - - Verb verb = new(); - verb.Text = Loc.GetString("verb-common-toggle-light"); - verb.IconTexture = "/Textures/Interface/VerbIcons/light.svg.192dpi.png"; - verb.Act = component.Activated - ? () => component.TurnOff() - : () => component.TurnOn(args.User); - - args.Verbs.Add(verb); - } - } -} diff --git a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs new file mode 100644 index 0000000000..67aa2ccb35 --- /dev/null +++ b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs @@ -0,0 +1,283 @@ +using System.Collections.Generic; +using System.Linq; +using Content.Server.Clothing.Components; +using Content.Server.Items; +using Content.Server.Light.Components; +using Content.Server.Popups; +using Content.Server.PowerCell.Components; +using Content.Shared.ActionBlocker; +using Content.Shared.Actions; +using Content.Shared.Actions.Components; +using Content.Shared.Examine; +using Content.Shared.Interaction; +using Content.Shared.Light.Component; +using Content.Shared.Rounding; +using Content.Shared.Verbs; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Player; +using Robust.Shared.Utility; + +namespace Content.Server.Light.EntitySystems +{ + [UsedImplicitly] + public sealed class HandheldLightSystem : EntitySystem + { + [Dependency] private readonly ActionBlockerSystem _blocker = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + // TODO: Ideally you'd be able to subscribe to power stuff to get events at certain percentages.. or something? + // But for now this will be better anyway. + private readonly HashSet _activeLights = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + SubscribeLocalEvent(OnGetState); + + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(AddToggleLightVerb); + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnUse); + } + + private void OnGetState(EntityUid uid, HandheldLightComponent component, ref ComponentGetState args) + { + args.State = new SharedHandheldLightComponent.HandheldLightComponentState(GetLevel(component)); + } + + private byte? GetLevel(HandheldLightComponent component) + { + // Curently every single flashlight has the same number of levels for status and that's all it uses the charge for + // Thus we'll just check if the level changes. + if (component.Cell == null) + return null; + + var currentCharge = component.Cell.CurrentCharge; + + if (MathHelper.CloseToPercent(currentCharge, 0) || component.Wattage > currentCharge) + return 0; + + return (byte?) ContentHelpers.RoundToNearestLevels(currentCharge / component.Cell.MaxCharge * 255, 255, SharedHandheldLightComponent.StatusLevels); + } + + private void OnInit(EntityUid uid, HandheldLightComponent component, ComponentInit args) + { + EntityManager.EnsureComponent(uid); + component.CellSlot = EntityManager.EnsureComponent(uid); + + // Want to make sure client has latest data on level so battery displays properly. + component.Dirty(EntityManager); + } + + private void OnRemove(EntityUid uid, HandheldLightComponent component, ComponentRemove args) + { + _activeLights.Remove(component); + } + + private void OnInteractUsing(EntityUid uid, HandheldLightComponent component, InteractUsingEvent args) + { + // TODO: https://github.com/space-wizards/space-station-14/pull/5864#discussion_r775191916 + if (args.Handled) return; + + if (!_blocker.CanInteract(args.User)) return; + if (!component.CellSlot.InsertCell(args.Used)) return; + component.Dirty(EntityManager); + args.Handled = true; + } + + private void OnUse(EntityUid uid, HandheldLightComponent component, UseInHandEvent args) + { + if (args.Handled) return; + + if (ToggleStatus(args.User, component)) + args.Handled = true; + } + + /// + /// Illuminates the light if it is not active, extinguishes it if it is active. + /// + /// True if the light's status was toggled, false otherwise. + public bool ToggleStatus(EntityUid user, HandheldLightComponent component) + { + if (!_blocker.CanUse(user)) return false; + return component.Activated ? TurnOff(component) : TurnOn(user, component); + } + + private void OnExamine(EntityUid uid, HandheldLightComponent component, ExaminedEvent args) + { + args.PushMarkup(component.Activated + ? Loc.GetString("handheld-light-component-on-examine-is-on-message") + : Loc.GetString("handheld-light-component-on-examine-is-off-message")); + } + + public override void Shutdown() + { + base.Shutdown(); + _activeLights.Clear(); + } + + public override void Update(float frameTime) + { + var toRemove = new RemQueue(); + + foreach (var handheld in _activeLights) + { + if (handheld.Deleted) + { + toRemove.Add(handheld); + continue; + } + + if (handheld.Paused) continue; + TryUpdate(handheld, frameTime); + } + + foreach (var light in toRemove) + { + _activeLights.Remove(light); + } + } + + private void AddToggleLightVerb(EntityUid uid, HandheldLightComponent component, GetActivationVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) return; + + Verb verb = new() + { + Text = Loc.GetString("verb-common-toggle-light"), + IconTexture = "/Textures/Interface/VerbIcons/light.svg.192dpi.png", + Act = component.Activated + ? () => TurnOff(component) + : () => TurnOn(args.User, component) + }; + + args.Verbs.Add(verb); + } + + public bool TurnOff(HandheldLightComponent component, bool makeNoise = true) + { + if (!component.Activated) return false; + + SetState(component, false); + component.Activated = false; + UpdateLightAction(component); + _activeLights.Remove(component); + component.LastLevel = null; + component.Dirty(EntityManager); + + if (makeNoise) + SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOffSound.GetSound(), component.Owner); + + return true; + } + + public bool TurnOn(EntityUid user, HandheldLightComponent component) + { + if (component.Activated) return false; + + if (component.Cell == null) + { + SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOnFailSound.GetSound(), component.Owner); + _popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), component.Owner, Filter.Entities(user)); + UpdateLightAction(component); + return false; + } + + // To prevent having to worry about frame time in here. + // Let's just say you need a whole second of charge before you can turn it on. + // Simple enough. + if (component.Wattage > component.Cell.CurrentCharge) + { + SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOnFailSound.GetSound(), component.Owner); + _popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), component.Owner, Filter.Entities(user)); + UpdateLightAction(component); + return false; + } + + component.Activated = true; + UpdateLightAction(component); + SetState(component, true); + _activeLights.Add(component); + component.LastLevel = GetLevel(component); + component.Dirty(EntityManager); + + SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOnSound.GetSound(), component.Owner); + return true; + } + + private void SetState(HandheldLightComponent component, bool on) + { + // TODO: Oh dear + if (EntityManager.TryGetComponent(component.Owner, out SpriteComponent? sprite)) + { + sprite.LayerSetVisible(1, on); + } + + if (EntityManager.TryGetComponent(component.Owner, out PointLightComponent? light)) + { + light.Enabled = on; + } + + if (EntityManager.TryGetComponent(component.Owner, out ClothingComponent? clothing)) + { + clothing.ClothingEquippedPrefix = Loc.GetString(on ? "on" : "off"); + } + + if (EntityManager.TryGetComponent(component.Owner, out ItemComponent? item)) + { + item.EquippedPrefix = Loc.GetString(on ? "on" : "off"); + } + } + + private void UpdateLightAction(HandheldLightComponent component) + { + if (!EntityManager.TryGetComponent(component.Owner, out ItemActionsComponent? actions)) return; + + actions.Toggle(ItemActionType.ToggleLight, component.Activated); + } + + public void TryUpdate(HandheldLightComponent component, float frameTime) + { + if (component.Cell == null) + { + TurnOff(component, false); + return; + } + + var appearanceComponent = EntityManager.GetComponent(component.Owner); + + if (component.Cell.MaxCharge - component.Cell.CurrentCharge < component.Cell.MaxCharge * 0.70) + { + appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower); + } + else if (component.Cell.MaxCharge - component.Cell.CurrentCharge < component.Cell.MaxCharge * 0.90) + { + appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.LowPower); + } + else + { + appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.Dying); + } + + if (component.Activated && !component.Cell.TryUseCharge(component.Wattage * frameTime)) TurnOff(component, false); + + var level = GetLevel(component); + + if (level != component.LastLevel) + { + component.LastLevel = level; + component.Dirty(EntityManager); + } + } + } +} diff --git a/Content.Shared/Light/Component/SharedHandheldLightComponent.cs b/Content.Shared/Light/Component/SharedHandheldLightComponent.cs index f377a733f9..54f218912a 100644 --- a/Content.Shared/Light/Component/SharedHandheldLightComponent.cs +++ b/Content.Shared/Light/Component/SharedHandheldLightComponent.cs @@ -5,17 +5,16 @@ using Robust.Shared.Serialization; namespace Content.Shared.Light.Component { - [NetworkedComponent()] + [NetworkedComponent] + [ComponentProtoName("HandheldLight")] public abstract class SharedHandheldLightComponent : Robust.Shared.GameObjects.Component { - public sealed override string Name => "HandheldLight"; - protected abstract bool HasCell { get; } - protected const int StatusLevels = 6; + public const int StatusLevels = 6; [Serializable, NetSerializable] - protected sealed class HandheldLightComponentState : ComponentState + public sealed class HandheldLightComponentState : ComponentState { public byte? Charge { get; }