do_after (#1616)
* do_after Ports (most of) do_after from SS13. Callers are expected to await the DoAfter task from the DoAfterSystem. I had a dummy component for in-game testing which I removed for the PR so nothing in game uses do_after at the moment. Currently only the movement cancellation is predicted client-side. * Minor do_after doc cleanup * do_the_shuffle Fix nullable build errors. * The last nullable * Implement NeedHand Thanks zum. * nullable dereference * Adjust the system query Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
This commit is contained in:
88
Content.Client/GameObjects/Components/DoAfterComponent.cs
Normal file
88
Content.Client/GameObjects/Components/DoAfterComponent.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Client.GameObjects.EntitySystems.DoAfter;
|
||||||
|
using Content.Shared.GameObjects.Components;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects.Systems;
|
||||||
|
using Robust.Shared.Interfaces.Network;
|
||||||
|
using Robust.Shared.Interfaces.Timing;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Players;
|
||||||
|
|
||||||
|
namespace Content.Client.GameObjects.Components
|
||||||
|
{
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class DoAfterComponent : SharedDoAfterComponent
|
||||||
|
{
|
||||||
|
public override string Name => "DoAfter";
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<byte, DoAfterMessage> DoAfters => _doAfters;
|
||||||
|
private readonly Dictionary<byte, DoAfterMessage> _doAfters = new Dictionary<byte, DoAfterMessage>();
|
||||||
|
|
||||||
|
public readonly List<(TimeSpan CancelTime, DoAfterMessage Message)> CancelledDoAfters =
|
||||||
|
new List<(TimeSpan CancelTime, DoAfterMessage Message)>();
|
||||||
|
|
||||||
|
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<DoAfterSystem>().Gui?.AddDoAfter(msg);
|
||||||
|
break;
|
||||||
|
case CancelledDoAfterMessage msg:
|
||||||
|
Cancel(msg.ID);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a DoAfter without showing a cancellation graphic.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="doAfter"></param>
|
||||||
|
public void Remove(DoAfterMessage doAfter)
|
||||||
|
{
|
||||||
|
if (_doAfters.ContainsKey(doAfter.ID))
|
||||||
|
{
|
||||||
|
_doAfters.Remove(doAfter.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = CancelledDoAfters.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var cancelled = CancelledDoAfters[i];
|
||||||
|
|
||||||
|
if (cancelled.Message == doAfter)
|
||||||
|
{
|
||||||
|
CancelledDoAfters.RemoveAt(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EntitySystem.Get<DoAfterSystem>().Gui?.RemoveDoAfter(doAfter.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark a DoAfter as cancelled and show a cancellation graphic.
|
||||||
|
/// </summary>
|
||||||
|
/// Actual removal is handled by DoAfterEntitySystem.
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <param name="currentTime"></param>
|
||||||
|
public void Cancel(byte id, TimeSpan? currentTime = null)
|
||||||
|
{
|
||||||
|
foreach (var (_, cancelled) in CancelledDoAfters)
|
||||||
|
{
|
||||||
|
if (cancelled.ID == id)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var doAfterMessage = _doAfters[id];
|
||||||
|
currentTime ??= IoCManager.Resolve<IGameTiming>().CurTime;
|
||||||
|
CancelledDoAfters.Add((currentTime.Value, doAfterMessage));
|
||||||
|
EntitySystem.Get<DoAfterSystem>().Gui?.CancelDoAfter(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterBar.cs
Normal file
136
Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterBar.cs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using Robust.Client.Graphics.Drawing;
|
||||||
|
using Robust.Client.Graphics.Shaders;
|
||||||
|
using Robust.Client.UserInterface;
|
||||||
|
using Robust.Shared.Interfaces.Timing;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
|
namespace Content.Client.GameObjects.EntitySystems.DoAfter
|
||||||
|
{
|
||||||
|
public sealed class DoAfterBar : Control
|
||||||
|
{
|
||||||
|
private IGameTiming _gameTiming = default!;
|
||||||
|
|
||||||
|
private ShaderInstance _shader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set from 0.0f to 1.0f to reflect bar progress
|
||||||
|
/// </summary>
|
||||||
|
public float Ratio
|
||||||
|
{
|
||||||
|
get => _ratio;
|
||||||
|
set => _ratio = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float _ratio = 1.0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flash red until removed
|
||||||
|
/// </summary>
|
||||||
|
public bool Cancelled
|
||||||
|
{
|
||||||
|
get => _cancelled;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_cancelled == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cancelled = value;
|
||||||
|
if (_cancelled)
|
||||||
|
{
|
||||||
|
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||||
|
_lastFlash = _gameTiming.CurTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _cancelled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the cancellation bar red?
|
||||||
|
/// </summary>
|
||||||
|
private bool _flash = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Last time we swapped the flash.
|
||||||
|
/// </summary>
|
||||||
|
private TimeSpan _lastFlash;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long each cancellation bar flash lasts in seconds.
|
||||||
|
/// </summary>
|
||||||
|
private const float FlashTime = 0.125f;
|
||||||
|
|
||||||
|
private const int XPixelDiff = 20 * DoAfterBarScale;
|
||||||
|
|
||||||
|
public const byte DoAfterBarScale = 2;
|
||||||
|
private static readonly Color StartColor = new Color(0.8f, 0.0f, 0.2f);
|
||||||
|
private static readonly Color EndColor = new Color(0.2f, 0.4f, 1.0f);
|
||||||
|
|
||||||
|
public DoAfterBar()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
_shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void FrameUpdate(FrameEventArgs args)
|
||||||
|
{
|
||||||
|
base.FrameUpdate(args);
|
||||||
|
if (Cancelled)
|
||||||
|
{
|
||||||
|
if ((_gameTiming.CurTime - _lastFlash).TotalSeconds > FlashTime)
|
||||||
|
{
|
||||||
|
_lastFlash = _gameTiming.CurTime;
|
||||||
|
_flash = !_flash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Draw(DrawingHandleScreen handle)
|
||||||
|
{
|
||||||
|
base.Draw(handle);
|
||||||
|
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
if (Cancelled)
|
||||||
|
{
|
||||||
|
if ((_gameTiming.CurTime - _lastFlash).TotalSeconds > FlashTime)
|
||||||
|
{
|
||||||
|
_lastFlash = _gameTiming.CurTime;
|
||||||
|
_flash = !_flash;
|
||||||
|
}
|
||||||
|
|
||||||
|
color = new Color(1.0f, 0.0f, 0.0f, _flash ? 1.0f : 0.0f);
|
||||||
|
}
|
||||||
|
else if (Ratio >= 1.0f)
|
||||||
|
{
|
||||||
|
color = new Color(0.92f, 0.77f, 0.34f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// lerp
|
||||||
|
color = new Color(
|
||||||
|
StartColor.R + (EndColor.R - StartColor.R) * Ratio,
|
||||||
|
StartColor.G + (EndColor.G - StartColor.G) * Ratio,
|
||||||
|
StartColor.B + (EndColor.B - StartColor.B) * Ratio,
|
||||||
|
StartColor.A);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.UseShader(_shader);
|
||||||
|
// If you want to make this less hard-coded be my guest
|
||||||
|
var leftOffset = 2 * DoAfterBarScale;
|
||||||
|
var box = new UIBox2i(
|
||||||
|
leftOffset,
|
||||||
|
-2 + 2 * DoAfterBarScale,
|
||||||
|
leftOffset + (int) (XPixelDiff * Ratio),
|
||||||
|
-2);
|
||||||
|
handle.DrawRect(box, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterGui.cs
Normal file
184
Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterGui.cs
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Client.GameObjects.Components;
|
||||||
|
using Content.Client.Utility;
|
||||||
|
using Content.Shared.GameObjects.Components;
|
||||||
|
using Robust.Client.Interfaces.Graphics.ClientEye;
|
||||||
|
using Robust.Client.Interfaces.UserInterface;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Shared.Interfaces.GameObjects;
|
||||||
|
using Robust.Shared.Interfaces.Timing;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
|
namespace Content.Client.GameObjects.EntitySystems.DoAfter
|
||||||
|
{
|
||||||
|
public sealed class DoAfterGui : VBoxContainer
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
|
|
||||||
|
private Dictionary<byte, PanelContainer> _doAfterControls = new Dictionary<byte, PanelContainer>();
|
||||||
|
private Dictionary<byte, DoAfterBar> _doAfterBars = new Dictionary<byte, DoAfterBar>();
|
||||||
|
|
||||||
|
// We'll store cancellations for a little bit just so we can flash the graphic to indicate it's cancelled
|
||||||
|
private Dictionary<byte, TimeSpan> _cancelledDoAfters = new Dictionary<byte, TimeSpan>();
|
||||||
|
|
||||||
|
public IEntity? AttachedEntity { get; set; }
|
||||||
|
private ScreenCoordinates _playerPosition;
|
||||||
|
|
||||||
|
// 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 DoAfterGui()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(this);
|
||||||
|
SeparationOverride = 0;
|
||||||
|
|
||||||
|
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add the necessary control for a DoAfter progress bar.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
public void AddDoAfter(DoAfterMessage message)
|
||||||
|
{
|
||||||
|
if (_doAfterControls.ContainsKey(message.ID))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var doAfterBar = new DoAfterBar
|
||||||
|
{
|
||||||
|
SizeFlagsVertical = SizeFlags.ShrinkCenter
|
||||||
|
};
|
||||||
|
|
||||||
|
_doAfterBars[message.ID] = doAfterBar;
|
||||||
|
|
||||||
|
var control = new PanelContainer
|
||||||
|
{
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new TextureRect
|
||||||
|
{
|
||||||
|
Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png"),
|
||||||
|
TextureScale = Vector2.One * DoAfterBar.DoAfterBarScale,
|
||||||
|
SizeFlagsVertical = SizeFlags.ShrinkCenter,
|
||||||
|
},
|
||||||
|
|
||||||
|
doAfterBar
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AddChild(control);
|
||||||
|
_doAfterControls.Add(message.ID, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE THAT THE BELOW ONLY HANDLES THE UI SIDE
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a DoAfter without showing a cancel graphic.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancels a DoAfter and shows a graphic indicating it has been cancelled to the player.
|
||||||
|
/// </summary>
|
||||||
|
/// Can be called multiple times on the 1 DoAfter because of the client predicting the cancellation.
|
||||||
|
/// <param name="id"></param>
|
||||||
|
public void CancelDoAfter(byte id)
|
||||||
|
{
|
||||||
|
if (_cancelledDoAfters.ContainsKey(id))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var control = _doAfterControls[id];
|
||||||
|
_doAfterBars[id].Cancelled = true;
|
||||||
|
_cancelledDoAfters.Add(id, _gameTiming.CurTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void FrameUpdate(FrameEventArgs args)
|
||||||
|
{
|
||||||
|
base.FrameUpdate(args);
|
||||||
|
|
||||||
|
if (AttachedEntity == null || !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;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set position ready for 2nd+ frames.
|
||||||
|
_playerPosition = _eyeManager.WorldToScreen(AttachedEntity.Transform.GridPosition);
|
||||||
|
LayoutContainer.SetPosition(this, new Vector2(_playerPosition.X - Width / 2, _playerPosition.Y - Height - 30.0f));
|
||||||
|
|
||||||
|
if (_firstDraw)
|
||||||
|
{
|
||||||
|
_firstDraw = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Visible = true;
|
||||||
|
var currentTime = _gameTiming.CurTime;
|
||||||
|
var toCancel = new List<byte>();
|
||||||
|
|
||||||
|
// Cleanup cancelled DoAfters
|
||||||
|
foreach (var (id, cancelTime) in _cancelledDoAfters)
|
||||||
|
{
|
||||||
|
if ((currentTime - cancelTime).TotalSeconds > DoAfterSystem.ExcessTime)
|
||||||
|
{
|
||||||
|
toCancel.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var id in toCancel)
|
||||||
|
{
|
||||||
|
RemoveDoAfter(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 0 -> 1.0f of the things
|
||||||
|
foreach (var (id, message) in doAfters)
|
||||||
|
{
|
||||||
|
if (_cancelledDoAfters.ContainsKey(id) || !_doAfterControls.ContainsKey(id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var doAfterBar = _doAfterBars[id];
|
||||||
|
doAfterBar.Ratio = MathF.Min(1.0f,
|
||||||
|
(float) (currentTime - message.StartTime).TotalSeconds / message.Delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Client.GameObjects.Components;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Client.GameObjects.EntitySystems;
|
||||||
|
using Robust.Shared.GameObjects.Systems;
|
||||||
|
using Robust.Shared.Interfaces.GameObjects;
|
||||||
|
using Robust.Shared.Interfaces.Timing;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
namespace Content.Client.GameObjects.EntitySystems.DoAfter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles events that need to happen after a certain amount of time where the event could be cancelled by factors
|
||||||
|
/// such as moving.
|
||||||
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class DoAfterSystem : EntitySystem
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* How this is currently setup (client-side):
|
||||||
|
* DoAfterGui handles the actual bars displayed above heads. It also uses FrameUpdate to flash cancellations
|
||||||
|
* DoAfterEntitySystem handles checking predictions every tick as well as removing / cancelling DoAfters due to time elapsed.
|
||||||
|
* DoAfterComponent handles network messages inbound as well as storing the DoAfter data.
|
||||||
|
* It'll also handle overall cleanup when one is removed (i.e. removing it from DoAfterGui).
|
||||||
|
*/
|
||||||
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rather than checking attached player every tick we'll just store it from the message.
|
||||||
|
/// </summary>
|
||||||
|
private IEntity? _player;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// We'll use an excess time so stuff like finishing effects can show.
|
||||||
|
/// </summary>
|
||||||
|
public const float ExcessTime = 0.5f;
|
||||||
|
|
||||||
|
public DoAfterGui? Gui { get; private set; }
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
SubscribeLocalEvent<PlayerAttachSysMessage>(message => HandlePlayerAttached(message.AttachedEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
base.Shutdown();
|
||||||
|
Gui?.Dispose();
|
||||||
|
Gui = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandlePlayerAttached(IEntity? entity)
|
||||||
|
{
|
||||||
|
_player = entity;
|
||||||
|
// Setup the GUI and pass the new data to it if applicable.
|
||||||
|
Gui?.Dispose();
|
||||||
|
|
||||||
|
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 == null || !_player.TryGetComponent(out DoAfterComponent doAfterComponent))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var doAfters = doAfterComponent.DoAfters.ToList();
|
||||||
|
if (doAfters.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userGrid = _player.Transform.GridPosition;
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
Gui?.RemoveDoAfter(id);
|
||||||
|
doAfterComponent.Remove(doAfter);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't predict cancellation if it's already finished.
|
||||||
|
if (elapsedTime > doAfter.Delay)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predictions
|
||||||
|
if (doAfter.BreakOnUserMove)
|
||||||
|
{
|
||||||
|
if (userGrid != doAfter.UserGrid)
|
||||||
|
{
|
||||||
|
doAfterComponent.Cancel(id, currentTime);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doAfter.BreakOnTargetMove)
|
||||||
|
{
|
||||||
|
var targetEntity = _entityManager.GetEntity(doAfter.TargetUid);
|
||||||
|
|
||||||
|
if (targetEntity.Transform.GridPosition != doAfter.TargetGrid)
|
||||||
|
{
|
||||||
|
doAfterComponent.Cancel(id, currentTime);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = doAfterComponent.CancelledDoAfters.Count;
|
||||||
|
// Remove cancelled DoAfters after ExcessTime has elapsed
|
||||||
|
for (var i = count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var cancelled = doAfterComponent.CancelledDoAfters[i];
|
||||||
|
if ((currentTime - cancelled.CancelTime).TotalSeconds > ExcessTime)
|
||||||
|
{
|
||||||
|
doAfterComponent.Remove(cancelled.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
Normal file
60
Content.IntegrationTests/Tests/DoAfter/DoAfterServerTest.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.GameObjects.Components;
|
||||||
|
using Content.Server.GameObjects.EntitySystems;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Robust.Shared.GameObjects.Systems;
|
||||||
|
using Robust.Shared.Interfaces.GameObjects;
|
||||||
|
using Robust.Shared.Interfaces.Map;
|
||||||
|
using Robust.Shared.Interfaces.Timing;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.DoAfter
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
[TestOf(typeof(DoAfterComponent))]
|
||||||
|
public class DoAfterServerTest : ContentIntegrationTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task Test()
|
||||||
|
{
|
||||||
|
Task<DoAfterStatus> task = null;
|
||||||
|
var server = StartServerDummyTicker();
|
||||||
|
float tickTime = 0.0f;
|
||||||
|
|
||||||
|
// That it finishes successfully
|
||||||
|
server.Post(() =>
|
||||||
|
{
|
||||||
|
tickTime = 1.0f / IoCManager.Resolve<IGameTiming>().TickRate;
|
||||||
|
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||||
|
mapManager.CreateNewMapEntity(MapId.Nullspace);
|
||||||
|
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||||
|
var mob = entityManager.SpawnEntity("HumanMob_Content", MapCoordinates.Nullspace);
|
||||||
|
var cancelToken = new CancellationTokenSource();
|
||||||
|
var args = new DoAfterEventArgs(mob, tickTime / 2, cancelToken.Token);
|
||||||
|
task = EntitySystem.Get<DoAfterSystem>().DoAfter(args);
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.WaitRunTicks(1);
|
||||||
|
Assert.That(task.Result == DoAfterStatus.Finished);
|
||||||
|
|
||||||
|
// That cancel works on mob move
|
||||||
|
server.Post(() =>
|
||||||
|
{
|
||||||
|
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||||
|
mapManager.CreateNewMapEntity(MapId.Nullspace);
|
||||||
|
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||||
|
var mob = entityManager.SpawnEntity("HumanMob_Content", MapCoordinates.Nullspace);
|
||||||
|
var cancelToken = new CancellationTokenSource();
|
||||||
|
var args = new DoAfterEventArgs(mob, tickTime * 2, cancelToken.Token);
|
||||||
|
task = EntitySystem.Get<DoAfterSystem>().DoAfter(args);
|
||||||
|
mob.Transform.GridPosition = mob.Transform.GridPosition.Translated(new Vector2(0.1f, 0.1f));
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.WaitRunTicks(1);
|
||||||
|
Assert.That(task.Result == DoAfterStatus.Cancelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
Content.Server/GameObjects/Components/DoAfterComponent.cs
Normal file
130
Content.Server/GameObjects/Components/DoAfterComponent.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Server.GameObjects.EntitySystems;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class DoAfterComponent : SharedDoAfterComponent
|
||||||
|
{
|
||||||
|
public IReadOnlyCollection<DoAfter> DoAfters => _doAfters.Keys;
|
||||||
|
private readonly Dictionary<DoAfter, byte> _doAfters = new Dictionary<DoAfter, byte>();
|
||||||
|
|
||||||
|
// So the client knows which one to update (and so we don't send all of the do_afters every time 1 updates)
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// THE ALMIGHTY PYRAMID
|
||||||
|
var message = new DoAfterMessage(
|
||||||
|
id,
|
||||||
|
doAfter.UserGrid,
|
||||||
|
doAfter.TargetGrid,
|
||||||
|
doAfter.StartTime,
|
||||||
|
doAfter.EventArgs.Delay,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedClient = actorComponent.playerSession.ConnectedClient;
|
||||||
|
if (!connectedClient.IsConnected)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Call when the particular DoAfter is finished.
|
||||||
|
/// Client should be tracking this independently.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="doAfter"></param>
|
||||||
|
public void Finished(DoAfter doAfter)
|
||||||
|
{
|
||||||
|
if (!_doAfters.ContainsKey(doAfter))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_doAfters.Remove(doAfter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
172
Content.Server/GameObjects/EntitySystems/DoAfter/DoAfter.cs
Normal file
172
Content.Server/GameObjects/EntitySystems/DoAfter/DoAfter.cs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.GameObjects.Components;
|
||||||
|
using Content.Server.GameObjects.Components.GUI;
|
||||||
|
using Content.Server.GameObjects.Components.Mobs;
|
||||||
|
using Robust.Shared.Interfaces.Timing;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.GameObjects.EntitySystems
|
||||||
|
{
|
||||||
|
public sealed class DoAfter
|
||||||
|
{
|
||||||
|
public Task<DoAfterStatus> AsTask { get; }
|
||||||
|
|
||||||
|
private TaskCompletionSource<DoAfterStatus> Tcs { get;}
|
||||||
|
|
||||||
|
public DoAfterEventArgs EventArgs;
|
||||||
|
|
||||||
|
public TimeSpan StartTime { get; }
|
||||||
|
|
||||||
|
public float Elapsed { get; set; }
|
||||||
|
|
||||||
|
public GridCoordinates UserGrid { get; }
|
||||||
|
|
||||||
|
public GridCoordinates TargetGrid { get; }
|
||||||
|
|
||||||
|
private bool _tookDamage;
|
||||||
|
|
||||||
|
public DoAfterStatus Status => AsTask.IsCompletedSuccessfully ? AsTask.Result : DoAfterStatus.Running;
|
||||||
|
|
||||||
|
// NeedHand
|
||||||
|
private string? _activeHand;
|
||||||
|
private ItemComponent? _activeItem;
|
||||||
|
|
||||||
|
public DoAfter(DoAfterEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
EventArgs = eventArgs;
|
||||||
|
StartTime = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||||
|
|
||||||
|
if (eventArgs.BreakOnUserMove)
|
||||||
|
{
|
||||||
|
UserGrid = eventArgs.User.Transform.GridPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventArgs.BreakOnTargetMove)
|
||||||
|
{
|
||||||
|
// Target should never be null if the bool is set.
|
||||||
|
TargetGrid = eventArgs.Target!.Transform.GridPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For this we need to stay on the same hand slot and need the same item in that hand slot
|
||||||
|
// (or if there is no item there we need to keep it free).
|
||||||
|
if (eventArgs.NeedHand && eventArgs.User.TryGetComponent(out HandsComponent handsComponent))
|
||||||
|
{
|
||||||
|
_activeHand = handsComponent.ActiveHand;
|
||||||
|
_activeItem = handsComponent.GetActiveHand;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tcs = new TaskCompletionSource<DoAfterStatus>();
|
||||||
|
AsTask = Tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleDamage(object? sender, DamageEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
_tookDamage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run(float frameTime)
|
||||||
|
{
|
||||||
|
switch (Status)
|
||||||
|
{
|
||||||
|
case DoAfterStatus.Running:
|
||||||
|
break;
|
||||||
|
case DoAfterStatus.Cancelled:
|
||||||
|
case DoAfterStatus.Finished:
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
Elapsed += frameTime;
|
||||||
|
|
||||||
|
if (IsFinished())
|
||||||
|
{
|
||||||
|
Tcs.SetResult(DoAfterStatus.Finished);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsCancelled())
|
||||||
|
{
|
||||||
|
Tcs.SetResult(DoAfterStatus.Cancelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsCancelled()
|
||||||
|
{
|
||||||
|
//https://github.com/tgstation/tgstation/blob/1aa293ea337283a0191140a878eeba319221e5df/code/__HELPERS/mobs.dm
|
||||||
|
if (EventArgs.CancelToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO :Handle inertia in space.
|
||||||
|
if (EventArgs.BreakOnUserMove && EventArgs.User.Transform.GridPosition != UserGrid)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EventArgs.BreakOnTargetMove && EventArgs.Target!.Transform.GridPosition != TargetGrid)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EventArgs.BreakOnDamage && _tookDamage)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EventArgs.ExtraCheck != null && !EventArgs.ExtraCheck.Invoke())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EventArgs.BreakOnStun &&
|
||||||
|
EventArgs.User.TryGetComponent(out StunnableComponent stunnableComponent) &&
|
||||||
|
stunnableComponent.Stunned)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EventArgs.NeedHand)
|
||||||
|
{
|
||||||
|
if (!EventArgs.User.TryGetComponent(out HandsComponent handsComponent))
|
||||||
|
{
|
||||||
|
// If we had a hand but no longer have it that's still a paddlin'
|
||||||
|
if (_activeHand != null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var currentActiveHand = handsComponent.ActiveHand;
|
||||||
|
if (_activeHand != currentActiveHand)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentItem = handsComponent.GetActiveHand;
|
||||||
|
if (_activeItem != currentItem)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsFinished()
|
||||||
|
{
|
||||||
|
if (Elapsed <= EventArgs.Delay)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using Robust.Shared.Interfaces.GameObjects;
|
||||||
|
|
||||||
|
namespace Content.Server.GameObjects.EntitySystems
|
||||||
|
{
|
||||||
|
public sealed class DoAfterEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The entity invoking do_after
|
||||||
|
/// </summary>
|
||||||
|
public IEntity User { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long does the do_after require to complete
|
||||||
|
/// </summary>
|
||||||
|
public float Delay { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applicable target (if relevant)
|
||||||
|
/// </summary>
|
||||||
|
public IEntity? Target { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manually cancel the do_after so it no longer runs
|
||||||
|
/// </summary>
|
||||||
|
public CancellationToken CancelToken { get; }
|
||||||
|
|
||||||
|
// Break the chains
|
||||||
|
/// <summary>
|
||||||
|
/// Whether we need to keep our active hand as is (i.e. can't change hand or change item).
|
||||||
|
/// This also covers requiring the hand to be free (if applicable).
|
||||||
|
/// </summary>
|
||||||
|
public bool NeedHand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If do_after stops when the user moves
|
||||||
|
/// </summary>
|
||||||
|
public bool BreakOnUserMove { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If do_after stops when the target moves (if there is a target)
|
||||||
|
/// </summary>
|
||||||
|
public bool BreakOnTargetMove { get; }
|
||||||
|
public bool BreakOnDamage { get; }
|
||||||
|
public bool BreakOnStun { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional conditions that need to be met. Return false to cancel.
|
||||||
|
/// </summary>
|
||||||
|
public Func<bool>? ExtraCheck { get; }
|
||||||
|
|
||||||
|
public DoAfterEventArgs(
|
||||||
|
IEntity user,
|
||||||
|
float delay,
|
||||||
|
CancellationToken cancelToken,
|
||||||
|
IEntity? target = null,
|
||||||
|
bool needHand = true,
|
||||||
|
bool breakOnUserMove = true,
|
||||||
|
bool breakOnTargetMove = true,
|
||||||
|
bool breakOnDamage = true,
|
||||||
|
bool breakOnStun = true,
|
||||||
|
Func<bool>? extraCheck = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
User = user;
|
||||||
|
Delay = delay;
|
||||||
|
CancelToken = cancelToken;
|
||||||
|
Target = target;
|
||||||
|
NeedHand = needHand;
|
||||||
|
BreakOnUserMove = breakOnUserMove;
|
||||||
|
BreakOnTargetMove = breakOnTargetMove;
|
||||||
|
BreakOnDamage = breakOnDamage;
|
||||||
|
BreakOnStun = breakOnStun;
|
||||||
|
ExtraCheck = extraCheck;
|
||||||
|
|
||||||
|
if (Target == null)
|
||||||
|
{
|
||||||
|
BreakOnTargetMove = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.GameObjects.Components;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Server.Interfaces.Timing;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects.Systems;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
namespace Content.Server.GameObjects.EntitySystems
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class DoAfterSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IPauseManager _pauseManager = default!;
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
base.Update(frameTime);
|
||||||
|
|
||||||
|
foreach (var comp in ComponentManager.EntityQuery<DoAfterComponent>())
|
||||||
|
{
|
||||||
|
if (_pauseManager.IsGridPaused(comp.Owner.Transform.GridID)) continue;
|
||||||
|
|
||||||
|
var cancelled = new List<DoAfter>(0);
|
||||||
|
var finished = new List<DoAfter>(0);
|
||||||
|
|
||||||
|
foreach (var doAfter in comp.DoAfters)
|
||||||
|
{
|
||||||
|
doAfter.Run(frameTime);
|
||||||
|
|
||||||
|
switch (doAfter.Status)
|
||||||
|
{
|
||||||
|
case DoAfterStatus.Running:
|
||||||
|
break;
|
||||||
|
case DoAfterStatus.Cancelled:
|
||||||
|
cancelled.Add(doAfter);
|
||||||
|
break;
|
||||||
|
case DoAfterStatus.Finished:
|
||||||
|
finished.Add(doAfter);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var doAfter in cancelled)
|
||||||
|
{
|
||||||
|
comp.Cancelled(doAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var doAfter in finished)
|
||||||
|
{
|
||||||
|
comp.Finished(doAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
finished.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tasks that are delayed until the specified time has passed
|
||||||
|
/// These can be potentially cancelled by the user moving or when other things happen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="eventArgs"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<DoAfterStatus> DoAfter(DoAfterEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
// Setup
|
||||||
|
var doAfter = new DoAfter(eventArgs);
|
||||||
|
// Caller's gonna be responsible for this I guess
|
||||||
|
var doAfterComponent = eventArgs.User.GetComponent<DoAfterComponent>();
|
||||||
|
doAfterComponent.Add(doAfter);
|
||||||
|
DamageableComponent? damageableComponent = null;
|
||||||
|
|
||||||
|
// TODO: If the component's deleted this may not get unsubscribed?
|
||||||
|
if (eventArgs.BreakOnDamage && eventArgs.User.TryGetComponent(out damageableComponent))
|
||||||
|
{
|
||||||
|
damageableComponent.Damaged += doAfter.HandleDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
await doAfter.AsTask;
|
||||||
|
|
||||||
|
if (damageableComponent != null)
|
||||||
|
{
|
||||||
|
damageableComponent.Damaged -= doAfter.HandleDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return doAfter.Status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DoAfterStatus
|
||||||
|
{
|
||||||
|
Running,
|
||||||
|
Cancelled,
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Interfaces.GameObjects;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.GameObjects.Components
|
||||||
|
{
|
||||||
|
public abstract class SharedDoAfterComponent : Component
|
||||||
|
{
|
||||||
|
public override string Name => "DoAfter";
|
||||||
|
|
||||||
|
public override uint? NetID => ContentNetIDs.DO_AFTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class CancelledDoAfterMessage : ComponentMessage
|
||||||
|
{
|
||||||
|
public byte ID { get; }
|
||||||
|
|
||||||
|
public CancelledDoAfterMessage(byte id)
|
||||||
|
{
|
||||||
|
ID = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// We send a trimmed-down version of the DoAfter for the client for it to use.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class DoAfterMessage : ComponentMessage
|
||||||
|
{
|
||||||
|
// To see what these do look at DoAfter and DoAfterEventArgs
|
||||||
|
public byte ID { get; }
|
||||||
|
|
||||||
|
public TimeSpan StartTime { get; }
|
||||||
|
|
||||||
|
public GridCoordinates UserGrid { get; }
|
||||||
|
|
||||||
|
public GridCoordinates TargetGrid { get; }
|
||||||
|
|
||||||
|
public EntityUid TargetUid { get; }
|
||||||
|
|
||||||
|
public float Delay { get; }
|
||||||
|
|
||||||
|
// TODO: The other ones need predicting
|
||||||
|
public bool BreakOnUserMove { get; }
|
||||||
|
|
||||||
|
public bool BreakOnTargetMove { get; }
|
||||||
|
|
||||||
|
public DoAfterMessage(byte id, GridCoordinates userGrid, GridCoordinates targetGrid, TimeSpan startTime, float delay, bool breakOnUserMove, bool breakOnTargetMove, EntityUid targetUid = default)
|
||||||
|
{
|
||||||
|
ID = id;
|
||||||
|
UserGrid = userGrid;
|
||||||
|
TargetGrid = targetGrid;
|
||||||
|
StartTime = startTime;
|
||||||
|
Delay = delay;
|
||||||
|
BreakOnUserMove = breakOnUserMove;
|
||||||
|
BreakOnTargetMove = breakOnTargetMove;
|
||||||
|
TargetUid = targetUid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
public const uint THROWN_ITEM = 1054;
|
public const uint THROWN_ITEM = 1054;
|
||||||
public const uint STRAP = 1055;
|
public const uint STRAP = 1055;
|
||||||
public const uint DISPOSABLE = 1056;
|
public const uint DISPOSABLE = 1056;
|
||||||
|
public const uint DO_AFTER = 1057;
|
||||||
|
|
||||||
// Net IDs for integration tests.
|
// Net IDs for integration tests.
|
||||||
public const uint PREDICTION_TEST = 10001;
|
public const uint PREDICTION_TEST = 10001;
|
||||||
|
|||||||
@@ -144,6 +144,7 @@
|
|||||||
proper: true
|
proper: true
|
||||||
- type: Pullable
|
- type: Pullable
|
||||||
- type: CanSeeGases
|
- type: CanSeeGases
|
||||||
|
- type: DoAfter
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
save: false
|
save: false
|
||||||
|
|||||||
BIN
Resources/Textures/Interface/Misc/progress_bar.rsi/icon.png
Normal file
BIN
Resources/Textures/Interface/Misc/progress_bar.rsi/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
15
Resources/Textures/Interface/Misc/progress_bar.rsi/meta.json
Normal file
15
Resources/Textures/Interface/Misc/progress_bar.rsi/meta.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"size": {
|
||||||
|
"x": 24,
|
||||||
|
"y": 7
|
||||||
|
},
|
||||||
|
"license": "CC-BY-SA-3.0",
|
||||||
|
"copyright": "https://github.com/tgstation/tgstation/blob/886ca0f8dddf83ecaf10c92ff106172722352192/icons/effects/progessbar.dmi",
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"name": "icon",
|
||||||
|
"directions": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user