Interaction clean up (#4091)

* Rename and clean up interaction events

* Fix hand equip events

* Refactor duplicate client input validation

* Rename Use handler

* Move unneeded InRangeUnobstructed methods to extensions only

* Clean up UseInteractions

* Clean up ActivateItemInWorld

* Replace explicit range check with InRangeUnobstructed

Remove TransformComponent check, since transform is guaranteed now.

* Revert transform check removal

* More cleanup

* Reorder interaction checks

* Rename attack eventargs to interact

* Test V1

* Add interaction test

* Fix interaction test

* Fix container interaction test

* Rename interaction methods

* Rename player to user and attacked to target

* Clean up InteractAfter

* Clean up InRangeUnobstructed usages

* Rename attack to interact and weapon to used

* Changed can't reach message to only play when holding something

Cleaned up bracket formatting

* Fix Airtight validation check

* Remove extra words in comments

* Fix FaceClick rotation

* Move duplicate map check and face to method

* Fix test
This commit is contained in:
ShadowCommander
2021-06-07 05:49:43 -07:00
committed by GitHub
parent 9d7094fbd7
commit 6870b88a56
14 changed files with 662 additions and 695 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Items.Storage;
@@ -29,6 +30,8 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Random;
using Robust.Shared.Localization;
using Content.Shared.Interfaces;
namespace Content.Server.GameObjects.EntitySystems.Click
{
@@ -43,16 +46,17 @@ namespace Content.Server.GameObjects.EntitySystems.Click
public override void Initialize()
{
SubscribeNetworkEvent<DragDropRequestEvent>(HandleDragDropMessage);
SubscribeNetworkEvent<DragDropRequestEvent>(HandleDragDropRequestEvent);
CommandBinds.Builder
.Bind(EngineKeyFunctions.Use,
new PointerInputCmdHandler(HandleClientUseItemInHand))
new PointerInputCmdHandler(HandleUseInteraction))
.Bind(ContentKeyFunctions.WideAttack,
new PointerInputCmdHandler(HandleWideAttack))
.Bind(ContentKeyFunctions.ActivateItemInWorld,
new PointerInputCmdHandler(HandleActivateItemInWorld))
.Bind(ContentKeyFunctions.TryPullObject, new PointerInputCmdHandler(HandleTryPullObject))
.Bind(ContentKeyFunctions.TryPullObject,
new PointerInputCmdHandler(HandleTryPullObject))
.Register<InteractionSystem>();
}
@@ -62,19 +66,57 @@ namespace Content.Server.GameObjects.EntitySystems.Click
base.Shutdown();
}
private void HandleDragDropMessage(DragDropRequestEvent msg, EntitySessionEventArgs args)
#region Client Input Validation
private bool ValidateClientInput(ICommonSession? session, EntityCoordinates coords, EntityUid uid, [NotNullWhen(true)] out IEntity? userEntity)
{
var performer = args.SenderSession.AttachedEntity;
userEntity = null;
if (performer == null) return;
if (!EntityManager.TryGetEntity(msg.Dropped, out var dropped)) return;
if (!EntityManager.TryGetEntity(msg.Target, out var target)) return;
if (!coords.IsValid(_entityManager))
{
Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}");
return false;
}
var interactionArgs = new DragDropEvent(performer, msg.DropLocation, dropped, target);
if (uid.IsClientSide())
{
Logger.WarningS("system.interaction",
$"Client sent interaction with client-side entity. Session={session}, Uid={uid}");
return false;
}
userEntity = ((IPlayerSession?) session)?.AttachedEntity;
if (userEntity == null || !userEntity.IsValid())
{
Logger.WarningS("system.interaction",
$"Client sent interaction with no attached entity. Session={session}");
return false;
}
return true;
}
#endregion
#region Drag drop
private void HandleDragDropRequestEvent(DragDropRequestEvent msg, EntitySessionEventArgs args)
{
if (!ValidateClientInput(args.SenderSession, msg.DropLocation, msg.Target, out var userEntity))
{
Logger.InfoS("system.interaction", $"DragDropRequestEvent input validation failed");
return;
}
if (!EntityManager.TryGetEntity(msg.Dropped, out var dropped))
return;
if (!EntityManager.TryGetEntity(msg.Target, out var target))
return;
var interactionArgs = new DragDropEvent(userEntity, msg.DropLocation, dropped, target);
// must be in range of both the target and the object they are drag / dropping
// Client also does this check but ya know we gotta validate it.
if (!interactionArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return;
if (!interactionArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return;
// trigger dragdrops on the dropped entity
RaiseLocalEvent(dropped.Uid, interactionArgs);
@@ -98,25 +140,21 @@ namespace Content.Server.GameObjects.EntitySystems.Click
}
}
}
#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 (!EntityManager.TryGetEntity(uid, out var used))
return false;
var playerEnt = ((IPlayerSession?) session)?.AttachedEntity;
if (playerEnt == null || !playerEnt.IsValid())
{
return false;
}
if (!playerEnt.Transform.Coordinates.InRange(EntityManager, used.Transform.Coordinates, InteractionRange))
{
return false;
}
InteractionActivate(playerEnt, used);
InteractionActivate(user, used);
return true;
}
@@ -126,61 +164,45 @@ namespace Content.Server.GameObjects.EntitySystems.Click
/// </summary>
public void TryInteractionActivate(IEntity? user, IEntity? used)
{
if (user != null && used != null && ActionBlockerSystem.CanUse(user))
{
InteractionActivate(user, used);
}
if (user == null || used == null)
return;
InteractionActivate(user, used);
}
private void InteractionActivate(IEntity user, IEntity used)
{
if (!ActionBlockerSystem.CanInteract(user) || ! ActionBlockerSystem.CanUse(user))
return;
// all activates should only fire when in range / unbostructed
if (!InRangeUnobstructed(user, used, ignoreInsideBlocker: true, popup: true))
return;
var activateMsg = new ActivateInWorldEvent(user, used);
RaiseLocalEvent(used.Uid, activateMsg);
if (activateMsg.Handled)
{
return;
}
if (!used.TryGetComponent(out IActivate? activateComp))
{
return;
}
// all activates should only fire when in range / unbostructed
var activateEventArgs = new ActivateEventArgs(user, used);
if (activateEventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
{
activateComp.Activate(activateEventArgs);
}
activateComp.Activate(activateEventArgs);
}
#endregion
private bool HandleWideAttack(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
// client sanitization
if (!coords.IsValid(_entityManager))
{
Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}");
return true;
}
if (uid.IsClientSide())
{
Logger.WarningS("system.interaction",
$"Client sent attack with client-side entity. Session={session}, Uid={uid}");
return true;
}
var userEntity = ((IPlayerSession?) session)?.AttachedEntity;
if (userEntity == null || !userEntity.IsValid())
if (!ValidateClientInput(session, coords, uid, out var userEntity))
{
Logger.InfoS("system.interaction", $"WideAttack input validation failed");
return true;
}
if (userEntity.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode)
{
DoAttack(userEntity, coords, true);
}
return true;
}
@@ -192,222 +214,144 @@ namespace Content.Server.GameObjects.EntitySystems.Click
/// <param name="entity"></param>
/// <param name="coords"></param>
/// <param name="uid"></param>
internal void UseItemInHand(IEntity entity, EntityCoordinates coords, EntityUid uid)
internal void AiUseInteraction(IEntity entity, EntityCoordinates coords, EntityUid uid)
{
if (entity.HasComponent<ActorComponent>())
{
throw new InvalidOperationException();
}
if (entity.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode)
{
DoAttack(entity, coords, false, uid);
}
else
{
UserInteraction(entity, coords, uid);
}
UserInteraction(entity, coords, uid);
}
public bool HandleClientUseItemInHand(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
public bool HandleUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
// client sanitization
if (!coords.IsValid(_entityManager))
if (!ValidateClientInput(session, coords, uid, out var userEntity))
{
Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}");
Logger.InfoS("system.interaction", $"Use input validation failed");
return true;
}
if (uid.IsClientSide())
{
Logger.WarningS("system.interaction",
$"Client sent interaction with client-side entity. Session={session}, Uid={uid}");
return true;
}
var userEntity = ((IPlayerSession?) session)?.AttachedEntity;
if (userEntity == null || !userEntity.IsValid())
{
return true;
}
if (userEntity.TryGetComponent(out CombatModeComponent? combat) && combat.IsInCombatMode)
DoAttack(userEntity, coords, false, uid);
else
UserInteraction(userEntity, coords, uid);
UserInteraction(userEntity, coords, uid);
return true;
}
private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
// client sanitization
if (!coords.IsValid(_entityManager))
if (!ValidateClientInput(session, coords, uid, out var userEntity))
{
Logger.InfoS("system.interaction", $"Invalid Coordinates for pulling: client={session}, coords={coords}");
return false;
Logger.InfoS("system.interaction", $"TryPullObject input validation failed");
return true;
}
if (uid.IsClientSide())
{
Logger.WarningS("system.interaction",
$"Client sent pull interaction with client-side entity. Session={session}, Uid={uid}");
if (userEntity.Uid == uid)
return false;
}
var player = session?.AttachedEntity;
if (player == null)
{
Logger.WarningS("system.interaction",
$"Client sent pulling interaction with no attached entity. Session={session}, Uid={uid}");
return false;
}
if (!EntityManager.TryGetEntity(uid, out var pulledObject))
{
return false;
}
if (player == pulledObject)
{
if (!InRangeUnobstructed(userEntity, pulledObject, popup: true))
return false;
}
if (!pulledObject.TryGetComponent(out PullableComponent? pull))
{
return false;
}
var dist = player.Transform.Coordinates.Position - pulledObject.Transform.Coordinates.Position;
if (dist.LengthSquared > InteractionRangeSquared)
{
return false;
}
return pull.TogglePull(player);
return pull.TogglePull(userEntity);
}
public async void UserInteraction(IEntity player, EntityCoordinates coordinates, EntityUid clickedUid)
public async void UserInteraction(IEntity user, EntityCoordinates coordinates, EntityUid clickedUid)
{
// Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null
if (!EntityManager.TryGetEntity(clickedUid, out var attacked))
{
attacked = null;
}
// Verify player has a transform component
if (!player.TryGetComponent<ITransformComponent>(out var playerTransform))
if (user.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode)
{
DoAttack(user, coordinates, false, clickedUid);
return;
}
// Verify player is on the same map as the entity he clicked on
if (coordinates.GetMapId(_entityManager) != playerTransform.MapID)
if (!ValidateInteractAndFace(user, coordinates))
return;
if (!ActionBlockerSystem.CanInteract(user))
return;
// Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null
EntityManager.TryGetEntity(clickedUid, out var target);
// Check if interacted entity is in the same container, the direct child, or direct parent of the user.
if (target != null && !user.IsInSameOrParentContainer(target))
{
Logger.WarningS("system.interaction",
$"Player named {player.Name} clicked on a map he isn't located on");
$"User entity named {user.Name} clicked on object {target.Name} that isn't the parent, child, or in the same container");
return;
}
// Verify player has a hand, and find what object he is currently holding in his active hand
if (!player.TryGetComponent<IHandsComponent>(out var hands))
{
// Verify user has a hand, and find what object he is currently holding in his active hand
if (!user.TryGetComponent<IHandsComponent>(out var hands))
return;
}
var item = hands.GetActiveHand?.Owner;
ClickFace(player, coordinates);
if (!ActionBlockerSystem.CanInteract(player))
// TODO: Replace with body interaction range when we get something like arm length or telekinesis or something.
var inRangeUnobstructed = user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true);
if (target == null || !inRangeUnobstructed)
{
return;
}
// In a container where the attacked entity is not the container's owner
if (player.TryGetContainer(out var playerContainer) &&
attacked != playerContainer.Owner)
{
// Either the attacked entity is null, not contained or in a different container
if (attacked == null ||
!attacked.TryGetContainer(out var attackedContainer) ||
attackedContainer != playerContainer)
{
if (item == null)
return;
}
}
// TODO: Check if client should be able to see that object to click on it in the first place
// Clicked on empty space behavior, try using ranged attack
if (attacked == null)
{
if (item != null)
if (!await InteractUsingRanged(user, item, target, coordinates, inRangeUnobstructed) &&
!inRangeUnobstructed)
{
// After attack: Check if we clicked on an empty location, if so the only interaction we can do is AfterInteract
var distSqrt = (playerTransform.WorldPosition - coordinates.ToMapPos(EntityManager)).LengthSquared;
InteractAfter(player, item, coordinates, distSqrt <= InteractionRangeSquared);
var message = Loc.GetString("You can't reach there!");
user.PopupMessage(message);
}
return;
}
// Verify attacked object is on the map if we managed to click on it somehow
if (!attacked.Transform.IsMapTransform)
{
Logger.WarningS("system.interaction",
$"Player named {player.Name} clicked on object {attacked.Name} that isn't currently on the map somehow");
return;
}
// RangedInteract/AfterInteract: Check distance between user and clicked item, if too large parse it in the ranged function
// TODO: have range based upon the item being used? or base it upon some variables of the player himself?
var distance = (playerTransform.WorldPosition - attacked.Transform.WorldPosition).LengthSquared;
if (distance > InteractionRangeSquared)
{
if (item != null)
{
RangedInteraction(player, item, attacked, coordinates);
return;
}
return; // Add some form of ranged InteractHand here if you need it someday, or perhaps just ways to modify the range of InteractHand
}
// We are close to the nearby object and the object isn't contained in our active hand
// InteractUsing/AfterInteract: We will either use the item on the nearby object
if (item != null)
{
await Interaction(player, item, attacked, coordinates);
}
// InteractHand/Activate: Since our hand is empty we will use InteractHand/Activate
else
{
Interaction(player, attacked);
// We are close to the nearby object and the object isn't contained in our active hand
// InteractUsing/AfterInteract: We will either use the item on the nearby object
if (item != null)
await InteractUsing(user, item, target, coordinates);
// InteractHand/Activate: Since our hand is empty we will use InteractHand/Activate
else
InteractHand(user, target);
}
}
private void ClickFace(IEntity player, EntityCoordinates coordinates)
private bool ValidateInteractAndFace(IEntity user, EntityCoordinates coordinates)
{
var diff = coordinates.ToMapPos(EntityManager) - player.Transform.MapPosition.Position;
// Verify user is on the same map as the entity he clicked on
if (coordinates.GetMapId(_entityManager) != user.Transform.MapID)
{
Logger.WarningS("system.interaction",
$"User entity named {user.Name} clicked on a map he isn't located on");
return false;
}
FaceClickCoordinates(user, coordinates);
return true;
}
private void FaceClickCoordinates(IEntity user, EntityCoordinates coordinates)
{
var diff = coordinates.ToMapPos(EntityManager) - user.Transform.MapPosition.Position;
if (diff.LengthSquared <= 0.01f)
return;
var diffAngle = Angle.FromWorldVec(diff);
if (ActionBlockerSystem.CanChangeDirection(player))
if (ActionBlockerSystem.CanChangeDirection(user))
{
player.Transform.LocalRotation = diffAngle;
user.Transform.WorldRotation = diffAngle;
}
else
{
if (player.TryGetComponent(out BuckleComponent? buckle) && (buckle.BuckledTo != null))
if (user.TryGetComponent(out BuckleComponent? buckle) && (buckle.BuckledTo != null))
{
// We're buckled to another object. Is that object rotatable?
if (buckle.BuckledTo!.Owner.TryGetComponent(out SharedRotatableComponent? rotatable) && rotatable.RotateWhileAnchored)
{
// Note the assumption that even if unanchored, player can only do spinnychair with an "independent wheel".
// (Since the player being buckled to it holds it down with their weight.)
// Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
// (Since the user being buckled to it holds it down with their weight.)
// This is logically equivalent to RotateWhileAnchored.
// Barstools and office chairs have independent wheels, while regular chairs don't.
rotatable.Owner.Transform.LocalRotation = diffAngle;
@@ -419,90 +363,87 @@ namespace Content.Server.GameObjects.EntitySystems.Click
/// <summary>
/// We didn't click on any entity, try doing an AfterInteract on the click location
/// </summary>
private async void InteractAfter(IEntity user, IEntity weapon, EntityCoordinates clickLocation, bool canReach)
private async Task<bool> InteractDoAfter(IEntity user, IEntity used, IEntity? target, EntityCoordinates clickLocation, bool canReach)
{
var message = new AfterInteractEvent(user, weapon, null, clickLocation, canReach);
RaiseLocalEvent(weapon.Uid, message);
if (message.Handled)
var afterInteractEvent = new AfterInteractEvent(user, used, target, clickLocation, canReach);
RaiseLocalEvent(used.Uid, afterInteractEvent, false);
if (afterInteractEvent.Handled)
return true;
var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, target, canReach);
var afterInteracts = used.GetAllComponents<IAfterInteract>().OrderByDescending(x => x.Priority).ToList();
foreach (var afterInteract in afterInteracts)
{
return;
if (await afterInteract.AfterInteract(afterInteractEventArgs))
return true;
}
var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, null, canReach);
await DoAfterInteract(weapon, afterInteractEventArgs);
return false;
}
/// <summary>
/// Uses a weapon/object on an entity
/// Uses a item/object on an entity
/// Finds components with the InteractUsing interface and calls their function
/// NOTE: Does not have an InRangeUnobstructed check
/// </summary>
public async Task Interaction(IEntity user, IEntity weapon, IEntity attacked, EntityCoordinates clickLocation)
public async Task InteractUsing(IEntity user, IEntity used, IEntity target, EntityCoordinates clickLocation)
{
var attackMsg = new InteractUsingEvent(user, weapon, attacked, clickLocation);
RaiseLocalEvent(attacked.Uid, attackMsg);
if (attackMsg.Handled)
if (!ActionBlockerSystem.CanInteract(user))
return;
var attackBys = attacked.GetAllComponents<IInteractUsing>().OrderByDescending(x => x.Priority);
var attackByEventArgs = new InteractUsingEventArgs(user, clickLocation, weapon, attacked);
// all AttackBys should only happen when in range / unobstructed, so no range check is needed
if (attackByEventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
{
foreach (var attackBy in attackBys)
{
if (await attackBy.InteractUsing(attackByEventArgs))
{
// If an InteractUsing returns a status completion we finish our attack
return;
}
}
}
var afterAtkMsg = new AfterInteractEvent(user, weapon, attacked, clickLocation, true);
RaiseLocalEvent(weapon.Uid, afterAtkMsg, false);
if (afterAtkMsg.Handled)
{
// all interactions should only happen when in range / unobstructed, so no range check is needed
var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation);
RaiseLocalEvent(target.Uid, interactUsingEvent);
if (interactUsingEvent.Handled)
return;
var interactUsingEventArgs = new InteractUsingEventArgs(user, clickLocation, used, target);
var interactUsings = target.GetAllComponents<IInteractUsing>().OrderByDescending(x => x.Priority);
foreach (var interactUsing in interactUsings)
{
// If an InteractUsing returns a status completion we finish our interaction
if (await interactUsing.InteractUsing(interactUsingEventArgs))
return;
}
// If we aren't directly attacking the nearby object, lets see if our item has an after attack we can do
var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: true);
await DoAfterInteract(weapon, afterAttackEventArgs);
// If we aren't directly interacting with the nearby object, lets see if our item has an after interact we can do
await InteractDoAfter(user, used, target, clickLocation, true);
}
/// <summary>
/// Uses an empty hand on an entity
/// Finds components with the InteractHand interface and calls their function
/// NOTE: Does not have an InRangeUnobstructed check
/// </summary>
public void Interaction(IEntity user, IEntity attacked)
public void InteractHand(IEntity user, IEntity target)
{
var message = new AttackHandEvent(user, attacked);
RaiseLocalEvent(attacked.Uid, message);
if (!ActionBlockerSystem.CanInteract(user))
return;
// all interactions should only happen when in range / unobstructed, so no range check is needed
var message = new InteractHandEvent(user, target);
RaiseLocalEvent(target.Uid, message);
if (message.Handled)
return;
var attackHandEventArgs = new InteractHandEventArgs(user, attacked);
var interactHandEventArgs = new InteractHandEventArgs(user, target);
// all attackHands should only fire when in range / unobstructed
if (attackHandEventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
var interactHandComps = target.GetAllComponents<IInteractHand>().ToList();
foreach (var interactHandComp in interactHandComps)
{
var attackHands = attacked.GetAllComponents<IInteractHand>().ToList();
foreach (var attackHand in attackHands)
{
if (attackHand.InteractHand(attackHandEventArgs))
{
// If an InteractHand returns a status completion we finish our attack
return;
}
}
// If an InteractHand returns a status completion we finish our interaction
if (interactHandComp.InteractHand(interactHandEventArgs))
return;
}
// Else we run Activate.
InteractionActivate(user, attacked);
InteractionActivate(user, target);
}
#region Hands
#region Use
/// <summary>
/// Activates the IUse behaviors of an entity
/// Verifies that the user is capable of doing the use interaction first
@@ -534,23 +475,21 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var useMsg = new UseInHandEvent(user, used);
RaiseLocalEvent(used.Uid, useMsg);
if (useMsg.Handled)
{
return;
}
var uses = used.GetAllComponents<IUse>().ToList();
// Try to use item on any components which have the interface
foreach (var use in uses)
{
// If a Use returns a status completion we finish our interaction
if (use.UseEntity(new UseEntityEventArgs(user)))
{
// If a Use returns a status completion we finish our attack
return;
}
}
}
#endregion
#region Throw
/// <summary>
/// Activates the Throw behavior of an object
/// Verifies that the user is capable of doing the throw interaction first
@@ -572,9 +511,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var throwMsg = new ThrownEvent(user, thrown);
RaiseLocalEvent(thrown.Uid, throwMsg);
if (throwMsg.Handled)
{
return;
}
var comps = thrown.GetAllComponents<IThrown>().ToList();
var args = new ThrownEventArgs(user);
@@ -585,7 +522,9 @@ namespace Content.Server.GameObjects.EntitySystems.Click
comp.Thrown(args);
}
}
#endregion
#region Equip
/// <summary>
/// Calls Equipped on all components that implement the IEquipped interface
/// on an entity that has been equipped.
@@ -595,9 +534,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var equipMsg = new EquippedEvent(user, equipped, slot);
RaiseLocalEvent(equipped.Uid, equipMsg);
if (equipMsg.Handled)
{
return;
}
var comps = equipped.GetAllComponents<IEquipped>().ToList();
@@ -617,9 +554,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var unequipMsg = new UnequippedEvent(user, equipped, slot);
RaiseLocalEvent(equipped.Uid, unequipMsg);
if (unequipMsg.Handled)
{
return;
}
var comps = equipped.GetAllComponents<IUnequipped>().ToList();
@@ -630,6 +565,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
}
}
#region Equip Hand
/// <summary>
/// Calls EquippedHand on all components that implement the IEquippedHand interface
/// on an item.
@@ -639,9 +575,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var equippedHandMessage = new EquippedHandEvent(user, item, hand);
RaiseLocalEvent(item.Uid, equippedHandMessage);
if (equippedHandMessage.Handled)
{
return;
}
var comps = item.GetAllComponents<IEquippedHand>().ToList();
@@ -660,9 +594,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var unequippedHandMessage = new UnequippedHandEvent(user, item, hand);
RaiseLocalEvent(item.Uid, unequippedHandMessage);
if (unequippedHandMessage.Handled)
{
return;
}
var comps = item.GetAllComponents<IUnequippedHand>().ToList();
@@ -671,7 +603,10 @@ namespace Content.Server.GameObjects.EntitySystems.Click
comp.UnequippedHand(new UnequippedHandEventArgs(user, hand));
}
}
#endregion
#endregion
#region Drop
/// <summary>
/// Activates the Dropped behavior of an object
/// Verifies that the user is capable of doing the drop interaction first
@@ -693,9 +628,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var dropMsg = new DroppedEvent(user, item, intentional);
RaiseLocalEvent(item.Uid, dropMsg);
if (dropMsg.Handled)
{
return;
}
item.Transform.LocalRotation = intentional ? Angle.Zero : (_random.Next(0, 100) / 100f) * MathHelper.TwoPi;
@@ -707,7 +640,9 @@ namespace Content.Server.GameObjects.EntitySystems.Click
comp.Dropped(new DroppedEventArgs(user, intentional));
}
}
#endregion
#region Hand Selected
/// <summary>
/// Calls HandSelected on all components that implement the IHandSelected interface
/// on an item entity on a hand that has just been selected.
@@ -717,9 +652,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var handSelectedMsg = new HandSelectedEvent(user, item);
RaiseLocalEvent(item.Uid, handSelectedMsg);
if (handSelectedMsg.Handled)
{
return;
}
var comps = item.GetAllComponents<IHandSelected>().ToList();
@@ -739,9 +672,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
var handDeselectedMsg = new HandDeselectedEvent(user, item);
RaiseLocalEvent(item.Uid, handDeselectedMsg);
if (handDeselectedMsg.Handled)
{
return;
}
var comps = item.GetAllComponents<IHandDeselected>().ToList();
@@ -751,88 +682,70 @@ namespace Content.Server.GameObjects.EntitySystems.Click
comp.HandDeselected(new HandDeselectedEventArgs(user));
}
}
#endregion
#endregion
/// <summary>
/// Will have two behaviors, either "uses" the weapon at range on the entity if it is capable of accepting that action
/// Or it will use the weapon itself on the position clicked, regardless of what was there
/// 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 async void RangedInteraction(IEntity user, IEntity weapon, IEntity attacked, EntityCoordinates clickLocation)
public async Task<bool> InteractUsingRanged(IEntity user, IEntity used, IEntity? target, EntityCoordinates clickLocation, bool inRangeUnobstructed)
{
var rangedMsg = new RangedInteractEvent(user, weapon, attacked, clickLocation);
RaiseLocalEvent(attacked.Uid, rangedMsg);
if (rangedMsg.Handled)
return;
var rangedAttackBys = attacked.GetAllComponents<IRangedInteract>().ToList();
var rangedAttackByEventArgs = new RangedInteractEventArgs(user, weapon, clickLocation);
// See if we have a ranged attack interaction
foreach (var t in rangedAttackBys)
if (target != null)
{
if (t.RangedInteract(rangedAttackByEventArgs))
var rangedMsg = new RangedInteractEvent(user, used, target, clickLocation);
RaiseLocalEvent(target.Uid, rangedMsg);
if (rangedMsg.Handled)
return true;
var rangedInteractions = target.GetAllComponents<IRangedInteract>().ToList();
var rangedInteractionEventArgs = new RangedInteractEventArgs(user, used, clickLocation);
// See if we have a ranged interaction
foreach (var t in rangedInteractions)
{
// If an InteractUsing returns a status completion we finish our attack
return;
// If an InteractUsingRanged returns a status completion we finish our interaction
if (t.RangedInteract(rangedInteractionEventArgs))
return true;
}
}
var afterAtkMsg = new AfterInteractEvent(user, weapon, attacked, clickLocation, false);
RaiseLocalEvent(weapon.Uid, afterAtkMsg);
if (afterAtkMsg.Handled)
return;
// See if we have a ranged attack interaction
var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: false);
await DoAfterInteract(weapon, afterAttackEventArgs);
if (inRangeUnobstructed)
return await InteractDoAfter(user, used, target, clickLocation, false);
else
return await InteractDoAfter(user, used, null, clickLocation, false);
}
private static async Task DoAfterInteract(IEntity weapon, AfterInteractEventArgs afterAttackEventArgs)
public void DoAttack(IEntity user, EntityCoordinates coordinates, bool wideAttack, EntityUid targetUid = default)
{
var afterAttacks = weapon.GetAllComponents<IAfterInteract>().OrderByDescending(x => x.Priority).ToList();
if (!ValidateInteractAndFace(user, coordinates))
return;
foreach (var afterAttack in afterAttacks)
if (!ActionBlockerSystem.CanAttack(user))
return;
IEntity? targetEnt = null;
if (!wideAttack)
{
if (await afterAttack.AfterInteract(afterAttackEventArgs))
// Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null
EntityManager.TryGetEntity(targetUid, out targetEnt);
// Check if interacted entity is in the same container, the direct child, or direct parent of the user.
if (targetEnt != null && !user.IsInSameOrParentContainer(targetEnt))
{
Logger.WarningS("system.interaction",
$"User entity named {user.Name} clicked on object {targetEnt.Name} that isn't the parent, child, or in the same container");
return;
}
}
}
public void DoAttack(IEntity player, EntityCoordinates coordinates, bool wideAttack, EntityUid targetUid = default)
{
// Verify player is on the same map as the entity he clicked on
if (coordinates.GetMapId(EntityManager) != player.Transform.MapID)
{
Logger.WarningS("system.interaction",
$"Player named {player.Name} clicked on a map he isn't located on");
return;
}
ClickFace(player, coordinates);
if (!ActionBlockerSystem.CanAttack(player) ||
(!wideAttack && !player.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true)))
{
return;
}
// In a container where the target entity is not the container's owner
if (player.TryGetContainer(out var playerContainer) &&
(!EntityManager.TryGetEntity(targetUid, out var target) ||
target != playerContainer.Owner))
{
// Either the target entity is null, not contained or in a different container
if (target == null ||
!target.TryGetContainer(out var attackedContainer) ||
attackedContainer != playerContainer)
{
// TODO: Replace with body attack range when we get something like arm length or telekinesis or something.
if (!user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true))
return;
}
}
// Verify player has a hand, and find what object he is currently holding in his active hand
if (player.TryGetComponent<IHandsComponent>(out var hands))
// Verify user has a hand, and find what object he is currently holding in his active hand
if (user.TryGetComponent<IHandsComponent>(out var hands))
{
var item = hands.GetActiveHand?.Owner;
@@ -840,7 +753,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
{
if (wideAttack)
{
var ev = new WideAttackEvent(item, player, coordinates);
var ev = new WideAttackEvent(item, user, coordinates);
RaiseLocalEvent(item.Uid, ev, false);
if(ev.Handled)
@@ -848,30 +761,29 @@ namespace Content.Server.GameObjects.EntitySystems.Click
}
else
{
var ev = new ClickAttackEvent(item, player, coordinates, targetUid);
var ev = new ClickAttackEvent(item, user, coordinates, targetUid);
RaiseLocalEvent(item.Uid, ev, false);
if(ev.Handled)
return;
}
}
else
else if (!wideAttack &&
(targetEnt != null || EntityManager.TryGetEntity(targetUid, out targetEnt)) &&
targetEnt.HasComponent<ItemComponent>())
{
// We pick up items if our hand is empty, even if we're in combat mode.
if (EntityManager.TryGetEntity(targetUid, out var targetEnt) && targetEnt.HasComponent<ItemComponent>())
{
Interaction(player, targetEnt);
return;
}
InteractHand(user, targetEnt);
return;
}
}
// TODO: Make this saner?
// Attempt to do unarmed combat. We don't check for handled just because at this point it doesn't matter.
if(wideAttack)
RaiseLocalEvent(player.Uid, new WideAttackEvent(player, player, coordinates), false);
RaiseLocalEvent(user.Uid, new WideAttackEvent(user, user, coordinates), false);
else
RaiseLocalEvent(player.Uid, new ClickAttackEvent(player, player, coordinates, targetUid), false);
RaiseLocalEvent(user.Uid, new ClickAttackEvent(user, user, coordinates, targetUid), false);
}
}
}