From aba9b0e3f93789bd168ebe0cc7c9419263da0dd4 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sat, 10 Oct 2020 22:59:17 +1100 Subject: [PATCH] Make DoAfter visible to all (#2183) Co-authored-by: Metal Gear Sloth --- .../Components/DoAfterComponent.cs | 108 ++++++++--- .../EntitySystems/DoAfter/DoAfterGui.cs | 58 +++--- .../EntitySystems/DoAfter/DoAfterSystem.cs | 181 +++++++++--------- .../Components/DoAfterComponent.cs | 81 ++------ .../Components/SharedDoAfterComponent.cs | 16 +- 5 files changed, 217 insertions(+), 227 deletions(-) diff --git a/Content.Client/GameObjects/Components/DoAfterComponent.cs b/Content.Client/GameObjects/Components/DoAfterComponent.cs index e751f5a996..1dd735490d 100644 --- a/Content.Client/GameObjects/Components/DoAfterComponent.cs +++ b/Content.Client/GameObjects/Components/DoAfterComponent.cs @@ -3,10 +3,7 @@ using System; using System.Collections.Generic; using Content.Client.GameObjects.EntitySystems.DoAfter; using Content.Shared.GameObjects.Components; -using Robust.Client.GameObjects; using Robust.Shared.GameObjects; -using Robust.Shared.GameObjects.Systems; -using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; @@ -19,62 +16,112 @@ namespace Content.Client.GameObjects.Components { public override string Name => "DoAfter"; - public IReadOnlyDictionary DoAfters => _doAfters; - private readonly Dictionary _doAfters = new Dictionary(); + public IReadOnlyDictionary DoAfters => _doAfters; + private readonly Dictionary _doAfters = new Dictionary(); - public readonly List<(TimeSpan CancelTime, DoAfterMessage Message)> CancelledDoAfters = - new List<(TimeSpan CancelTime, DoAfterMessage Message)>(); + public readonly List<(TimeSpan CancelTime, ClientDoAfter Message)> CancelledDoAfters = + new List<(TimeSpan CancelTime, ClientDoAfter Message)>(); + + public DoAfterGui? Gui { get; set; } public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null) { base.HandleNetworkMessage(message, netChannel, session); switch (message) { - case DoAfterMessage msg: - _doAfters.Add(msg.ID, msg); - EntitySystem.Get().Gui?.AddDoAfter(msg); - break; case CancelledDoAfterMessage msg: Cancel(msg.ID); break; } } - public override void HandleMessage(ComponentMessage message, IComponent? component) + public override void OnAdd() { - base.HandleMessage(message, component); - switch (message) + base.OnAdd(); + Enable(); + } + + public override void OnRemove() + { + base.OnRemove(); + Disable(); + } + + /// + /// For handling PVS so we dispose of controls if they go out of range + /// + public void Enable() + { + if (Gui != null && !Gui.Disposed) + return; + + Gui = new DoAfterGui {AttachedEntity = Owner}; + + foreach (var (_, doAfter) in _doAfters) { - case PlayerDetachedMsg _: - _doAfters.Clear(); - CancelledDoAfters.Clear(); - break; + Gui.AddDoAfter(doAfter); + } + + foreach (var (_, cancelled) in CancelledDoAfters) + { + Gui.CancelDoAfter(cancelled.ID); + } + } + + public void Disable() + { + Gui?.Dispose(); + } + + public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) + { + base.HandleComponentState(curState, nextState); + if (!(curState is DoAfterComponentState state)) + return; + + _doAfters.Clear(); + + foreach (var doAfter in state.DoAfters) + { + _doAfters.Add(doAfter.ID, doAfter); + } + + if (Gui == null || Gui.Disposed) + return; + + foreach (var (_, doAfter) in _doAfters) + { + Gui.AddDoAfter(doAfter); } } /// /// Remove a DoAfter without showing a cancellation graphic. /// - /// - public void Remove(DoAfterMessage doAfter) + /// + public void Remove(ClientDoAfter clientDoAfter) { - if (_doAfters.ContainsKey(doAfter.ID)) - { - _doAfters.Remove(doAfter.ID); - } + if (_doAfters.ContainsKey(clientDoAfter.ID)) + _doAfters.Remove(clientDoAfter.ID); + var found = false; + for (var i = CancelledDoAfters.Count - 1; i >= 0; i--) { var cancelled = CancelledDoAfters[i]; - if (cancelled.Message == doAfter) + if (cancelled.Message == clientDoAfter) { CancelledDoAfters.RemoveAt(i); + found = true; break; } } - - EntitySystem.Get().Gui?.RemoveDoAfter(doAfter.ID); + + if (!found) + _doAfters.Remove(clientDoAfter.ID); + + Gui?.RemoveDoAfter(clientDoAfter.ID); } /// @@ -88,15 +135,16 @@ namespace Content.Client.GameObjects.Components foreach (var (_, cancelled) in CancelledDoAfters) { if (cancelled.ID == id) - { return; - } } + if (!_doAfters.ContainsKey(id)) + return; + var doAfterMessage = _doAfters[id]; currentTime ??= IoCManager.Resolve().CurTime; CancelledDoAfters.Add((currentTime.Value, doAfterMessage)); - EntitySystem.Get().Gui?.CancelDoAfter(id); + Gui?.CancelDoAfter(id); } } } \ No newline at end of file diff --git a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterGui.cs b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterGui.cs index 4be9d551ea..5228ab4bd1 100644 --- a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterGui.cs +++ b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterGui.cs @@ -32,7 +32,7 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter // This behavior probably shouldn't be happening; so for whatever reason the control position is set the frame after // I got NFI why because I don't know the UI internals - private bool _firstDraw = true; + public bool FirstDraw { get; set; } public DoAfterGui() { @@ -43,21 +43,18 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin); } - /// - /// Called when the mind is detached from an entity - /// - /// Rather than just dispose of the Gui we'll just remove its child controls and re-use the control. - public void Detached() + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + if (Disposed) + return; + foreach (var (_, control) in _doAfterControls) { control.Dispose(); } + _doAfterControls.Clear(); - foreach (var (_, control) in _doAfterBars) - { - control.Dispose(); - } _doAfterBars.Clear(); _cancelledDoAfters.Clear(); } @@ -66,12 +63,10 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter /// Add the necessary control for a DoAfter progress bar. /// /// - public void AddDoAfter(DoAfterMessage message) + public void AddDoAfter(ClientDoAfter message) { if (_doAfterControls.ContainsKey(message.ID)) - { return; - } var doAfterBar = new DoAfterBar { @@ -108,18 +103,16 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter public void RemoveDoAfter(byte id) { if (!_doAfterControls.ContainsKey(id)) - { return; - } var control = _doAfterControls[id]; RemoveChild(control); _doAfterControls.Remove(id); _doAfterBars.Remove(id); + if (_cancelledDoAfters.ContainsKey(id)) - { _cancelledDoAfters.Remove(id); - } + } /// @@ -130,12 +123,15 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter public void CancelDoAfter(byte id) { if (_cancelledDoAfters.ContainsKey(id)) - { return; - } - var control = _doAfterControls[id]; - _doAfterBars[id].Cancelled = true; + if (!_doAfterBars.TryGetValue(id, out var doAfterBar)) + { + doAfterBar = new DoAfterBar(); + _doAfterBars[id] = doAfterBar; + } + + doAfterBar.Cancelled = true; _cancelledDoAfters.Add(id, _gameTiming.CurTime); } @@ -143,28 +139,24 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter { base.FrameUpdate(args); - if (AttachedEntity?.IsValid() != true || !AttachedEntity.TryGetComponent(out DoAfterComponent? doAfterComponent)) + if (AttachedEntity?.IsValid() != true || + !AttachedEntity.TryGetComponent(out DoAfterComponent? doAfterComponent)) { return; } var doAfters = doAfterComponent.DoAfters; - - // Nothing to render so we'll hide. - if (doAfters.Count == 0 && _cancelledDoAfters.Count == 0) - { - _firstDraw = true; - Visible = false; + if (doAfters.Count == 0) return; - } // Set position ready for 2nd+ frames. _playerPosition = _eyeManager.CoordinatesToScreen(AttachedEntity.Transform.Coordinates); LayoutContainer.SetPosition(this, new Vector2(_playerPosition.X - Width / 2, _playerPosition.Y - Height - 30.0f)); - if (_firstDraw) + if (FirstDraw) { - _firstDraw = false; + Visible = false; + FirstDraw = false; return; } @@ -176,9 +168,7 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter foreach (var (id, cancelTime) in _cancelledDoAfters) { if ((currentTime - cancelTime).TotalSeconds > DoAfterSystem.ExcessTime) - { toCancel.Add(id); - } } foreach (var id in toCancel) @@ -190,9 +180,7 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter foreach (var (id, message) in doAfters) { if (_cancelledDoAfters.ContainsKey(id) || !_doAfterControls.ContainsKey(id)) - { continue; - } var doAfterBar = _doAfterBars[id]; doAfterBar.Ratio = MathF.Min(1.0f, diff --git a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs index 1f048e327f..686714b14f 100644 --- a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs +++ b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterSystem.cs @@ -1,6 +1,8 @@ #nullable enable +using System.Collections.Generic; using System.Linq; using Content.Client.GameObjects.Components; +using Content.Shared.GameObjects.EntitySystems; using JetBrains.Annotations; using Robust.Client.GameObjects.EntitySystems; using Robust.Shared.GameObjects.Systems; @@ -27,133 +29,132 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IEntityManager _entityManager = default!; - /// - /// Rather than checking attached player every tick we'll just store it from the message. - /// - private IEntity? _player; - /// /// We'll use an excess time so stuff like finishing effects can show. /// public const float ExcessTime = 0.5f; - public DoAfterGui? Gui { get; private set; } + // Each component in range will have its own vBox which we need to keep track of so if they go out of range or + // come into range it needs altering + private HashSet _knownComponents = new HashSet(); + private IEntity? _attachedEntity; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(message => HandlePlayerAttached(message.AttachedEntity)); + SubscribeLocalEvent(HandlePlayerAttached); } - public override void Shutdown() + private void HandlePlayerAttached(PlayerAttachSysMessage message) { - base.Shutdown(); - Gui?.Dispose(); - Gui = null; - _player = null; + _attachedEntity = message.AttachedEntity; } - - private void HandlePlayerAttached(IEntity? entity) - { - _player = entity; - // Setup the GUI and pass the new data to it if applicable. - Gui?.Detached(); - - if (entity == null) - { - return; - } - - Gui ??= new DoAfterGui(); - Gui.AttachedEntity = entity; - - if (entity.TryGetComponent(out DoAfterComponent? doAfterComponent)) - { - foreach (var (_, doAfter) in doAfterComponent.DoAfters) - { - Gui.AddDoAfter(doAfter); - } - } - } - + public override void Update(float frameTime) { base.Update(frameTime); var currentTime = _gameTiming.CurTime; - - if (_player?.IsValid() != true) - { + var foundComps = new HashSet(); + + // Can't see any I guess? + if (_attachedEntity == null || _attachedEntity.Deleted) return; - } - if (!_player.TryGetComponent(out DoAfterComponent? doAfterComponent)) + foreach (var comp in ComponentManager.EntityQuery()) { - return; - } - - var doAfters = doAfterComponent.DoAfters.ToList(); - if (doAfters.Count == 0) - { - return; - } - - var userGrid = _player.Transform.Coordinates; - - // Check cancellations / finishes - foreach (var (id, doAfter) in doAfters) - { - var elapsedTime = (currentTime - doAfter.StartTime).TotalSeconds; - - // If we've passed the final time (after the excess to show completion graphic) then remove. - if (elapsedTime > doAfter.Delay + ExcessTime) + if (!_knownComponents.Contains(comp)) { - Gui?.RemoveDoAfter(id); - doAfterComponent.Remove(doAfter); - continue; + _knownComponents.Add(comp); + } + + var doAfters = comp.DoAfters.ToList(); + + if (doAfters.Count == 0) + { + if (comp.Gui != null) + comp.Gui.FirstDraw = true; + + return; } - // Don't predict cancellation if it's already finished. - if (elapsedTime > doAfter.Delay) - { - continue; - } + var range = (comp.Owner.Transform.WorldPosition - _attachedEntity.Transform.WorldPosition).Length + 0.01f; - // Predictions - if (doAfter.BreakOnUserMove) + if (comp.Owner != _attachedEntity && !ExamineSystemShared.InRangeUnOccluded( + _attachedEntity.Transform.MapPosition, + comp.Owner.Transform.MapPosition, range, + entity => entity == comp.Owner || entity == _attachedEntity)) { - if (userGrid != doAfter.UserGrid) + if (comp.Gui != null) + comp.Gui.FirstDraw = true; + + return; + } + + comp.Enable(); + + var userGrid = comp.Owner.Transform.Coordinates; + + // Check cancellations / finishes + foreach (var (id, doAfter) in doAfters) + { + var elapsedTime = (currentTime - doAfter.StartTime).TotalSeconds; + + // If we've passed the final time (after the excess to show completion graphic) then remove. + if (elapsedTime > doAfter.Delay + ExcessTime) { - doAfterComponent.Cancel(id, currentTime); - continue; - } - } - - if (doAfter.BreakOnTargetMove) - { - if (!_entityManager.TryGetEntity(doAfter.TargetUid, out var targetEntity)) - { - // Cancel if the target entity doesn't exist. - doAfterComponent.Cancel(id, currentTime); + comp.Remove(doAfter); continue; } - if (targetEntity.Transform.Coordinates != doAfter.TargetGrid) - { - doAfterComponent.Cancel(id, currentTime); + // Don't predict cancellation if it's already finished. + if (elapsedTime > doAfter.Delay) continue; + + // Predictions + if (doAfter.BreakOnUserMove) + { + if (userGrid != doAfter.UserGrid) + { + comp.Cancel(id, currentTime); + continue; + } + } + + if (doAfter.BreakOnTargetMove) + { + var targetEntity = _entityManager.GetEntity(doAfter.TargetUid); + + if (targetEntity.Transform.Coordinates != doAfter.TargetGrid) + { + comp.Cancel(id, currentTime); + continue; + } } } + + var count = comp.CancelledDoAfters.Count; + // Remove cancelled DoAfters after ExcessTime has elapsed + for (var i = count - 1; i >= 0; i--) + { + var cancelled = comp.CancelledDoAfters[i]; + if ((currentTime - cancelled.CancelTime).TotalSeconds > ExcessTime) + { + comp.Remove(cancelled.Message); + } + } + + // Remove any components that we no longer need to track + foundComps.Add(comp); } - var count = doAfterComponent.CancelledDoAfters.Count; - // Remove cancelled DoAfters after ExcessTime has elapsed - for (var i = count - 1; i >= 0; i--) + foreach (var comp in foundComps) { - var cancelled = doAfterComponent.CancelledDoAfters[i]; - if ((currentTime - cancelled.CancelTime).TotalSeconds > ExcessTime) + if (!_knownComponents.Contains(comp)) { - doAfterComponent.Remove(cancelled.Message); + _knownComponents.Remove(comp); + comp.Disable(); } } } diff --git a/Content.Server/GameObjects/Components/DoAfterComponent.cs b/Content.Server/GameObjects/Components/DoAfterComponent.cs index 2013028ae6..5ae0a4ce83 100644 --- a/Content.Server/GameObjects/Components/DoAfterComponent.cs +++ b/Content.Server/GameObjects/Components/DoAfterComponent.cs @@ -2,11 +2,7 @@ using System.Collections.Generic; using Content.Server.GameObjects.EntitySystems.DoAfter; using Content.Shared.GameObjects.Components; -using Robust.Server.GameObjects; -using Robust.Server.Interfaces.GameObjects; using Robust.Shared.GameObjects; -using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Interfaces.Network; namespace Content.Server.GameObjects.Components { @@ -20,30 +16,15 @@ namespace Content.Server.GameObjects.Components // we'll just send them the index. Doesn't matter if it wraps around. private byte _runningIndex; - public override void HandleMessage(ComponentMessage message, IComponent? component) + public override ComponentState GetComponentState() { - base.HandleMessage(message, component); - switch (message) - { - case PlayerAttachedMsg _: - UpdateClient(); - break; - } - } - - // Only sending data to the relevant client (at least, other clients don't need to know about do_after for now). - private void UpdateClient() - { - if (!TryGetConnectedClient(out var connectedClient)) - { - return; - } - - foreach (var (doAfter, id) in _doAfters) + var toAdd = new List(); + + foreach (var doAfter in DoAfters) { // THE ALMIGHTY PYRAMID - var message = new DoAfterMessage( - id, + var clientDoAfter = new ClientDoAfter( + _doAfters[doAfter], doAfter.UserGrid, doAfter.TargetGrid, doAfter.StartTime, @@ -51,65 +32,27 @@ namespace Content.Server.GameObjects.Components doAfter.EventArgs.BreakOnUserMove, doAfter.EventArgs.BreakOnTargetMove, doAfter.EventArgs.Target?.Uid ?? EntityUid.Invalid); - - SendNetworkMessage(message, connectedClient); - } - } - - private bool TryGetConnectedClient(out INetChannel? connectedClient) - { - connectedClient = null; - - if (!Owner.TryGetComponent(out IActorComponent? actorComponent)) - { - return false; + + toAdd.Add(clientDoAfter); } - connectedClient = actorComponent.playerSession.ConnectedClient; - if (!connectedClient.IsConnected) - { - return false; - } - - return true; + return new DoAfterComponentState(toAdd); } public void Add(DoAfter doAfter) { _doAfters.Add(doAfter, _runningIndex); - - if (TryGetConnectedClient(out var connectedClient)) - { - var message = new DoAfterMessage( - _runningIndex, - doAfter.UserGrid, - doAfter.TargetGrid, - doAfter.StartTime, - doAfter.EventArgs.Delay, - doAfter.EventArgs.BreakOnUserMove, - doAfter.EventArgs.BreakOnTargetMove, - doAfter.EventArgs.Target?.Uid ?? EntityUid.Invalid); - - SendNetworkMessage(message, connectedClient); - } - _runningIndex++; + Dirty(); } public void Cancelled(DoAfter doAfter) { if (!_doAfters.TryGetValue(doAfter, out var index)) - { return; - } - - if (TryGetConnectedClient(out var connectedClient)) - { - var message = new CancelledDoAfterMessage(index); - SendNetworkMessage(message, connectedClient); - } _doAfters.Remove(doAfter); + SendNetworkMessage(new CancelledDoAfterMessage(index)); } /// @@ -120,9 +63,7 @@ namespace Content.Server.GameObjects.Components public void Finished(DoAfter doAfter) { if (!_doAfters.ContainsKey(doAfter)) - { return; - } _doAfters.Remove(doAfter); } diff --git a/Content.Shared/GameObjects/Components/SharedDoAfterComponent.cs b/Content.Shared/GameObjects/Components/SharedDoAfterComponent.cs index d8be76081f..bd1c506b7e 100644 --- a/Content.Shared/GameObjects/Components/SharedDoAfterComponent.cs +++ b/Content.Shared/GameObjects/Components/SharedDoAfterComponent.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Serialization; @@ -12,6 +13,17 @@ namespace Content.Shared.GameObjects.Components public override uint? NetID => ContentNetIDs.DO_AFTER; } + [Serializable, NetSerializable] + public sealed class DoAfterComponentState : ComponentState + { + public List DoAfters { get; } + + public DoAfterComponentState(List doAfters) : base(ContentNetIDs.DO_AFTER) + { + DoAfters = doAfters; + } + } + [Serializable, NetSerializable] public sealed class CancelledDoAfterMessage : ComponentMessage { @@ -27,7 +39,7 @@ namespace Content.Shared.GameObjects.Components /// We send a trimmed-down version of the DoAfter for the client for it to use. /// [Serializable, NetSerializable] - public sealed class DoAfterMessage : ComponentMessage + public sealed class ClientDoAfter { // To see what these do look at DoAfter and DoAfterEventArgs public byte ID { get; } @@ -47,7 +59,7 @@ namespace Content.Shared.GameObjects.Components public bool BreakOnTargetMove { get; } - public DoAfterMessage(byte id, EntityCoordinates userGrid, EntityCoordinates targetGrid, TimeSpan startTime, float delay, bool breakOnUserMove, bool breakOnTargetMove, EntityUid targetUid = default) + public ClientDoAfter(byte id, EntityCoordinates userGrid, EntityCoordinates targetGrid, TimeSpan startTime, float delay, bool breakOnUserMove, bool breakOnTargetMove, EntityUid targetUid = default) { ID = id; UserGrid = userGrid;