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:
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user