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 ;
using Content.Shared.Interaction.Helpers ;
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-02-11 01:13:03 -08:00
using Robust.Shared.GameObjects ;
2021-12-16 23:42:02 +13:00
using Robust.Shared.Input.Binding ;
2021-07-26 12:58:17 +02:00
using Robust.Shared.IoC ;
2021-06-21 02:13:54 +02:00
using Robust.Shared.Localization ;
2021-12-16 23:42:02 +13:00
using Robust.Shared.Log ;
2020-04-22 00:58:31 +10:00
using Robust.Shared.Map ;
2021-10-25 20:06:12 +13:00
using Robust.Shared.Maths ;
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 ;
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
{
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 ! ;
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 ;
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 ( )
{
SubscribeAllEvent < InteractInventorySlotEvent > ( HandleInteractInventorySlotEvent ) ;
CommandBinds . Builder
. Bind ( ContentKeyFunctions . AltActivateItemInWorld ,
new PointerInputCmdHandler ( HandleAltUseInteraction ) )
. Register < SharedInteractionSystem > ( ) ;
}
public override void Shutdown ( )
{
CommandBinds . Unregister < SharedInteractionSystem > ( ) ;
base . Shutdown ( ) ;
}
/// <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 ;
}
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>
public async void UserInteraction ( EntityUid user , EntityCoordinates coordinates , EntityUid ? target , bool altInteract = false )
{
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 ;
if ( ! _actionBlockerSystem . CanInteract ( user ) )
return ;
// 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)
if ( target ! = null & & ! user . IsInSameOrParentContainer ( target . Value ) & & ! CanAccessViaStorage ( user , target . Value ) )
return ;
// Verify user has a hand, and find what object they are currently holding in their active hand
if ( ! TryComp ( user , out SharedHandsComponent ? hands ) )
return ;
// 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 )
{
2021-12-30 18:27:15 -08:00
if ( ! hands . TryGetActiveHeldEntity ( out var heldEntity ) )
2021-12-16 23:42:02 +13:00
return ;
2021-12-30 18:27:15 -08:00
if ( ! await InteractUsingRanged ( user , heldEntity . Value , target , coordinates , inRangeUnobstructed ) & &
2021-12-16 23:42:02 +13:00
! inRangeUnobstructed )
{
var message = Loc . GetString ( "interaction-system-user-interaction-cannot-reach" ) ;
user . PopupMessage ( message ) ;
}
return ;
}
2021-12-30 18:27:15 -08:00
2022-01-01 16:16:45 +13:00
// We are close to the nearby object.
if ( altInteract )
{
// Perform alternative interactions, using context menu verbs.
AltInteract ( user , target . Value ) ;
}
else if ( ! hands . TryGetActiveHeldEntity ( out var heldEntity ) )
{
// Since our hand is empty we will use InteractHand/Activate
InteractHand ( user , target . Value ) ;
}
else if ( heldEntity ! = target )
{
await InteractUsing ( user , heldEntity . Value , target . Value , coordinates ) ;
2021-12-16 23:42:02 +13:00
}
}
public virtual void InteractHand ( EntityUid user , EntityUid target )
{
// 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.
}
public virtual async Task < bool > 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.
return await Task . FromResult ( true ) ;
}
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
}
/// <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>
/// <param name="collisionMask">The mask to check for collisions</param>
/// <param name="ignoredEnt">
/// The entity to be ignored when checking for collisions.
/// </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-12-04 12:35:33 +01:00
EntityUid ? ignoredEnt = null )
2020-08-30 11:37:06 +02:00
{
var predicate = ignoredEnt = = null
? null
2021-12-04 12:35:33 +01:00
: ( Ignored ) ( e = > e = = ignoredEnt ) ;
2020-08-30 11:37:06 +02:00
return UnobstructedDistance ( origin , other , collisionMask , predicate ) ;
}
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>
/// <param name="ignoreInsideBlocker">
/// If true and <see cref="origin"/> or <see cref="other"/> are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
/// </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 ,
2021-03-15 21:55:49 +01:00
Ignored ? predicate = null ,
2020-08-30 11:37:06 +02:00
bool ignoreInsideBlocker = false )
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
2022-01-30 14:00:11 +11:00
// Uhh this does mean we could raycast infinity distance so may need to limit it.
2020-08-30 11:37:06 +02:00
var dir = other . Position - origin . Position ;
2022-01-30 14:00:11 +11:00
var lengthSquared = dir . LengthSquared ;
2020-04-22 00:58:31 +10:00
2022-01-30 14:00:11 +11:00
if ( lengthSquared . Equals ( 0f ) ) return true ;
// If range specified also check it
if ( range > 0f & & lengthSquared > range * range ) return false ;
2020-04-22 00:58:31 +10:00
2020-08-30 11:37:06 +02:00
predicate ? ? = _ = > false ;
var ray = new CollisionRay ( origin . Position , dir . Normalized , ( int ) collisionMask ) ;
2021-07-26 12:58:17 +02:00
var rayResults = _sharedBroadphaseSystem . IntersectRayWithPredicate ( origin . MapId , ray , dir . Length , predicate . Invoke , false ) . ToList ( ) ;
2020-08-30 11:37:06 +02:00
if ( rayResults . Count = = 0 ) return true ;
2021-09-16 13:02:10 +10:00
// TODO: Wot? This should just be in the predicate.
2020-08-30 11:37:06 +02:00
if ( ! ignoreInsideBlocker ) return false ;
2020-10-28 13:04:29 +01:00
foreach ( var result in rayResults )
{
2021-12-16 23:42:02 +13:00
if ( ! TryComp ( result . HitEntity , out IPhysBody ? p ) )
2020-10-28 13:04:29 +01:00
{
continue ;
}
2020-08-30 11:37:06 +02:00
2021-03-08 04:09:59 +11:00
var bBox = p . GetWorldAABB ( ) ;
2020-10-28 13:04:29 +01:00
if ( bBox . Contains ( origin . Position ) | | bBox . Contains ( other . Position ) )
{
continue ;
}
return false ;
}
return true ;
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.
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="ignoreInsideBlocker">
/// If true and <see cref="origin"/> or <see cref="other"/> are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
/// </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 ignoreInsideBlocker = false ,
bool popup = false )
{
2021-12-04 12:35:33 +01:00
predicate ? ? = e = > e = = origin | | e = = other ;
2021-12-16 23:42:02 +13:00
return InRangeUnobstructed ( origin , Transform ( other ) . MapPosition , range , collisionMask , predicate , ignoreInsideBlocker , popup ) ;
2020-08-30 11:37:06 +02:00
}
/// <summary>
/// Checks that an entity and a component 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 component to use.</param>
/// <param name="range">
/// Maximum distance between the entity and component.
/// </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="ignoreInsideBlocker">
/// If true and <see cref="origin"/> or <see cref="other"/> are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
/// </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
IComponent 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 ignoreInsideBlocker = false ,
bool popup = false )
{
2021-06-07 05:49:43 -07:00
return InRangeUnobstructed ( origin , other . Owner , range , collisionMask , predicate , ignoreInsideBlocker , popup ) ;
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="ignoreInsideBlocker">
/// If true and <see cref="origin"/> or <see cref="other"/> are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
/// </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 ignoreInsideBlocker = false ,
bool popup = false )
{
2021-06-07 05:49:43 -07:00
return InRangeUnobstructed ( origin , other . ToMap ( EntityManager ) , range , collisionMask , predicate , ignoreInsideBlocker , 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="ignoreInsideBlocker">
/// If true and <see cref="origin"/> or <see cref="other"/> are inside
/// the obstruction, ignores the obstruction and considers the interaction
/// unobstructed.
/// Therefore, setting this to true makes this check more permissive,
/// such as allowing an interaction to occur inside something impassable
/// (like a wall). The default, false, makes the check more restrictive.
/// </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 ignoreInsideBlocker = false ,
bool popup = false )
{
2021-12-16 23:42:02 +13:00
var originPosition = Transform ( origin ) . MapPosition ;
2021-12-04 12:35:33 +01:00
predicate ? ? = e = > e = = origin ;
2020-08-30 11:37:06 +02:00
var inRange = InRangeUnobstructed ( originPosition , other , range , collisionMask , predicate , ignoreInsideBlocker ) ;
if ( ! inRange & & popup )
{
2021-06-21 02:13:54 +02:00
var message = Loc . GetString ( "shared-interaction-system-in-range-unobstructed-cannot-reach" ) ;
2020-09-01 12:34:53 +02:00
origin . PopupMessage ( message ) ;
2020-08-30 11:37:06 +02:00
}
return inRange ;
}
2021-10-25 20:06:12 +13:00
2021-10-28 13:19:38 +02:00
public bool InteractDoBefore (
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 )
{
var ev = new BeforeInteractEvent ( 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-01-30 13:50:10 +13:00
public async Task InteractUsing ( EntityUid user , EntityUid used , EntityUid target , EntityCoordinates clickLocation , bool predicted = false )
2021-10-25 20:06:12 +13:00
{
2021-12-03 15:53:09 +01:00
if ( ! _actionBlockerSystem . CanInteract ( user ) )
2021-10-25 20:06:12 +13:00
return ;
2021-10-28 13:19:38 +02:00
if ( InteractDoBefore ( 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 ;
}
// 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>
/// We didn't click on any entity, try doing an AfterInteract on the click location
/// </summary>
2021-12-04 12:35:33 +01:00
public async Task < bool > 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 )
return true ;
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 ) )
return true ;
}
return false ;
}
#region ActivateItemInWorld
/// <summary>
/// Activates the IActivate behavior of an object
/// Verifies that the user is capable of doing the use interaction first
/// </summary>
2021-12-04 12:35:33 +01:00
public void TryInteractionActivate ( EntityUid ? user , EntityUid ? used )
2021-10-25 20:06:12 +13:00
{
if ( user = = null | | used = = null )
return ;
2021-12-04 12:35:33 +01:00
InteractionActivate ( user . Value , used . Value ) ;
2021-10-25 20:06:12 +13:00
}
2021-12-04 12:35:33 +01:00
protected void InteractionActivate ( EntityUid user , EntityUid used )
2021-10-25 20:06:12 +13:00
{
2022-01-06 14:51:34 +13:00
if ( TryComp ( used , out UseDelayComponent ? delayComponent ) & & delayComponent . ActiveDelay )
return ;
2021-10-25 20:06:12 +13:00
2021-12-03 15:53:09 +01:00
if ( ! _actionBlockerSystem . CanInteract ( user ) | | ! _actionBlockerSystem . CanUse ( user ) )
2021-10-25 20:06:12 +13:00
return ;
// all activates should only fire when in range / unobstructed
if ( ! InRangeUnobstructed ( user , used , ignoreInsideBlocker : true , popup : true ) )
return ;
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)
2021-12-03 15:53:09 +01:00
if ( ! user . IsInSameOrParentContainer ( used ) & & ! CanAccessViaStorage ( user , used ) )
2021-11-29 12:25:22 +13:00
return ;
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}" ) ;
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
if ( ! TryComp ( used , out IActivate ? activateComp ) )
2021-10-25 20:06:12 +13:00
return ;
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.
2021-10-25 20:06:12 +13:00
}
#endregion
#region Hands
#region Use
/// <summary>
2022-01-05 02:23:01 +13:00
/// Attempt to perform a use-interaction on an entity. If no interaction occurs, it will instead attempt to
/// activate the entity.
2021-10-25 20:06:12 +13:00
/// </summary>
2022-01-05 02:23:01 +13:00
public void TryUseInteraction ( EntityUid user , EntityUid used )
2021-10-25 20:06:12 +13:00
{
2022-01-05 02:23:01 +13:00
if ( _actionBlockerSystem . CanUse ( user ) & & UseInteraction ( user , used ) )
return ;
// no use-interaction occurred. Attempt to activate the item instead.
InteractionActivate ( user , used ) ;
2021-10-25 20:06:12 +13:00
}
/// <summary>
/// Activates the IUse behaviors of an entity without first checking
/// if the user is capable of doing the use interaction.
/// </summary>
2022-01-05 02:23:01 +13:00
/// <returns>True if the interaction was handled. False otherwise</returns>
public bool UseInteraction ( EntityUid user , EntityUid used )
2021-10-25 20:06:12 +13:00
{
2022-01-06 14:51:34 +13:00
if ( TryComp ( used , out UseDelayComponent ? delayComponent ) & & delayComponent . ActiveDelay )
return true ; // if the item is on cooldown, we consider this handled.
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
return 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
2021-11-29 12:25:22 +13:00
var verbs = _verbSystem . GetLocalVerbs ( target , user , VerbType . Alternative ) [ VerbType . Alternative ] ;
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]
public class InteractInventorySlotEvent : EntityEventArgs
{
/// <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
}