Predict general interactions. (#6856)

This commit is contained in:
Leon Friedrich
2022-03-09 20:12:17 +13:00
committed by GitHub
parent 60e7ef6073
commit 0f435f31c8
10 changed files with 283 additions and 256 deletions

View File

@@ -80,9 +80,8 @@ namespace Content.Client.DragDrop
_dropTargetOutOfRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetOutOfRange).Instance();
// needs to fire on mouseup and mousedown so we can detect a drag / drop
CommandBinds.Builder
.Bind(EngineKeyFunctions.Use, new PointerInputCmdHandler(OnUse, false))
.BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(OnUse, false), new[] { typeof(SharedInteractionSystem) })
.Register<DragDropSystem>();
}
public override void Shutdown()

View File

@@ -44,6 +44,16 @@ public sealed class DoorSystem : SharedDoorSystem
SubscribeLocalEvent<DoorComponent, GotEmaggedEvent>(OnEmagged);
}
protected override void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
{
// TODO once access permissions are shared, move this back to shared.
if (args.Handled || !door.ClickOpen)
return;
TryToggleDoor(uid, door, args.User);
args.Handled = true;
}
protected override void SetCollidable(EntityUid uid, bool collidable,
DoorComponent? door = null,
PhysicsComponent? physics = null,

View File

@@ -1,4 +1,3 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
@@ -6,13 +5,11 @@ using Content.Server.CombatMode;
using Content.Server.Hands.Components;
using Content.Server.Pulling;
using Content.Server.Storage.Components;
using Content.Server.Timing;
using Content.Shared.ActionBlocker;
using Content.Shared.Database;
using Content.Shared.DragDrop;
using Content.Shared.Input;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Item;
using Content.Shared.Pulling.Components;
using Content.Shared.Timing;
@@ -20,11 +17,7 @@ using Content.Shared.Weapons.Melee;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Players;
@@ -39,7 +32,6 @@ namespace Content.Server.Interaction
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly PullingSystem _pullSystem = default!;
[Dependency] private readonly AdminLogSystem _adminLogSystem = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
public override void Initialize()
{
@@ -48,12 +40,8 @@ namespace Content.Server.Interaction
SubscribeNetworkEvent<DragDropRequestEvent>(HandleDragDropRequestEvent);
CommandBinds.Builder
.Bind(EngineKeyFunctions.Use,
new PointerInputCmdHandler(HandleUseInteraction))
.Bind(ContentKeyFunctions.WideAttack,
new PointerInputCmdHandler(HandleWideAttack))
.Bind(ContentKeyFunctions.ActivateItemInWorld,
new PointerInputCmdHandler(HandleActivateItemInWorld))
.Bind(ContentKeyFunctions.TryPullObject,
new PointerInputCmdHandler(HandleTryPullObject))
.Register<InteractionSystem>();
@@ -86,11 +74,6 @@ namespace Content.Server.Interaction
return storage.SubscribedSessions.Contains(actor.PlayerSession);
}
protected override void BeginDelay(UseDelayComponent? component = null)
{
_useDelay.BeginDelay(component);
}
#region Drag drop
private void HandleDragDropRequestEvent(DragDropRequestEvent msg, EntitySessionEventArgs args)
{
@@ -146,23 +129,6 @@ namespace Content.Server.Interaction
}
#endregion
#region ActivateItemInWorld
private bool HandleActivateItemInWorld(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (!ValidateClientInput(session, coords, uid, out var user))
{
Logger.InfoS("system.interaction", $"ActivateItemInWorld input validation failed");
return false;
}
if (Deleted(uid))
return false;
InteractionActivate(user.Value, uid);
return true;
}
#endregion
private bool HandleWideAttack(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
// client sanitization
@@ -193,20 +159,6 @@ namespace Content.Server.Interaction
UserInteraction(entity, coords, uid);
}
public bool HandleUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
// client sanitization
if (!ValidateClientInput(session, coords, uid, out var userEntity))
{
Logger.InfoS("system.interaction", $"Use input validation failed");
return true;
}
UserInteraction(userEntity.Value, coords, !Deleted(uid) ? uid : null);
return true;
}
private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (!ValidateClientInput(session, coords, uid, out var userEntity))
@@ -230,68 +182,6 @@ namespace Content.Server.Interaction
return _pullSystem.TogglePull(userEntity.Value, pull);
}
/// <summary>
/// Uses an empty hand on an entity
/// Finds components with the InteractHand interface and calls their function
/// NOTE: Does not have any range or can-interact checks. These should all have been done before this function is called.
/// </summary>
public override void InteractHand(EntityUid user, EntityUid target)
{
// TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction.
// all interactions should only happen when in range / unobstructed, so no range check is needed
var message = new InteractHandEvent(user, target);
RaiseLocalEvent(target, message);
_adminLogSystem.Add(LogType.InteractHand, LogImpact.Low, $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}");
if (message.Handled)
return;
var interactHandEventArgs = new InteractHandEventArgs(user, target);
var interactHandComps = AllComps<IInteractHand>(target).ToList();
foreach (var interactHandComp in interactHandComps)
{
// If an InteractHand returns a status completion we finish our interaction
#pragma warning disable 618
if (interactHandComp.InteractHand(interactHandEventArgs))
#pragma warning restore 618
return;
}
// Else we run Activate.
InteractionActivate(user, target,
checkCanInteract: false,
checkUseDelay: true,
checkAccess: false);
}
/// <summary>
/// Will have two behaviors, either "uses" the used entity at range on the target entity if it is capable of accepting that action
/// Or it will use the used entity itself on the position clicked, regardless of what was there
/// </summary>
public override void InteractUsingRanged(
EntityUid user,
EntityUid used,
EntityUid? target,
EntityCoordinates clickLocation,
bool inRangeUnobstructed)
{
// TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction.
if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed))
return;
if (target != null)
{
var rangedMsg = new RangedInteractEvent(user, used, target.Value, clickLocation);
RaiseLocalEvent(target.Value, rangedMsg);
if (rangedMsg.Handled)
return;
}
InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed);
}
public override void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, EntityUid? target = null)
{
// TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction.

View File

@@ -1,90 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Content.Shared.Cooldown;
using Content.Shared.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Timing;
public sealed class UseDelaySystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
private HashSet<UseDelayComponent> _activeDelays = new();
public override void Update(float frameTime)
{
base.Update(frameTime);
var toRemove = new RemQueue<UseDelayComponent>();
foreach (var delay in _activeDelays)
{
MetaDataComponent? metaData = null;
if (Deleted(delay.Owner, metaData) ||
delay.CancellationTokenSource?.Token.IsCancellationRequested == true)
{
toRemove.Add(delay);
continue;
}
if (Paused(delay.Owner, metaData)) continue;
delay.Elapsed += frameTime;
if (delay.Elapsed < delay.Delay) continue;
toRemove.Add(delay);
}
foreach (var delay in toRemove)
{
delay.CancellationTokenSource = null;
delay.Elapsed = 0f;
_activeDelays.Remove(delay);
}
}
public void BeginDelay(UseDelayComponent? component = null)
{
if (component == null ||
component.ActiveDelay ||
Deleted(component.Owner)) return;
component.CancellationTokenSource = new CancellationTokenSource();
DebugTools.Assert(!_activeDelays.Contains(component));
_activeDelays.Add(component);
var currentTime = _gameTiming.CurTime;
component.LastUseTime = currentTime;
var cooldown = EnsureComp<ItemCooldownComponent>(component.Owner);
cooldown.CooldownStart = currentTime;
cooldown.CooldownEnd = currentTime + TimeSpan.FromSeconds(component.Delay);
}
public void Cancel(UseDelayComponent component)
{
component.CancellationTokenSource?.Cancel();
component.CancellationTokenSource = null;
if (TryComp<ItemCooldownComponent>(component.Owner, out var cooldown))
{
cooldown.CooldownEnd = _gameTiming.CurTime;
}
}
public void Restart(UseDelayComponent component)
{
component.CancellationTokenSource?.Cancel();
component.CancellationTokenSource = null;
BeginDelay(component);
}
}

