* 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:
metalgearsloth
2020-08-09 02:16:13 +10:00
committed by GitHub
parent ee14d67756
commit 5b3b2e3207
14 changed files with 1186 additions and 0 deletions

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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,
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View 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
}
]
}