Predict general interactions. (#6856)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
138
Content.Shared/Timing/UseDelaySystem.cs
Normal file
138
Content.Shared/Timing/UseDelaySystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user