View File

@@ -162,12 +162,9 @@ public abstract class SharedDoorSystem : EntitySystem
#endregion
#region Interactions
private void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
protected virtual void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
{
if (args.Handled || !door.ClickOpen)
return;
TryToggleDoor(uid, door, args.User);
// avoid client-mispredicts, as the server will definitely handle this event
args.Handled = true;
}

View File

@@ -8,7 +8,6 @@ using Content.Shared.CombatMode;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Input;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Interaction.Components;
using Content.Shared.Physics;
using Content.Shared.Popups;
@@ -25,6 +24,7 @@ using Robust.Shared.Serialization;
using Content.Shared.Wall;
using Content.Shared.Item;
using Robust.Shared.Player;
using Robust.Shared.Input;
#pragma warning disable 618
@@ -43,6 +43,7 @@ namespace Content.Shared.Interaction
[Dependency] private readonly SharedAdminLogSystem _adminLogSystem = default!;
[Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
public const float InteractionRange = 2;
@@ -61,6 +62,10 @@ namespace Content.Shared.Interaction
CommandBinds.Builder
.Bind(ContentKeyFunctions.AltActivateItemInWorld,
new PointerInputCmdHandler(HandleAltUseInteraction))
.Bind(EngineKeyFunctions.Use,
new PointerInputCmdHandler(HandleUseInteraction))
.Bind(ContentKeyFunctions.ActivateItemInWorld,
new PointerInputCmdHandler(HandleActivateItemInWorld))
.Register<SharedInteractionSystem>();
}
@@ -144,6 +149,20 @@ namespace Content.Shared.Interaction
return false;
}
public bool HandleUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
// client sanitization
if (!ValidateClientInput(session, coords, uid, out var userEntity))
{
Logger.InfoS("system.interaction", $"Use input validation failed");
return true;
}
UserInteraction(userEntity.Value, coords, !Deleted(uid) ? uid : null);
return false;
}
/// <summary>
/// Resolves user interactions with objects.
/// </summary>
@@ -243,9 +262,31 @@ namespace Content.Shared.Interaction
inRangeUnobstructed);
}
public virtual void InteractHand(EntityUid user, EntityUid target)
public void InteractHand(EntityUid user, EntityUid target)
{
// TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction.
// all interactions should only happen when in range / unobstructed, so no range check is needed
var message = new InteractHandEvent(user, target);
RaiseLocalEvent(target, message);
_adminLogSystem.Add(LogType.InteractHand, LogImpact.Low, $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}");
if (message.Handled)
return;
var interactHandEventArgs = new InteractHandEventArgs(user, target);
var interactHandComps = AllComps<IInteractHand>(target).ToList();
foreach (var interactHandComp in interactHandComps)
{
// If an InteractHand returns a status completion we finish our interaction
#pragma warning disable 618
if (interactHandComp.InteractHand(interactHandEventArgs))
#pragma warning restore 618
return;
}
// Else we run Activate.
InteractionActivate(user, target,
checkCanInteract: false,
checkUseDelay: true,
checkAccess: false);
}
public virtual void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack,
@@ -254,10 +295,22 @@ namespace Content.Shared.Interaction
// TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction.
}
public virtual void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target,
public void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target,
EntityCoordinates clickLocation, bool inRangeUnobstructed)
{
// TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction.
if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed))
return;
if (target != null)
{
var rangedMsg = new RangedInteractEvent(user, used, target.Value, clickLocation);
RaiseLocalEvent(target.Value, rangedMsg);
if (rangedMsg.Handled)
return;
}
InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed);
}
protected bool ValidateInteractAndFace(EntityUid user, EntityCoordinates coordinates)
@@ -594,8 +647,8 @@ namespace Content.Shared.Interaction
return;
var interactUsingEventArgs = new InteractUsingEventArgs(user, clickLocation, used, target);
var interactUsings = AllComps<IInteractUsing>(target).OrderByDescending(x => x.Priority);
foreach (var interactUsing in interactUsings)
{
// If an InteractUsing returns a status completion we finish our interaction
@@ -636,6 +689,21 @@ namespace Content.Shared.Interaction
}
#region ActivateItemInWorld
private bool HandleActivateItemInWorld(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (!ValidateClientInput(session, coords, uid, out var user))
{
Logger.InfoS("system.interaction", $"ActivateItemInWorld input validation failed");
return false;
}
if (Deleted(uid))
return false;
InteractionActivate(user.Value, uid);
return false;
}
/// <summary>
/// Raises <see cref="ActivateInWorldEvent"/> events and activates the IActivate behavior of an object.
/// </summary>
@@ -671,18 +739,20 @@ namespace Content.Shared.Interaction
RaiseLocalEvent(used, activateMsg);
if (activateMsg.Handled)
{
BeginDelay(delayComponent);
_useDelay.BeginDelay(used, delayComponent);
_adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
return true;
}
if (!TryComp(used, out IActivate? activateComp))
var activatable = AllComps<IActivate>(used).FirstOrDefault();
if (activatable == null)
return false;
var activateEventArgs = new ActivateEventArgs(user, used);
activateComp.Activate(activateEventArgs);
BeginDelay(delayComponent);
_adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}"); // No way to check success.
activatable.Activate(new ActivateEventArgs(user, used));
// No way to check success.
_useDelay.BeginDelay(used, delayComponent);
_adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
return true;
}
#endregion
@@ -718,7 +788,7 @@ namespace Content.Shared.Interaction
RaiseLocalEvent(used, useMsg);
if (useMsg.Handled)
{
BeginDelay(delayComponent);
_useDelay.BeginDelay(used, delayComponent);
return true;
}
@@ -730,7 +800,7 @@ namespace Content.Shared.Interaction
// If a Use returns a status completion we finish our interaction
if (use.UseEntity(new UseEntityEventArgs(user)))
{
BeginDelay(delayComponent);
_useDelay.BeginDelay(used, delayComponent);
return true;
}
}
@@ -739,12 +809,6 @@ namespace Content.Shared.Interaction
return InteractionActivate(user, used, false, false, false);
}
protected virtual void BeginDelay(UseDelayComponent? component = null)
{
// This is temporary until we have predicted UseDelay.
return;
}
/// <summary>
/// Alternative interactions on an entity.
/// </summary>

