2021-08-22 03:20:18 +10:00
using System ;
2021-12-16 23:42:02 +13:00
using System.Diagnostics.CodeAnalysis ;
2021-04-01 00:04:56 -07:00
using System.Linq ;
2021-10-25 20:06:12 +13:00
using System.Threading.Tasks ;
using Content.Shared.ActionBlocker ;
2021-11-24 16:52:31 -06:00
using Content.Shared.Administration.Logs ;
2021-12-16 23:42:02 +13:00
using Content.Shared.CombatMode ;
2021-11-28 14:56:53 +01:00
using Content.Shared.Database ;
2021-10-25 20:06:12 +13:00
using Content.Shared.Hands.Components ;
2021-12-16 23:42:02 +13:00
using Content.Shared.Input ;
2022-02-18 17:57:31 -05:00
using Content.Shared.Interaction.Helpers ;
using Content.Shared.Interaction.Components ;
2020-04-25 11:37:59 +02:00
using Content.Shared.Physics ;
2021-09-26 15:18:45 +02:00
using Content.Shared.Popups ;
2021-10-25 20:06:12 +13:00
using Content.Shared.Throwing ;
using Content.Shared.Timing ;
using Content.Shared.Verbs ;
2020-04-22 00:58:31 +10:00
using JetBrains.Annotations ;
2021-11-29 12:25:22 +13:00
using Robust.Shared.Containers ;
2021-12-16 23:42:02 +13:00
using Robust.Shared.Input.Binding ;
2020-04-22 00:58:31 +10:00
using Robust.Shared.Map ;
2021-02-11 01:13:03 -08:00
using Robust.Shared.Physics ;
2021-11-29 12:25:22 +13:00
using Robust.Shared.Players ;
2021-08-22 03:20:18 +10:00
using Robust.Shared.Serialization ;
2022-02-17 15:40:03 +13:00
using Content.Shared.Wall ;
using Content.Shared.Item ;
2022-02-05 15:39:01 +13:00
using Robust.Shared.Player ;
2020-04-22 00:58:31 +10:00
2021-10-27 18:10:40 +02:00
#pragma warning disable 618
2021-06-09 22:19:39 +02:00
namespace Content.Shared.Interaction
2020-04-22 00:58:31 +10:00
{
/// <summary>
/// Governs interactions during clicking on entities
/// </summary>
[UsedImplicitly]
2021-09-16 13:02:10 +10:00
public abstract class SharedInteractionSystem : EntitySystem
2020-04-22 00:58:31 +10:00
{
2022-02-17 15:40:03 +13:00
[Dependency] private readonly IMapManager _mapManager = default ! ;
2021-10-10 14:18:19 +11:00
[Dependency] private readonly SharedPhysicsSystem _sharedBroadphaseSystem = default ! ;
2021-10-25 20:06:12 +13:00
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default ! ;
[Dependency] private readonly SharedVerbSystem _verbSystem = default ! ;
2021-11-24 16:52:31 -06:00
[Dependency] private readonly SharedAdminLogSystem _adminLogSystem = default ! ;
2021-12-16 23:42:02 +13:00
[Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default ! ;
2022-02-05 15:39:01 +13:00
[Dependency] private readonly SharedPopupSystem _popupSystem = default ! ;
2022-01-31 20:08:53 +13:00
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default ! ;
2021-07-26 12:58:17 +02:00
2020-04-22 00:58:31 +10:00
public const float InteractionRange = 2 ;
public const float InteractionRangeSquared = InteractionRange * InteractionRange ;
2022-02-09 07:08:07 +13:00
public const float MaxRaycastRange = 100 ;
2021-12-03 11:15:41 -08:00
public delegate bool Ignored ( EntityUid entity ) ;
2020-08-30 11:37:06 +02:00
2021-12-16 23:42:02 +13:00
public override void Initialize ( )
{
2022-01-31 04:26:07 +13:00
SubscribeLocalEvent < BoundUserInterfaceMessageAttempt > ( OnBoundInterfaceInteractAttempt ) ;
2021-12-16 23:42:02 +13:00
SubscribeAllEvent < InteractInventorySlotEvent > ( HandleInteractInventorySlotEvent ) ;
2022-02-18 17:57:31 -05:00
SubscribeLocalEvent < UnremoveableComponent , ContainerGettingRemovedAttemptEvent > ( OnRemoveAttempt ) ;
2021-12-16 23:42:02 +13:00
CommandBinds . Builder
. Bind ( ContentKeyFunctions . AltActivateItemInWorld ,
new PointerInputCmdHandler ( HandleAltUseInteraction ) )
. Register < SharedInteractionSystem > ( ) ;
}
public override void Shutdown ( )
{
CommandBinds . Unregister < SharedInteractionSystem > ( ) ;
base . Shutdown ( ) ;
}
2022-01-31 04:26:07 +13:00
/// <summary>
/// Check that the user that is interacting with the BUI is capable of interacting and can access the entity.
/// </summary>
private void OnBoundInterfaceInteractAttempt ( BoundUserInterfaceMessageAttempt ev )
{
2022-02-15 17:06:52 +13:00
if ( ev . Sender . AttachedEntity is not EntityUid user | | ! _actionBlockerSystem . CanInteract ( user , ev . Target ) )
2022-01-31 04:26:07 +13:00
{
ev . Cancel ( ) ;
return ;
}
2022-01-31 20:08:53 +13:00
if ( ! ContainerSystem . IsInSameOrParentContainer ( user , ev . Target ) & & ! CanAccessViaStorage ( user , ev . Target ) )
2022-01-31 04:26:07 +13:00
{
ev . Cancel ( ) ;
return ;
}
2022-02-17 15:40:03 +13:00
if ( ! InRangeUnobstructed ( user , ev . Target ) )
2022-01-31 04:26:07 +13:00
{
ev . Cancel ( ) ;
return ;
}
}
2022-02-18 17:57:31 -05:00
/// <summary>
/// Prevents an item with the Unremovable component from being removed from a container by almost any means
/// </summary>
private void OnRemoveAttempt ( EntityUid uid , UnremoveableComponent item , ContainerGettingRemovedAttemptEvent args )
{
args . Cancel ( ) ;
}
2021-12-16 23:42:02 +13:00
/// <summary>
/// Handles the event were a client uses an item in their inventory or in their hands, either by
/// alt-clicking it or pressing 'E' while hovering over it.
/// </summary>
private void HandleInteractInventorySlotEvent ( InteractInventorySlotEvent msg , EntitySessionEventArgs args )
{
var coords = Transform ( msg . ItemUid ) . Coordinates ;
// client sanitization
if ( ! ValidateClientInput ( args . SenderSession , coords , msg . ItemUid , out var user ) )
{
Logger . InfoS ( "system.interaction" , $"Inventory interaction validation failed. Session={args.SenderSession}" ) ;
return ;
}
2022-02-15 17:06:52 +13:00
// We won't bother to check that the target item is ACTUALLY in an inventory slot. UserInteraction() and
// InteractionActivate() should check that the item is accessible. So.. if a user wants to lie about an
// in-reach item being used in a slot... that should have no impact. This is functionally the same as if
// they had somehow directly clicked on that item.
2021-12-16 23:42:02 +13:00
if ( msg . AltInteract )
// Use 'UserInteraction' function - behaves as if the user alt-clicked the item in the world.
UserInteraction ( user . Value , coords , msg . ItemUid , msg . AltInteract ) ;
else
// User used 'E'. We want to activate it, not simulate clicking on the item
InteractionActivate ( user . Value , msg . ItemUid ) ;
}
public bool HandleAltUseInteraction ( ICommonSession ? session , EntityCoordinates coords , EntityUid uid )
{
// client sanitization
if ( ! ValidateClientInput ( session , coords , uid , out var user ) )
{
Logger . InfoS ( "system.interaction" , $"Alt-use input validation failed" ) ;
return true ;
}
UserInteraction ( user . Value , coords , uid , altInteract : true ) ;
return false ;
}
/// <summary>
/// Resolves user interactions with objects.
/// </summary>
/// <remarks>
/// Checks Whether combat mode is enabled and whether the user can actually interact with the given entity.
/// </remarks>
/// <param name="altInteract">Whether to use default or alternative interactions (usually as a result of
/// alt+clicking). If combat mode is enabled, the alternative action is to perform the default non-combat
/// interaction. Having an item in the active hand also disables alternative interactions.</param>
2022-02-15 17:06:52 +13:00
public void UserInteraction (
EntityUid user ,
EntityCoordinates coordinates ,
EntityUid ? target ,
bool altInteract = false ,
bool checkCanInteract = true ,
bool checkAccess = true ,
bool checkCanUse = true )
2021-12-16 23:42:02 +13:00
{
if ( target ! = null & & Deleted ( target . Value ) )
return ;
// TODO COMBAT Consider using alt-interact for advanced combat? maybe alt-interact disarms?
if ( ! altInteract & & TryComp ( user , out SharedCombatModeComponent ? combatMode ) & & combatMode . IsInCombatMode )
{
DoAttack ( user , coordinates , false , target ) ;
return ;
}
if ( ! ValidateInteractAndFace ( user , coordinates ) )
return ;
2022-02-15 17:06:52 +13:00
if ( altInteract & & target ! = null )
{
// Perform alternative interactions, using context menu verbs.
// These perform their own range, can-interact, and accessibility checks.
AltInteract ( user , target . Value ) ;
2022-02-15 23:40:48 +13:00
return ;
2022-02-15 17:06:52 +13:00
}
if ( checkCanInteract & & ! _actionBlockerSystem . CanInteract ( user , target ) )
2021-12-16 23:42:02 +13:00
return ;
// Check if interacted entity is in the same container, the direct child, or direct parent of the user.
2022-02-15 17:06:52 +13:00
// Also checks if the item is accessible via some storage UI (e.g., open backpack)
if ( checkAccess
& & target ! = null
& & ! ContainerSystem . IsInSameOrParentContainer ( user , target . Value )
& & ! CanAccessViaStorage ( user , target . Value ) )
2021-12-16 23:42:02 +13:00
return ;
2022-02-15 17:06:52 +13:00
// Does the user have hands?
Hand ? hand ;
if ( ! TryComp ( user , out SharedHandsComponent ? hands ) | | ! hands . TryGetActiveHand ( out hand ) )
2021-12-16 23:42:02 +13:00
return ;
2022-02-17 15:40:03 +13:00
var inRangeUnobstructed = target = = null
? ! checkAccess | | InRangeUnobstructed ( user , coordinates )
: ! checkAccess | | InRangeUnobstructed ( user , target . Value ) ; // permits interactions with wall mounted entities
2022-02-05 15:39:01 +13:00
2022-02-15 17:06:52 +13:00
// empty-hand interactions
if ( hand . HeldEntity = = null )
{
if ( inRangeUnobstructed & & target ! = null )
InteractHand ( user , target . Value ) ;
2021-12-16 23:42:02 +13:00
return ;
}
2021-12-30 18:27:15 -08:00
2022-02-15 17:06:52 +13:00
// Can the user use the held entity?
if ( checkCanUse & & ! _actionBlockerSystem . CanUseHeldEntity ( user ) )
return ;
2022-02-23 17:58:06 +13:00
if ( target = = hand . HeldEntity )
{
UseInHandInteraction ( user , target . Value , checkCanUse : false , checkCanInteract : false ) ;
return ;
}
2022-02-15 17:06:52 +13:00
if ( inRangeUnobstructed & & target ! = null )
2022-01-01 16:16:45 +13:00
{
2022-02-15 17:06:52 +13:00
InteractUsing (
user ,
hand . HeldEntity . Value ,
target . Value ,
coordinates ,
checkCanInteract : false ,
checkCanUse : false ) ;
return ;
2021-12-16 23:42:02 +13:00
}
2022-02-15 17:06:52 +13:00
InteractUsingRanged (
user ,
hand . HeldEntity . Value ,
target ,
coordinates ,
inRangeUnobstructed ) ;
2021-12-16 23:42:02 +13:00
}
2022-02-15 17:06:52 +13:00
public virtual void InteractHand ( EntityUid user , EntityUid target )
2021-12-16 23:42:02 +13:00
{
// TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction.
}
public virtual void DoAttack ( EntityUid user , EntityCoordinates coordinates , bool wideAttack ,
EntityUid ? targetUid = null )
{
// TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction.
}
2022-02-15 17:06:52 +13:00
public virtual void InteractUsingRanged ( EntityUid user , EntityUid used , EntityUid ? target ,
2021-12-16 23:42:02 +13:00
EntityCoordinates clickLocation , bool inRangeUnobstructed )
{
// TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction.
}
protected bool ValidateInteractAndFace ( EntityUid user , EntityCoordinates coordinates )
{
// Verify user is on the same map as the entity they clicked on
if ( coordinates . GetMapId ( EntityManager ) ! = Transform ( user ) . MapID )
return false ;
_rotateToFaceSystem . TryFaceCoordinates ( user , coordinates . ToMapPos ( EntityManager ) ) ;
return true ;
}
2020-05-28 13:23:50 +02:00
/// <summary>
/// Traces a ray from coords to otherCoords and returns the length
/// of the vector between coords and the ray's first hit.
/// </summary>
2020-08-30 11:37:06 +02:00
/// <param name="origin">Set of coordinates to use.</param>
/// <param name="other">Other set of coordinates to use.</param>
2020-05-28 13:23:50 +02:00
/// <param name="collisionMask">the mask to check for collisions</param>
2020-08-30 11:37:06 +02:00
/// <param name="predicate">
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
/// </param>
2020-05-28 13:23:50 +02:00
/// <returns>Length of resulting ray.</returns>
2020-08-30 11:37:06 +02:00
public float UnobstructedDistance (
MapCoordinates origin ,
MapCoordinates other ,
int collisionMask = ( int ) CollisionGroup . Impassable ,
2021-03-15 21:55:49 +01:00
Ignored ? predicate = null )
2020-05-28 13:23:50 +02:00
{
2020-08-30 11:37:06 +02:00
var dir = other . Position - origin . Position ;
2020-05-28 13:23:50 +02:00
if ( dir . LengthSquared . Equals ( 0f ) ) return 0f ;
2020-08-30 11:37:06 +02:00
predicate ? ? = _ = > false ;
var ray = new CollisionRay ( origin . Position , dir . Normalized , collisionMask ) ;
2021-07-26 12:58:17 +02:00
var rayResults = _sharedBroadphaseSystem . IntersectRayWithPredicate ( origin . MapId , ray , dir . Length , predicate . Invoke , false ) . ToList ( ) ;
2020-05-28 13:23:50 +02:00
if ( rayResults . Count = = 0 ) return dir . Length ;
2020-08-30 11:37:06 +02:00
return ( rayResults [ 0 ] . HitPos - origin . Position ) . Length ;
2020-05-28 13:23:50 +02:00
}
2020-04-22 00:58:31 +10:00
/// <summary>
/// Checks that these coordinates are within a certain distance without any
/// entity that matches the collision mask obstructing them.
/// If the <paramref name="range"/> is zero or negative,
2020-08-30 11:37:06 +02:00
/// this method will only check if nothing obstructs the two sets
/// of coordinates.
2020-04-22 00:58:31 +10:00
/// </summary>
2020-08-30 11:37:06 +02:00
/// <param name="origin">Set of coordinates to use.</param>
/// <param name="other">Other set of coordinates to use.</param>
/// <param name="range">
/// Maximum distance between the two sets of coordinates.
/// </param>
/// <param name="collisionMask">The mask to check for collisions.</param>
/// <param name="predicate">
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
/// </param>
/// <returns>
/// True if the two points are within a given range without being obstructed.
/// </returns>
public bool InRangeUnobstructed (
MapCoordinates origin ,
MapCoordinates other ,
float range = InteractionRange ,
CollisionGroup collisionMask = CollisionGroup . Impassable ,
2022-02-17 15:40:03 +13:00
Ignored ? predicate = null )
2020-04-22 00:58:31 +10:00
{
2022-01-30 14:00:11 +11:00
// Have to be on same map regardless.
if ( other . MapId ! = origin . MapId ) return false ;
2020-05-26 14:23:25 +02:00
2020-08-30 11:37:06 +02:00
var dir = other . Position - origin . Position ;
2022-02-09 07:08:07 +13:00
var length = dir . Length ;
2022-01-30 14:00:11 +11:00
// If range specified also check it
2022-02-09 07:08:07 +13:00
if ( range > 0f & & length > range ) return false ;
if ( MathHelper . CloseTo ( length , 0 ) ) return true ;
2020-04-22 00:58:31 +10:00
2020-08-30 11:37:06 +02:00
predicate ? ? = _ = > false ;
2022-02-09 07:08:07 +13:00
if ( length > MaxRaycastRange )
{
Logger . Warning ( "InRangeUnobstructed check performed over extreme range. Limiting CollisionRay size." ) ;
length = MaxRaycastRange ;
}
2020-08-30 11:37:06 +02:00
var ray = new CollisionRay ( origin . Position , dir . Normalized , ( int ) collisionMask ) ;
2022-02-09 07:08:07 +13:00
var rayResults = _sharedBroadphaseSystem . IntersectRayWithPredicate ( origin . MapId , ray , length , predicate . Invoke , false ) . ToList ( ) ;
2020-08-30 11:37:06 +02:00
2022-02-17 15:40:03 +13:00
return rayResults . Count = = 0 ;
2020-04-22 00:58:31 +10:00
}
2020-04-25 11:37:59 +02:00
/// <summary>
2020-08-30 11:37:06 +02:00
/// Checks that two entities are within a certain distance without any
2020-04-25 11:37:59 +02:00
/// entity that matches the collision mask obstructing them.
/// If the <paramref name="range"/> is zero or negative,
2020-08-30 11:37:06 +02:00
/// this method will only check if nothing obstructs the two entities.
2022-02-17 15:40:03 +13:00
/// This function will also check whether the other entity is a wall-mounted entity. If it is, it will
/// automatically ignore some obstructions.
2020-04-25 11:37:59 +02:00
/// </summary>
2020-08-30 11:37:06 +02:00
/// <param name="origin">The first entity to use.</param>
/// <param name="other">Other entity to use.</param>
/// <param name="range">
/// Maximum distance between the two entities.
/// </param>
/// <param name="collisionMask">The mask to check for collisions.</param>
/// <param name="predicate">
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
/// </param>
/// <param name="popup">
/// Whether or not to popup a feedback message on the origin entity for
/// it to see.
/// </param>
/// <returns>
/// True if the two points are within a given range without being obstructed.
/// </returns>
public bool InRangeUnobstructed (
2021-12-04 12:35:33 +01:00
EntityUid origin ,
EntityUid other ,
2020-08-30 11:37:06 +02:00
float range = InteractionRange ,
CollisionGroup collisionMask = CollisionGroup . Impassable ,
2021-03-15 21:55:49 +01:00
Ignored ? predicate = null ,
2020-08-30 11:37:06 +02:00
bool popup = false )
{
2022-02-17 15:40:03 +13:00
var originPosition = Transform ( origin ) . MapPosition ;
var transform = Transform ( other ) ;
var ( position , rotation ) = transform . GetWorldPositionRotation ( ) ;
var mapPos = new MapCoordinates ( position , transform . MapID ) ;
var wallPredicate = AddAnchoredPredicate ( other , mapPos , rotation , originPosition ) ;
Ignored combinedPredicate = e = >
{
return e = = origin
| | e = = other
| | ( predicate ? . Invoke ( e ) ? ? false )
| | ( wallPredicate ? . Invoke ( e ) ? ? false ) ;
} ;
var inRange = InRangeUnobstructed ( origin , mapPos , range , collisionMask , combinedPredicate , popup ) ;
if ( ! inRange & & popup )
{
var message = Loc . GetString ( "interaction-system-user-interaction-cannot-reach" ) ;
_popupSystem . PopupEntity ( message , origin , Filter . Entities ( origin ) ) ;
}
return inRange ;
2020-08-30 11:37:06 +02:00
}
public bool InRangeUnobstructed (
2022-02-17 15:40:03 +13:00
MapCoordinates origin ,
EntityUid target ,
2020-08-30 11:37:06 +02:00
float range = InteractionRange ,
CollisionGroup collisionMask = CollisionGroup . Impassable ,
2022-02-17 15:40:03 +13:00
Ignored ? predicate = null )
2020-08-30 11:37:06 +02:00
{
2022-02-17 15:40:03 +13:00
var transform = Transform ( target ) ;
var ( position , rotation ) = transform . GetWorldPositionRotation ( ) ;
var mapPos = new MapCoordinates ( position , transform . MapID ) ;
var wallPredicate = AddAnchoredPredicate ( target , mapPos , rotation , origin ) ;
Ignored combinedPredicate = e = >
{
return e = = target
| | ( predicate ? . Invoke ( e ) ? ? false )
| | ( wallPredicate ? . Invoke ( e ) ? ? false ) ;
} ;
return InRangeUnobstructed ( origin , mapPos , range , collisionMask , combinedPredicate ) ;
}
/// <summary>
/// If the target entity is either an item or a wall-mounted object, this will add a predicate to ignore any
/// anchored entities on that tile.
/// </summary>
public Ignored ? AddAnchoredPredicate (
EntityUid target ,
MapCoordinates targetPosition ,
Angle targetRotation ,
MapCoordinates origin )
{
if ( ! _mapManager . TryFindGridAt ( targetPosition , out var grid ) )
return null ;
if ( HasComp < SharedItemComponent > ( target ) )
{
// Ignore anchored entities on that tile.
var colliding = new HashSet < EntityUid > ( grid . GetAnchoredEntities ( targetPosition ) ) ;
return e = > colliding . Contains ( e ) ;
}
if ( ! TryComp ( target , out WallMountComponent ? wallMount ) )
return null ;
// wall-mount exemptions may be restricted to a specific angle range.
if ( wallMount . Arc < 360 )
{
var angle = Angle . FromWorldVec ( origin . Position - targetPosition . Position ) ;
var angleDelta = ( wallMount . Direction + targetRotation - angle ) . Reduced ( ) . FlipPositive ( ) ;
var inArc = angleDelta < wallMount . Arc / 2 | | Math . Tau - angleDelta < wallMount . Arc / 2 ;
if ( ! inArc )
return null ;
}
// Ignore anchored entities on that tile.
var ignored = new HashSet < EntityUid > ( grid . GetAnchoredEntities ( targetPosition ) ) ;
return e = > ignored . Contains ( e ) ;
2020-08-30 11:37:06 +02:00
}
/// <summary>
/// Checks that an entity and a set of grid coordinates are within a certain
/// distance without any entity that matches the collision mask
/// obstructing them.
/// If the <paramref name="range"/> is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
/// </summary>
/// <param name="origin">The entity to use.</param>
/// <param name="other">The grid coordinates to use.</param>
/// <param name="range">
/// Maximum distance between the two entity and set of grid coordinates.
/// </param>
/// <param name="collisionMask">The mask to check for collisions.</param>
/// <param name="predicate">
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
/// </param>
/// <param name="popup">
/// Whether or not to popup a feedback message on the origin entity for
/// it to see.
/// </param>
/// <returns>
/// True if the two points are within a given range without being obstructed.
/// </returns>
public bool InRangeUnobstructed (
2021-12-04 12:35:33 +01:00
EntityUid origin ,
2020-09-06 16:11:53 +02:00
EntityCoordinates other ,
2020-08-30 11:37:06 +02:00
float range = InteractionRange ,
CollisionGroup collisionMask = CollisionGroup . Impassable ,
2021-03-15 21:55:49 +01:00
Ignored ? predicate = null ,
2020-08-30 11:37:06 +02:00
bool popup = false )
{
2022-02-17 15:40:03 +13:00
return InRangeUnobstructed ( origin , other . ToMap ( EntityManager ) , range , collisionMask , predicate , popup ) ;
2020-08-30 11:37:06 +02:00
}
/// <summary>
/// Checks that an entity and a set of map coordinates are within a certain
/// distance without any entity that matches the collision mask
/// obstructing them.
/// If the <paramref name="range"/> is zero or negative,
/// this method will only check if nothing obstructs the entity and component.
/// </summary>
/// <param name="origin">The entity to use.</param>
/// <param name="other">The map coordinates to use.</param>
/// <param name="range">
/// Maximum distance between the two entity and set of map coordinates.
/// </param>
/// <param name="collisionMask">The mask to check for collisions.</param>
/// <param name="predicate">
/// A predicate to check whether to ignore an entity or not.
/// If it returns true, it will be ignored.
/// </param>
/// <param name="popup">
/// Whether or not to popup a feedback message on the origin entity for
/// it to see.
/// </param>
/// <returns>
/// True if the two points are within a given range without being obstructed.
/// </returns>
public bool InRangeUnobstructed (
2021-12-04 12:35:33 +01:00
EntityUid origin ,
2020-08-30 11:37:06 +02:00
MapCoordinates other ,
float range = InteractionRange ,
CollisionGroup collisionMask = CollisionGroup . Impassable ,
2021-03-15 21:55:49 +01:00
Ignored ? predicate = null ,
2020-08-30 11:37:06 +02:00
bool popup = false )
{
2022-02-17 15:40:03 +13:00
Ignored combinedPredicatre = e = > e = = origin | | ( predicate ? . Invoke ( e ) ? ? false ) ;
2021-12-16 23:42:02 +13:00
var originPosition = Transform ( origin ) . MapPosition ;
2022-02-17 15:40:03 +13:00
var inRange = InRangeUnobstructed ( originPosition , other , range , collisionMask , combinedPredicatre ) ;
2020-08-30 11:37:06 +02:00
if ( ! inRange & & popup )
{
2022-02-15 17:06:52 +13:00
var message = Loc . GetString ( "interaction-system-user-interaction-cannot-reach" ) ;
2022-02-17 15:40:03 +13:00
_popupSystem . PopupEntity ( message , origin , Filter . Entities ( origin ) ) ;
2020-08-30 11:37:06 +02:00
}
return inRange ;
}
2021-10-25 20:06:12 +13:00
2022-02-15 17:06:52 +13:00
public bool RangedInteractDoBefore (
2021-12-04 12:35:33 +01:00
EntityUid user ,
EntityUid used ,
EntityUid ? target ,
2021-10-25 20:06:12 +13:00
EntityCoordinates clickLocation ,
bool canReach )
{
2022-02-15 17:06:52 +13:00
var ev = new BeforeRangedInteractEvent ( user , used , target , clickLocation , canReach ) ;
2021-12-03 15:53:09 +01:00
RaiseLocalEvent ( used , ev , false ) ;
2021-10-25 20:06:12 +13:00
return ev . Handled ;
}
/// <summary>
/// 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>
2022-02-15 17:06:52 +13:00
public async void InteractUsing (
EntityUid user ,
EntityUid used ,
EntityUid target ,
EntityCoordinates clickLocation ,
bool predicted = false ,
bool checkCanInteract = true ,
bool checkCanUse = true )
2021-10-25 20:06:12 +13:00
{
2022-02-15 17:06:52 +13:00
if ( checkCanInteract & & ! _actionBlockerSystem . CanInteract ( user , target ) )
2021-10-25 20:06:12 +13:00
return ;
2022-02-15 17:06:52 +13:00
if ( checkCanUse & & ! _actionBlockerSystem . CanUseHeldEntity ( user ) )
return ;
if ( RangedInteractDoBefore ( user , used , target , clickLocation , true ) )
2021-10-25 20:06:12 +13:00
return ;
// all interactions should only happen when in range / unobstructed, so no range check is needed
2022-01-30 13:50:10 +13:00
var interactUsingEvent = new InteractUsingEvent ( user , used , target , clickLocation , predicted ) ;
2021-12-03 15:53:09 +01:00
RaiseLocalEvent ( target , interactUsingEvent ) ;
2021-10-25 20:06:12 +13:00
if ( interactUsingEvent . Handled )
return ;
var interactUsingEventArgs = new InteractUsingEventArgs ( user , clickLocation , used , target ) ;
2021-12-16 23:42:02 +13:00
var interactUsings = AllComps < IInteractUsing > ( target ) . OrderByDescending ( x = > x . Priority ) ;
2021-10-25 20:06:12 +13:00
foreach ( var interactUsing in interactUsings )
{
// If an InteractUsing returns a status completion we finish our interaction
if ( await interactUsing . InteractUsing ( interactUsingEventArgs ) )
return ;
}
2022-02-15 17:06:52 +13:00
InteractDoAfter ( user , used , target , clickLocation , canReach : true ) ;
2021-10-25 20:06:12 +13:00
}
/// <summary>
2022-02-05 15:39:01 +13:00
/// Used when clicking on an entity resulted in no other interaction. Used for low-priority interactions.
2021-10-25 20:06:12 +13:00
/// </summary>
2022-02-15 17:06:52 +13:00
public async void InteractDoAfter ( EntityUid user , EntityUid used , EntityUid ? target , EntityCoordinates clickLocation , bool canReach )
2021-10-25 20:06:12 +13:00
{
2021-12-11 15:36:50 +01:00
if ( target is { Valid : false } )
target = null ;
2021-10-25 20:06:12 +13:00
var afterInteractEvent = new AfterInteractEvent ( user , used , target , clickLocation , canReach ) ;
2021-12-03 15:53:09 +01:00
RaiseLocalEvent ( used , afterInteractEvent , false ) ;
2021-10-25 20:06:12 +13:00
if ( afterInteractEvent . Handled )
2022-02-15 17:06:52 +13:00
return ;
2021-10-25 20:06:12 +13:00
var afterInteractEventArgs = new AfterInteractEventArgs ( user , clickLocation , target , canReach ) ;
2021-12-16 23:42:02 +13:00
var afterInteracts = AllComps < IAfterInteract > ( used ) . OrderByDescending ( x = > x . Priority ) . ToList ( ) ;
2021-10-25 20:06:12 +13:00
foreach ( var afterInteract in afterInteracts )
{
if ( await afterInteract . AfterInteract ( afterInteractEventArgs ) )
2022-02-15 17:06:52 +13:00
return ;
2021-10-25 20:06:12 +13:00
}
2022-02-05 15:39:01 +13:00
if ( target = = null )
2022-02-15 17:06:52 +13:00
return ;
2022-02-05 15:39:01 +13:00
var afterInteractUsingEvent = new AfterInteractUsingEvent ( user , used , target , clickLocation , canReach ) ;
RaiseLocalEvent ( target . Value , afterInteractUsingEvent , false ) ;
2021-10-25 20:06:12 +13:00
}
#region ActivateItemInWorld
/// <summary>
2022-02-15 17:06:52 +13:00
/// Raises <see cref="ActivateInWorldEvent"/> events and activates the IActivate behavior of an object.
2021-10-25 20:06:12 +13:00
/// </summary>
2022-02-15 17:06:52 +13:00
/// <remarks>
/// Does not check the can-use action blocker. In activations interacts can target entities outside of the users
/// hands.
/// </remarks>
public bool InteractionActivate (
EntityUid user ,
EntityUid used ,
bool checkCanInteract = true ,
bool checkUseDelay = true ,
bool checkAccess = true )
2021-10-25 20:06:12 +13:00
{
2022-02-15 17:06:52 +13:00
UseDelayComponent ? delayComponent = null ;
if ( checkUseDelay
& & TryComp ( used , out delayComponent )
& & delayComponent . ActiveDelay )
return false ;
2021-10-25 20:06:12 +13:00
2022-02-15 17:06:52 +13:00
if ( checkCanInteract & & ! _actionBlockerSystem . CanInteract ( user , used ) )
return false ;
2021-10-25 20:06:12 +13:00
2022-02-17 15:40:03 +13:00
if ( checkAccess & & ! InRangeUnobstructed ( user , used , popup : true ) )
2022-02-15 17:06:52 +13:00
return false ;
2021-10-25 20:06:12 +13:00
2021-11-29 12:25:22 +13:00
// Check if interacted entity is in the same container, the direct child, or direct parent of the user.
// This is bypassed IF the interaction happened through an item slot (e.g., backpack UI)
2022-02-15 17:06:52 +13:00
if ( checkAccess & & ! ContainerSystem . IsInSameOrParentContainer ( user , used ) & & ! CanAccessViaStorage ( user , used ) )
return false ;
2021-11-29 12:25:22 +13:00
2021-10-25 20:06:12 +13:00
var activateMsg = new ActivateInWorldEvent ( user , used ) ;
2021-12-03 15:53:09 +01:00
RaiseLocalEvent ( used , activateMsg ) ;
2021-10-25 20:06:12 +13:00
if ( activateMsg . Handled )
2021-11-24 16:52:31 -06:00
{
2022-01-31 00:27:29 +11:00
BeginDelay ( delayComponent ) ;
2021-12-14 00:22:58 +13:00
_adminLogSystem . Add ( LogType . InteractActivate , LogImpact . Low , $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}" ) ;
2022-02-15 17:06:52 +13:00
return true ;
2021-11-24 16:52:31 -06:00
}
2021-10-25 20:06:12 +13:00
2021-12-16 23:42:02 +13:00
if ( ! TryComp ( used , out IActivate ? activateComp ) )
2022-02-15 17:06:52 +13:00
return false ;
2021-10-25 20:06:12 +13:00
var activateEventArgs = new ActivateEventArgs ( user , used ) ;
activateComp . Activate ( activateEventArgs ) ;
2022-01-31 00:27:29 +11:00
BeginDelay ( delayComponent ) ;
2021-12-14 00:22:58 +13:00
_adminLogSystem . Add ( LogType . InteractActivate , LogImpact . Low , $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}" ) ; // No way to check success.
2022-02-15 17:06:52 +13:00
return true ;
2021-10-25 20:06:12 +13:00
}
#endregion
#region Hands
#region Use
/// <summary>
2022-02-15 17:06:52 +13:00
/// Raises UseInHandEvents and activates the IUse behaviors of an entity
/// Does not check accessibility or range, for obvious reasons
2021-10-25 20:06:12 +13:00
/// </summary>
2022-01-05 02:23:01 +13:00
/// <returns>True if the interaction was handled. False otherwise</returns>
2022-02-15 17:06:52 +13:00
public bool UseInHandInteraction (
EntityUid user ,
EntityUid used ,
bool checkCanUse = true ,
bool checkCanInteract = true ,
bool checkUseDelay = true )
2021-10-25 20:06:12 +13:00
{
2022-02-15 17:06:52 +13:00
UseDelayComponent ? delayComponent = null ;
if ( checkUseDelay
& & TryComp ( used , out delayComponent )
& & delayComponent . ActiveDelay )
2022-01-06 14:51:34 +13:00
return true ; // if the item is on cooldown, we consider this handled.
2021-10-25 20:06:12 +13:00
2022-02-15 17:06:52 +13:00
if ( checkCanInteract & & ! _actionBlockerSystem . CanInteract ( user , used ) )
return false ;
if ( checkCanUse & & ! _actionBlockerSystem . CanUseHeldEntity ( user ) )
return false ;
2021-10-25 20:06:12 +13:00
var useMsg = new UseInHandEvent ( user , used ) ;
2021-12-03 15:53:09 +01:00
RaiseLocalEvent ( used , useMsg ) ;
2021-10-25 20:06:12 +13:00
if ( useMsg . Handled )
2022-01-06 14:51:34 +13:00
{
2022-01-31 00:27:29 +11:00
BeginDelay ( delayComponent ) ;
2022-01-05 02:23:01 +13:00
return true ;
2022-01-06 14:51:34 +13:00
}
2021-10-25 20:06:12 +13:00
2021-12-16 23:42:02 +13:00
var uses = AllComps < IUse > ( used ) . ToList ( ) ;
2021-10-25 20:06:12 +13:00
// 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 ) ) )
2022-01-06 14:51:34 +13:00
{
2022-01-31 00:27:29 +11:00
BeginDelay ( delayComponent ) ;
2022-01-05 02:23:01 +13:00
return true ;
2022-01-06 14:51:34 +13:00
}
2021-10-25 20:06:12 +13:00
}
2022-01-05 02:23:01 +13:00
2022-02-15 17:06:52 +13:00
// else, default to activating the item
return InteractionActivate ( user , used , false , false , false ) ;
2021-10-25 20:06:12 +13:00
}
2022-01-31 00:27:29 +11:00
protected virtual void BeginDelay ( UseDelayComponent ? component = null )
{
// This is temporary until we have predicted UseDelay.
return ;
}
2021-10-25 20:06:12 +13:00
/// <summary>
/// Alternative interactions on an entity.
/// </summary>
/// <remarks>
/// Uses the context menu verb list, and acts out the highest priority alternative interaction verb.
/// </remarks>
2022-01-05 02:23:01 +13:00
/// <returns>True if the interaction was handled, false otherwise.</returns>
public bool AltInteract ( EntityUid user , EntityUid target )
2021-10-25 20:06:12 +13:00
{
// Get list of alt-interact verbs
2022-02-10 15:30:59 +13:00
var verbs = _verbSystem . GetLocalVerbs ( target , user , typeof ( AlternativeVerb ) ) ;
2022-01-05 02:23:01 +13:00
if ( ! verbs . Any ( ) )
return false ;
_verbSystem . ExecuteVerb ( verbs . First ( ) , user , target ) ;
return true ;
2021-10-25 20:06:12 +13:00
}
#endregion
#region Throw
/// <summary>
/// Calls Thrown on all components that implement the IThrown interface
/// on an entity that has been thrown.
/// </summary>
2021-12-04 12:35:33 +01:00
public void ThrownInteraction ( EntityUid user , EntityUid thrown )
2021-10-25 20:06:12 +13:00
{
var throwMsg = new ThrownEvent ( user , thrown ) ;
2021-12-03 15:53:09 +01:00
RaiseLocalEvent ( thrown , throwMsg ) ;
2021-10-25 20:06:12 +13:00
if ( throwMsg . Handled )
2021-11-24 16:52:31 -06:00
{
2021-12-14 00:22:58 +13:00
_adminLogSystem . Add ( LogType . Throw , LogImpact . Low , $"{ToPrettyString(user):user} threw {ToPrettyString(thrown):entity}" ) ;
2021-10-25 20:06:12 +13:00
return ;
2021-11-24 16:52:31 -06:00
}
2021-10-25 20:06:12 +13:00
2021-12-14 00:22:58 +13:00
_adminLogSystem . Add ( LogType . Throw , LogImpact . Low , $"{ToPrettyString(user):user} threw {ToPrettyString(thrown):entity}" ) ;
2021-10-25 20:06:12 +13:00
}
#endregion
#region Drop
/// <summary>
/// Activates the Dropped behavior of an object
/// Verifies that the user is capable of doing the drop interaction first
/// </summary>
2021-12-04 12:35:33 +01:00
public bool TryDroppedInteraction ( EntityUid user , EntityUid item )
2021-10-25 20:06:12 +13:00
{
2021-12-26 15:32:45 +13:00
if ( ! _actionBlockerSystem . CanDrop ( user ) ) return false ;
2021-10-25 20:06:12 +13:00
2021-11-24 00:38:39 +01:00
DroppedInteraction ( user , item ) ;
2021-10-25 20:06:12 +13:00
return true ;
}
/// <summary>
/// Calls Dropped on all components that implement the IDropped interface
/// on an entity that has been dropped.
/// </summary>
2021-12-04 12:35:33 +01:00
public void DroppedInteraction ( EntityUid user , EntityUid item )
2021-10-25 20:06:12 +13:00
{
2021-12-03 15:53:09 +01:00
var dropMsg = new DroppedEvent ( user , item ) ;
RaiseLocalEvent ( item , dropMsg ) ;
2021-10-25 20:06:12 +13:00
if ( dropMsg . Handled )
2021-11-24 16:52:31 -06:00
{
2021-12-14 00:22:58 +13:00
_adminLogSystem . Add ( LogType . Drop , LogImpact . Low , $"{ToPrettyString(user):user} dropped {ToPrettyString(item):entity}" ) ;
2021-10-25 20:06:12 +13:00
return ;
2021-11-24 16:52:31 -06:00
}
2021-10-25 20:06:12 +13:00
2021-12-16 23:42:02 +13:00
Transform ( item ) . LocalRotation = Angle . Zero ;
2021-10-25 20:06:12 +13:00
2021-12-16 23:42:02 +13:00
var comps = AllComps < IDropped > ( item ) . ToList ( ) ;
2021-10-25 20:06:12 +13:00
// Call Land on all components that implement the interface
foreach ( var comp in comps )
{
2021-11-24 00:38:39 +01:00
comp . Dropped ( new DroppedEventArgs ( user ) ) ;
2021-10-25 20:06:12 +13:00
}
2021-12-14 00:22:58 +13:00
_adminLogSystem . Add ( LogType . Drop , LogImpact . Low , $"{ToPrettyString(user):user} dropped {ToPrettyString(item):entity}" ) ;
2021-10-25 20:06:12 +13:00
}
#endregion
#endregion
2021-11-29 12:25:22 +13:00
/// <summary>
/// If a target is in range, but not in the same container as the user, it may be inside of a backpack. This
/// checks if the user can access the item in these situations.
/// </summary>
public abstract bool CanAccessViaStorage ( EntityUid user , EntityUid target ) ;
2021-12-16 23:42:02 +13:00
protected bool ValidateClientInput ( ICommonSession ? session , EntityCoordinates coords ,
EntityUid uid , [ NotNullWhen ( true ) ] out EntityUid ? userEntity )
{
userEntity = null ;
if ( ! coords . IsValid ( EntityManager ) )
{
Logger . InfoS ( "system.interaction" , $"Invalid Coordinates: client={session}, coords={coords}" ) ;
return false ;
}
if ( uid . IsClientSide ( ) )
{
Logger . WarningS ( "system.interaction" ,
$"Client sent interaction with client-side entity. Session={session}, Uid={uid}" ) ;
return false ;
}
userEntity = session ? . AttachedEntity ;
if ( userEntity = = null | | ! userEntity . Value . Valid )
{
Logger . WarningS ( "system.interaction" ,
$"Client sent interaction with no attached entity. Session={session}" ) ;
return false ;
}
return true ;
}
2020-04-22 00:58:31 +10:00
}
2021-08-22 03:20:18 +10:00
/// <summary>
/// Raised when a player attempts to activate an item in an inventory slot or hand slot
/// </summary>
[Serializable, NetSerializable]
2022-02-15 17:06:52 +13:00
public sealed class InteractInventorySlotEvent : EntityEventArgs
2021-08-22 03:20:18 +10:00
{
/// <summary>
/// Entity that was interacted with.
/// </summary>
public EntityUid ItemUid { get ; }
/// <summary>
/// Whether the interaction used the alt-modifier to trigger alternative interactions.
/// </summary>
public bool AltInteract { get ; }
public InteractInventorySlotEvent ( EntityUid itemUid , bool altInteract = false )
{
ItemUid = itemUid ;
AltInteract = altInteract ;
}
}
2020-04-22 00:58:31 +10:00
}