View File

@@ -20,7 +20,7 @@ namespace Content.Shared.Item
/// Players can pick up, drop, and put items in bags, and they can be seen in player's hands.
/// </summary>
[NetworkedComponent()]
public abstract class SharedItemComponent : Component, IInteractHand
public abstract class SharedItemComponent : Component
{
[Dependency] private readonly IEntityManager _entMan = default!;
@@ -97,22 +97,6 @@ namespace Content.Shared.Item
[DataField("sprite")]
public readonly string? RsiPath;
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
{
var user = eventArgs.User;
if (!_entMan.TryGetComponent(user, out SharedHandsComponent hands))
return false;
var activeHand = hands.ActiveHand;
if (activeHand == null)
return false;
// hands checks action blockers
return hands.TryPickupEntityToActiveHand(Owner, animateUser: true);
}
public void RemovedFromSlot()
{
if (_entMan.TryGetComponent(Owner, out SharedSpriteComponent component))

View File

@@ -1,3 +1,5 @@
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Inventory.Events;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
@@ -17,11 +19,26 @@ namespace Content.Shared.Item
SubscribeLocalEvent<SharedSpriteComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<SharedSpriteComponent, GotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<SharedItemComponent, InteractHandEvent>(OnHandInteract);
SubscribeLocalEvent<SharedItemComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<SharedItemComponent, ComponentHandleState>(OnHandleState);
}
private void OnHandInteract(EntityUid uid, SharedItemComponent component, InteractHandEvent args)
{
if (args.Handled)
return;
if (!TryComp(args.User, out SharedHandsComponent? hands))
return;
if (hands.ActiveHand == null)
return;
args.Handled = hands.TryPickupEntity(hands.ActiveHand, uid, false, animateUser: false);
}
private void OnHandleState(EntityUid uid, SharedItemComponent component, ref ComponentHandleState args)
{
if (args.Current is not ItemComponentState state)

View File

@@ -1,8 +1,6 @@
using System;
using System.Threading;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Timing
{
@@ -10,20 +8,40 @@ namespace Content.Shared.Timing
/// Timer that creates a cooldown each time an object is activated/used
/// </summary>
[RegisterComponent]
[NetworkedComponent]
public sealed class UseDelayComponent : Component
{
[ViewVariables]
public TimeSpan LastUseTime;
[ViewVariables]
[DataField("delay")]
public float Delay = 1;
public TimeSpan? DelayEndTime;
[ViewVariables]
public float Elapsed = 0f;
[DataField("delay")]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan Delay = TimeSpan.FromSeconds(1);
/// <summary>
/// Stores remaining delay pausing (and eventually, serialization).
/// </summary>
[DataField("remainingDelay")]
public TimeSpan? RemainingDelay;
public CancellationTokenSource? CancellationTokenSource;
public bool ActiveDelay => CancellationTokenSource is { Token: { IsCancellationRequested: false } };
}
[Serializable, NetSerializable]
public sealed class UseDelayComponentState : ComponentState
{
public readonly TimeSpan LastUseTime;
public readonly TimeSpan Delay;
public readonly TimeSpan? DelayEndTime;
public UseDelayComponentState(TimeSpan lastUseTime, TimeSpan delay, TimeSpan? delayEndTime)
{
LastUseTime = lastUseTime;
Delay = delay;
DelayEndTime = delayEndTime;
}
}
}

View File

@@ -0,0 +1,138 @@
using System.Threading;
using Content.Shared.Cooldown;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Timing;
public sealed class UseDelaySystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
private HashSet<UseDelayComponent> _activeDelays = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UseDelayComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<UseDelayComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<UseDelayComponent, EntityPausedEvent>(OnPaused);
}
private void OnPaused(EntityUid uid, UseDelayComponent component, EntityPausedEvent args)
{
if (args.Paused)
{
// This entity just got paused, but wasn't before
if (component.DelayEndTime != null)
component.RemainingDelay = _gameTiming.CurTime - component.DelayEndTime;
_activeDelays.Remove(component);
}
else if (component.RemainingDelay == null)
{
// Got unpaused, but had no active delay
return;
}
// We got unpaused, resume the delay/cooldown. Currently this takes for granted that ItemCooldownComponent
// handles the pausing on its own. I'm not even gonna check, because I CBF fixing it if it doesn't.
component.DelayEndTime = _gameTiming.CurTime + component.RemainingDelay;
Dirty(component);
_activeDelays.Add(component);
}
private void OnHandleState(EntityUid uid, UseDelayComponent component, ref ComponentHandleState args)
{
if (args.Current is not UseDelayComponentState state)
return;
component.LastUseTime = state.LastUseTime;
component.Delay = state.Delay;
component.DelayEndTime = state.DelayEndTime;
if (component.DelayEndTime == null)
_activeDelays.Remove(component);
else
_activeDelays.Add(component);
}
private void OnGetState(EntityUid uid, UseDelayComponent component, ref ComponentGetState args)
{
args.State = new UseDelayComponentState(component.LastUseTime, component.Delay, component.DelayEndTime);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var toRemove = new RemQueue<UseDelayComponent>();
var curTime = _gameTiming.CurTime;
var mQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
foreach (var delay in _activeDelays)
{
if (curTime > delay.DelayEndTime
|| !mQuery.TryGetComponent(delay.Owner, out var meta)
|| meta.Deleted
|| delay.CancellationTokenSource?.Token.IsCancellationRequested == true)
{
toRemove.Add(delay);
}
}
foreach (var delay in toRemove)
{
delay.CancellationTokenSource = null;
delay.DelayEndTime = null;
_activeDelays.Remove(delay);
}
}
public void BeginDelay(EntityUid uid, UseDelayComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return;
if (component.ActiveDelay || Deleted(uid)) return;
component.CancellationTokenSource = new CancellationTokenSource();
DebugTools.Assert(!_activeDelays.Contains(component));
_activeDelays.Add(component);
var currentTime = _gameTiming.CurTime;
component.LastUseTime = currentTime;
component.DelayEndTime = currentTime + component.Delay;
Dirty(component);
// TODO just merge these components?
var cooldown = EnsureComp<ItemCooldownComponent>(component.Owner);
cooldown.CooldownStart = currentTime;
cooldown.CooldownEnd = component.DelayEndTime;
}
public void Cancel(UseDelayComponent component)
{
component.CancellationTokenSource?.Cancel();
component.CancellationTokenSource = null;
component.DelayEndTime = null;
_activeDelays.Remove(component);
Dirty(component);
if (TryComp<ItemCooldownComponent>(component.Owner, out var cooldown))
{
cooldown.CooldownEnd = _gameTiming.CurTime;
}
}
public void Restart(UseDelayComponent component)
{
component.CancellationTokenSource?.Cancel();
component.CancellationTokenSource = null;
BeginDelay(component.Owner, component);
}
}