2023-04-24 01:20:39 +01:00
using System.ComponentModel ;
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 Content.Shared.ActionBlocker ;
2023-03-06 06:12:08 +13:00
using Content.Shared.Administration ;
2021-11-24 16:52:31 -06:00
using Content.Shared.Administration.Logs ;
2023-03-06 06:12:08 +13:00
using Content.Shared.Administration.Managers ;
2021-12-16 23:42:02 +13:00
using Content.Shared.CombatMode ;
2021-11-28 14:56:53 +01:00
using Content.Shared.Database ;
2022-05-28 13:46:17 +12:00
using Content.Shared.Ghost ;
2023-04-24 01:20:39 +01:00
using Content.Shared.Hands ;
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.Components ;
2022-03-25 22:10:50 +01:00
using Content.Shared.Interaction.Events ;
2023-03-06 06:12:08 +13:00
using Content.Shared.Inventory ;
2023-04-24 01:20:39 +01:00
using Content.Shared.Inventory.Events ;
2022-03-25 22:10:50 +01:00
using Content.Shared.Item ;
2022-08-29 15:59:19 +10:00
using Content.Shared.Movement.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 ;
2022-10-19 01:06:44 +02:00
using Content.Shared.Pulling ;
using Content.Shared.Pulling.Components ;
2023-06-01 19:50:17 -05:00
using Content.Shared.Tag ;
2021-10-25 20:06:12 +13:00
using Content.Shared.Throwing ;
using Content.Shared.Timing ;
using Content.Shared.Verbs ;
2022-03-25 22:10:50 +01:00
using Content.Shared.Wall ;
2020-04-22 00:58:31 +10:00
using JetBrains.Annotations ;
2021-11-29 12:25:22 +13:00
using Robust.Shared.Containers ;
2023-04-24 01:20:39 +01:00
using Robust.Shared.GameObjects ;
2022-03-25 22:10:50 +01:00
using Robust.Shared.Input ;
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 ;
2023-04-24 01:20:39 +01:00
using Robust.Shared.Network ;
2021-02-11 01:13:03 -08:00
using Robust.Shared.Physics ;
2022-09-14 17:26:26 +10:00
using Robust.Shared.Physics.Components ;
using Robust.Shared.Physics.Systems ;
2021-11-29 12:25:22 +13:00
using Robust.Shared.Players ;
2023-02-01 00:33:00 +03:00
using Robust.Shared.Random ;
2021-08-22 03:20:18 +10:00
using Robust.Shared.Serialization ;
2022-03-12 12:53:42 +13:00
using Robust.Shared.Timing ;
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]
2022-12-10 12:05:39 -05:00
public abstract partial class SharedInteractionSystem : EntitySystem
2020-04-22 00:58:31 +10:00
{
2022-03-12 12:53:42 +13:00
[Dependency] private readonly IGameTiming _gameTiming = default ! ;
2023-04-24 01:20:39 +01:00
[Dependency] private readonly INetManager _net = default ! ;
2022-10-17 15:54:31 +11:00
[Dependency] private readonly IMapManager _mapManager = default ! ;
2023-03-06 06:12:08 +13:00
[Dependency] private readonly ISharedAdminManager _adminManager = default ! ;
2022-05-28 23:41:17 -07:00
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default ! ;
2022-10-17 15:54:31 +11:00
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default ! ;
2021-12-16 23:42:02 +13:00
[Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default ! ;
2022-10-17 15:54:31 +11:00
[Dependency] private readonly SharedContainerSystem _containerSystem = default ! ;
[Dependency] private readonly SharedPhysicsSystem _sharedBroadphaseSystem = default ! ;
[Dependency] private readonly SharedTransformSystem _transform = default ! ;
[Dependency] private readonly SharedVerbSystem _verbSystem = default ! ;
2022-02-05 15:39:01 +13:00
[Dependency] private readonly SharedPopupSystem _popupSystem = default ! ;
2022-03-09 20:12:17 +13:00
[Dependency] private readonly UseDelaySystem _useDelay = default ! ;
2022-10-19 01:06:44 +02:00
[Dependency] private readonly SharedPullingSystem _pullSystem = default ! ;
2023-03-06 06:12:08 +13:00
[Dependency] private readonly InventorySystem _inventory = default ! ;
2023-02-01 00:33:00 +03:00
[Dependency] private readonly IRobustRandom _random = default ! ;
2023-06-01 19:50:17 -05:00
[Dependency] private readonly TagSystem _tagSystem = default ! ;
2021-07-26 12:58:17 +02:00
2022-05-13 19:54:37 -07:00
private const CollisionGroup InRangeUnobstructedMask
= CollisionGroup . Impassable | CollisionGroup . InteractImpassable ;
2022-10-07 00:37:21 +11:00
public const float InteractionRange = 1.5f ;
2020-04-22 00:58:31 +10:00
public const float InteractionRangeSquared = InteractionRange * InteractionRange ;
2022-06-16 06:36:36 -07:00
public const float MaxRaycastRange = 100f ;
2022-02-09 07:08:07 +13:00
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 ) ;
2023-04-24 01:20:39 +01:00
SubscribeLocalEvent < UnremoveableComponent , GotUnequippedEvent > ( OnUnequip ) ;
SubscribeLocalEvent < UnremoveableComponent , GotUnequippedHandEvent > ( OnUnequipHand ) ;
SubscribeLocalEvent < UnremoveableComponent , DroppedEvent > ( OnDropped ) ;
2021-12-16 23:42:02 +13:00
CommandBinds . Builder
. Bind ( ContentKeyFunctions . AltActivateItemInWorld ,
new PointerInputCmdHandler ( HandleAltUseInteraction ) )
2022-03-09 20:12:17 +13:00
. Bind ( EngineKeyFunctions . Use ,
new PointerInputCmdHandler ( HandleUseInteraction ) )
. Bind ( ContentKeyFunctions . ActivateItemInWorld ,
new PointerInputCmdHandler ( HandleActivateItemInWorld ) )
2022-10-19 01:06:44 +02:00
. Bind ( ContentKeyFunctions . TryPullObject ,
new PointerInputCmdHandler ( HandleTryPullObject ) )
2021-12-16 23:42:02 +13:00
. Register < SharedInteractionSystem > ( ) ;
2022-12-10 12:05:39 -05:00
InitializeRelay ( ) ;
2023-08-12 17:39:58 -04:00
InitializeBlocking ( ) ;
2021-12-16 23:42:02 +13:00
}
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-10-17 15:54:31 +11:00
if ( ev . Sender . AttachedEntity is not { } user | | ! _actionBlockerSystem . CanInteract ( user , ev . Target ) )
2022-01-31 04:26:07 +13:00
{
ev . Cancel ( ) ;
return ;
}
2023-03-06 06:12:08 +13:00
// Check if the bound entity is accessible. Note that we allow admins to ignore this restriction, so that
// they can fiddle with UI's that people can't normally interact with (e.g., placing things directly into
// other people's backpacks).
if ( ! _containerSystem . IsInSameOrParentContainer ( user , ev . Target )
& & ! CanAccessViaStorage ( user , ev . Target )
& & ! _adminManager . HasAdminFlag ( user , AdminFlags . Admin ) )
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 ( ) ;
}
}
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 ( ) ;
}
2023-04-24 01:20:39 +01:00
/// <summary>
/// If item has DeleteOnDrop true then item will be deleted if removed from inventory, if it is false then item
/// loses Unremoveable when removed from inventory (gibbing).
/// </summary>
private void OnUnequip ( EntityUid uid , UnremoveableComponent item , GotUnequippedEvent args )
{
if ( ! item . DeleteOnDrop )
RemCompDeferred < UnremoveableComponent > ( uid ) ;
else if ( _net . IsServer )
QueueDel ( uid ) ;
}
private void OnUnequipHand ( EntityUid uid , UnremoveableComponent item , GotUnequippedHandEvent args )
{
if ( ! item . DeleteOnDrop )
RemCompDeferred < UnremoveableComponent > ( uid ) ;
else if ( _net . IsServer )
QueueDel ( uid ) ;
}
private void OnDropped ( EntityUid uid , UnremoveableComponent item , DroppedEvent args )
{
if ( ! item . DeleteOnDrop )
RemCompDeferred < UnremoveableComponent > ( uid ) ;
else if ( _net . IsServer )
QueueDel ( uid ) ;
}
2022-10-19 01:06:44 +02:00
private bool HandleTryPullObject ( ICommonSession ? session , EntityCoordinates coords , EntityUid uid )
{
if ( ! ValidateClientInput ( session , coords , uid , out var userEntity ) )
{
Logger . InfoS ( "system.interaction" , $"TryPullObject input validation failed" ) ;
return true ;
}
//is this user trying to pull themself?
if ( userEntity . Value = = uid )
return false ;
if ( Deleted ( uid ) )
return false ;
if ( ! InRangeUnobstructed ( userEntity . Value , uid , popup : true ) )
return false ;
if ( ! TryComp ( uid , out SharedPullableComponent ? pull ) )
return false ;
_pullSystem . TogglePull ( userEntity . Value , pull ) ;
return false ;
}
2022-02-18 17:57:31 -05:00
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 )
{
// client sanitization
2022-08-26 01:36:44 +12:00
if ( ! TryComp ( msg . ItemUid , out TransformComponent ? itemXform ) | | ! ValidateClientInput ( args . SenderSession , itemXform . Coordinates , msg . ItemUid , out var user ) )
2021-12-16 23:42:02 +13:00
{
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.
2022-08-26 01:36:44 +12:00
UserInteraction ( user . Value , itemXform . Coordinates , msg . ItemUid , msg . AltInteract ) ;
2021-12-16 23:42:02 +13:00
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 ;
}
2022-05-28 13:46:17 +12:00
UserInteraction ( user . Value , coords , uid , altInteract : true , checkAccess : ShouldCheckAccess ( user . Value ) ) ;
2021-12-16 23:42:02 +13:00
return false ;
}
2022-03-09 20:12:17 +13:00
public bool HandleUseInteraction ( ICommonSession ? session , EntityCoordinates coords , EntityUid uid )
{
// client sanitization
if ( ! ValidateClientInput ( session , coords , uid , out var userEntity ) )
{
Logger . InfoS ( "system.interaction" , $"Use input validation failed" ) ;
return true ;
}
2022-05-28 13:46:17 +12:00
UserInteraction ( userEntity . Value , coords , ! Deleted ( uid ) ? uid : null , checkAccess : ShouldCheckAccess ( userEntity . Value ) ) ;
2022-03-09 20:12:17 +13:00
return false ;
}
2022-05-28 13:46:17 +12:00
private bool ShouldCheckAccess ( EntityUid user )
{
// This is for Admin/mapping convenience. If ever there are other ghosts that can still interact, this check
// might need to be more selective.
2023-06-01 19:50:17 -05:00
return ! _tagSystem . HasTag ( user , "BypassInteractionRangeChecks" ) ;
2022-05-28 13:46:17 +12:00
}
2021-12-16 23:42:02 +13:00
/// <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
{
2022-12-10 12:05:39 -05:00
if ( TryComp < InteractionRelayComponent > ( user , out var relay ) & & relay . RelayEntity is not null )
{
2023-03-24 14:42:43 +13:00
// TODO this needs to be handled better. This probably bypasses many complex can-interact checks in weird roundabout ways.
if ( _actionBlockerSystem . CanInteract ( user , target ) )
{
UserInteraction ( relay . RelayEntity . Value , coordinates , target , altInteract , checkCanInteract , checkAccess , checkCanUse ) ;
return ;
}
2022-12-10 12:05:39 -05:00
}
2021-12-16 23:42:02 +13:00
if ( target ! = null & & Deleted ( target . Value ) )
return ;
2023-04-08 13:16:48 -07:00
if ( ! altInteract & & TryComp ( user , out CombatModeComponent ? combatMode ) & & combatMode . IsInCombatMode )
2021-12-16 23:42:02 +13:00
{
2022-09-29 15:51:59 +10:00
// Eat the input
2021-12-16 23:42:02 +13:00
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
2022-10-17 15:54:31 +11:00
& & ! _containerSystem . IsInSameOrParentContainer ( user , target . Value )
2022-02-15 17:06:52 +13:00
& & ! CanAccessViaStorage ( user , target . Value ) )
2021-12-16 23:42:02 +13:00
return ;
2022-12-10 12:05:39 -05:00
var inRangeUnobstructed = target = = null
? ! checkAccess | | InRangeUnobstructed ( user , coordinates )
: ! checkAccess | | InRangeUnobstructed ( user , target . Value ) ; // permits interactions with wall mounted entities
2022-02-15 17:06:52 +13:00
// Does the user have hands?
2023-04-07 11:21:12 -07:00
if ( ! TryComp ( user , out HandsComponent ? hands ) | | hands . ActiveHand = = null )
2022-08-13 09:49:41 -04:00
{
2022-12-10 12:05:39 -05:00
var ev = new InteractNoHandEvent ( user , target , coordinates ) ;
RaiseLocalEvent ( user , ev ) ;
2022-08-13 09:49:41 -04:00
if ( target ! = null )
{
2022-12-10 12:05:39 -05:00
var interactedEv = new InteractedNoHandEvent ( target . Value , user , coordinates ) ;
2022-11-03 23:16:23 -04:00
RaiseLocalEvent ( target . Value , interactedEv ) ;
2022-10-26 14:15:48 +13:00
DoContactInteraction ( user , target . Value , ev ) ;
2022-08-13 09:49:41 -04:00
}
2021-12-16 23:42:02 +13:00
return ;
2022-08-13 09:49:41 -04:00
}
2021-12-16 23:42:02 +13:00
2022-02-15 17:06:52 +13:00
// empty-hand interactions
2022-10-17 15:54:31 +11:00
if ( hands . ActiveHandEntity is not { } held )
2022-02-15 17:06:52 +13:00
{
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-03-17 20:13:31 +13:00
if ( target = = held )
2022-02-23 17:58:06 +13:00
{
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 ,
2022-03-17 20:13:31 +13:00
held ,
2022-02-15 17:06:52 +13:00
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 ,
2022-03-17 20:13:31 +13:00
held ,
2022-02-15 17:06:52 +13:00
target ,
coordinates ,
inRangeUnobstructed ) ;
2021-12-16 23:42:02 +13:00
}
2022-03-09 20:12:17 +13:00
public void InteractHand ( EntityUid user , EntityUid target )
2021-12-16 23:42:02 +13:00
{
2023-09-10 07:20:27 +01:00
// allow for special logic before main interaction
var ev = new BeforeInteractHandEvent ( target ) ;
RaiseLocalEvent ( user , ev ) ;
if ( ev . Handled )
{
_adminLogger . Add ( LogType . InteractHand , LogImpact . Low , $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}, but it was handled by another system" ) ;
return ;
}
2022-03-09 20:12:17 +13:00
// all interactions should only happen when in range / unobstructed, so no range check is needed
var message = new InteractHandEvent ( user , target ) ;
2022-06-22 09:53:41 +10:00
RaiseLocalEvent ( target , message , true ) ;
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . InteractHand , LogImpact . Low , $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}" ) ;
2022-10-26 14:15:48 +13:00
DoContactInteraction ( user , target , message ) ;
2022-03-09 20:12:17 +13:00
if ( message . Handled )
return ;
// Else we run Activate.
InteractionActivate ( user , target ,
checkCanInteract : false ,
checkUseDelay : true ,
checkAccess : false ) ;
2021-12-16 23:42:02 +13:00
}
2022-03-09 20:12:17 +13:00
public void InteractUsingRanged ( EntityUid user , EntityUid used , EntityUid ? target ,
2021-12-16 23:42:02 +13:00
EntityCoordinates clickLocation , bool inRangeUnobstructed )
{
2022-03-09 20:12:17 +13:00
if ( RangedInteractDoBefore ( user , used , target , clickLocation , inRangeUnobstructed ) )
return ;
if ( target ! = null )
{
var rangedMsg = new RangedInteractEvent ( user , used , target . Value , clickLocation ) ;
2022-06-22 09:53:41 +10:00
RaiseLocalEvent ( target . Value , rangedMsg , true ) ;
2022-03-09 20:12:17 +13:00
2022-10-26 14:15:48 +13:00
// We contact the USED entity, but not the target.
DoContactInteraction ( user , used , rangedMsg ) ;
2022-03-09 20:12:17 +13:00
if ( rangedMsg . Handled )
return ;
}
InteractDoAfter ( user , used , target , clickLocation , inRangeUnobstructed ) ;
2021-12-16 23:42:02 +13:00
}
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 ,
2022-05-13 19:54:37 -07:00
int collisionMask = ( int ) InRangeUnobstructedMask ,
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
2023-07-08 14:08:32 +10:00
if ( dir . LengthSquared ( ) . Equals ( 0f ) )
2022-10-17 15:54:31 +11:00
return 0f ;
2020-05-28 13:23:50 +02:00
2020-08-30 11:37:06 +02:00
predicate ? ? = _ = > false ;
2023-07-08 14:08:32 +10:00
var ray = new CollisionRay ( origin . Position , dir . Normalized ( ) , collisionMask ) ;
var rayResults = _sharedBroadphaseSystem . IntersectRayWithPredicate ( origin . MapId , ray , dir . Length ( ) , predicate . Invoke , false ) . ToList ( ) ;
2020-05-28 13:23:50 +02:00
2022-10-17 15:54:31 +11:00
if ( rayResults . Count = = 0 )
2023-07-08 14:08:32 +10:00
return dir . Length ( ) ;
2022-10-17 15:54:31 +11:00
2023-07-08 14:08:32 +10: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>
2023-06-01 19:50:17 -05:00
/// <param name="checkAccess">Perform range checks</param>
2020-08-30 11:37:06 +02:00
/// <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 ,
2022-05-13 19:54:37 -07:00
CollisionGroup collisionMask = InRangeUnobstructedMask ,
2023-06-01 19:50:17 -05:00
Ignored ? predicate = null ,
bool checkAccess = true )
2020-04-22 00:58:31 +10:00
{
2022-01-30 14:00:11 +11:00
// Have to be on same map regardless.
2022-10-17 15:54:31 +11:00
if ( other . MapId ! = origin . MapId )
return false ;
2023-04-19 03:43:09 -04:00
2023-06-01 19:50:17 -05:00
if ( ! checkAccess )
return true ;
2020-08-30 11:37:06 +02:00
var dir = other . Position - origin . Position ;
2023-07-08 14:08:32 +10:00
var length = dir . Length ( ) ;
2022-01-30 14:00:11 +11:00
// If range specified also check it
2022-10-17 15:54:31 +11:00
if ( range > 0f & & length > range )
return false ;
2022-02-09 07:08:07 +13:00
2022-10-17 15:54:31 +11:00
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 ;
}
2023-07-08 14:08:32 +10: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
2022-10-17 15:54:31 +11:00
public bool InRangeUnobstructed (
EntityUid origin ,
EntityUid other ,
float range = InteractionRange ,
CollisionGroup collisionMask = InRangeUnobstructedMask ,
Ignored ? predicate = null ,
bool popup = false )
{
if ( ! TryComp < TransformComponent > ( other , out var otherXform ) )
return false ;
return InRangeUnobstructed ( origin , other , otherXform . Coordinates , otherXform . LocalRotation , range , collisionMask , predicate ,
popup ) ;
}
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>
2022-10-17 15:54:31 +11:00
/// <param name="otherAngle">The local rotation to use for the other entity.</param>
2020-08-30 11:37:06 +02:00
/// <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>
2022-10-17 15:54:31 +11:00
/// <param name="otherCoordinates">The coordinates to use for the other entity.</param>
2020-08-30 11:37:06 +02:00
/// <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 ,
2022-10-17 15:54:31 +11:00
EntityCoordinates otherCoordinates ,
Angle otherAngle ,
2020-08-30 11:37:06 +02:00
float range = InteractionRange ,
2022-05-13 19:54:37 -07:00
CollisionGroup collisionMask = InRangeUnobstructedMask ,
2021-03-15 21:55:49 +01:00
Ignored ? predicate = null ,
2020-08-30 11:37:06 +02:00
bool popup = false )
2022-10-07 00:37:21 +11:00
{
2022-04-02 16:11:16 +13:00
Ignored combinedPredicate = e = > e = = origin | | ( predicate ? . Invoke ( e ) ? ? false ) ;
2022-10-07 00:37:21 +11:00
var inRange = true ;
MapCoordinates originPos = default ;
2022-10-17 15:54:31 +11:00
var targetPos = otherCoordinates . ToMap ( EntityManager ) ;
2022-10-07 00:37:21 +11:00
Angle targetRot = default ;
2022-10-17 15:54:31 +11:00
// So essentially:
// 1. If fixtures available check nearest point. We take in coordinates / angles because we might want to use a lag compensated position
// 2. Fall back to centre of body.
2022-10-07 00:37:21 +11:00
// Alternatively we could check centre distances first though
// that means we wouldn't be able to easily check overlap interactions.
if ( range > 0f & &
TryComp < FixturesComponent > ( origin , out var fixtureA ) & &
2022-10-07 17:12:46 +11:00
// These fixture counts are stuff that has the component but no fixtures for <reasons> (e.g. buttons).
// At least until they get removed.
fixtureA . FixtureCount > 0 & &
2022-10-07 00:37:21 +11:00
TryComp < FixturesComponent > ( other , out var fixtureB ) & &
2022-10-07 17:12:46 +11:00
fixtureB . FixtureCount > 0 & &
2022-10-17 15:54:31 +11:00
TryComp < TransformComponent > ( origin , out var xformA ) )
2022-10-07 00:37:21 +11:00
{
2023-09-01 12:30:29 +10:00
var ( worldPosA , worldRotA ) = xformA . GetWorldPositionRotation ( ) ;
2023-01-19 03:56:45 +01:00
var xfA = new Transform ( worldPosA , worldRotA ) ;
2022-10-17 15:54:31 +11:00
var parentRotB = _transform . GetWorldRotation ( otherCoordinates . EntityId ) ;
2023-01-19 03:56:45 +01:00
var xfB = new Transform ( targetPos . Position , parentRotB + otherAngle ) ;
2022-10-17 15:54:31 +11:00
2022-10-07 00:37:21 +11:00
// Different map or the likes.
if ( ! _sharedBroadphaseSystem . TryGetNearest ( origin , other ,
2022-10-17 15:54:31 +11:00
out _ , out _ , out var distance ,
xfA , xfB , fixtureA , fixtureB ) )
2022-10-07 00:37:21 +11:00
{
inRange = false ;
}
// Overlap, early out and no raycast.
else if ( distance . Equals ( 0f ) )
{
return true ;
}
2023-06-01 19:50:17 -05:00
// Entity can bypass range checks.
else if ( ! ShouldCheckAccess ( origin ) )
{
return true ;
}
2022-10-07 00:37:21 +11:00
// Out of range so don't raycast.
else if ( distance > range )
{
inRange = false ;
}
else
{
// We'll still do the raycast from the centres but we'll bump the range as we know they're in range.
originPos = xformA . MapPosition ;
2023-07-08 14:08:32 +10:00
range = ( originPos . Position - targetPos . Position ) . Length ( ) ;
2022-10-07 00:37:21 +11:00
}
}
2022-10-21 00:20:52 +11:00
// No fixtures, e.g. wallmounts.
2022-10-07 00:37:21 +11:00
else
{
originPos = Transform ( origin ) . MapPosition ;
2022-11-27 19:19:41 +13:00
var otherParent = Transform ( other ) . ParentUid ;
targetRot = otherParent . IsValid ( ) ? Transform ( otherParent ) . LocalRotation + otherAngle : otherAngle ;
2022-10-07 00:37:21 +11:00
}
// Do a raycast to check if relevant
if ( inRange )
{
var rayPredicate = GetPredicate ( originPos , other , targetPos , targetRot , collisionMask , combinedPredicate ) ;
2023-06-01 19:50:17 -05:00
inRange = InRangeUnobstructed ( originPos , targetPos , range , collisionMask , rayPredicate , ShouldCheckAccess ( origin ) ) ;
2022-10-07 00:37:21 +11:00
}
2022-02-17 15:40:03 +13:00
2022-03-12 12:53:42 +13:00
if ( ! inRange & & popup & & _gameTiming . IsFirstTimePredicted )
2022-02-17 15:40:03 +13:00
{
var message = Loc . GetString ( "interaction-system-user-interaction-cannot-reach" ) ;
2022-12-19 10:41:47 +13:00
_popupSystem . PopupEntity ( message , origin , origin ) ;
2022-02-17 15:40:03 +13:00
}
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 ,
2022-05-13 19:54:37 -07:00
CollisionGroup collisionMask = InRangeUnobstructedMask ,
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 ) ;
2023-09-01 12:30:29 +10:00
var ( position , rotation ) = transform . GetWorldPositionRotation ( ) ;
2022-02-17 15:40:03 +13:00
var mapPos = new MapCoordinates ( position , transform . MapID ) ;
2022-10-07 00:37:21 +11:00
var combinedPredicate = GetPredicate ( origin , target , mapPos , rotation , collisionMask , predicate ) ;
2022-02-17 15:40:03 +13:00
2022-10-07 00:37:21 +11:00
return InRangeUnobstructed ( origin , mapPos , range , collisionMask , combinedPredicate ) ;
}
/// <summary>
/// Gets the entities to ignore for an unobstructed raycast
/// </summary>
/// <example>
/// if the target entity is a wallmount we ignore all other entities on the tile.
/// </example>
private Ignored GetPredicate (
MapCoordinates origin ,
EntityUid target ,
MapCoordinates targetCoords ,
Angle targetRotation ,
CollisionGroup collisionMask ,
Ignored ? predicate = null )
{
2022-04-02 16:11:16 +13:00
HashSet < EntityUid > ignored = new ( ) ;
2022-02-17 15:40:03 +13:00
2022-07-27 03:53:47 -07:00
if ( HasComp < ItemComponent > ( target ) & & TryComp ( target , out PhysicsComponent ? physics ) & & physics . CanCollide )
2022-02-17 15:40:03 +13:00
{
2022-04-02 16:11:16 +13:00
// If the target is an item, we ignore any colliding entities. Currently done so that if items get stuck
// inside of walls, users can still pick them up.
ignored . UnionWith ( _sharedBroadphaseSystem . GetEntitiesIntersectingBody ( target , ( int ) collisionMask , false , physics ) ) ;
2022-02-17 15:40:03 +13:00
}
2022-04-02 16:11:16 +13:00
else if ( TryComp ( target , out WallMountComponent ? wallMount ) )
2022-02-17 15:40:03 +13:00
{
2022-04-02 16:11:16 +13:00
// wall-mount exemptions may be restricted to a specific angle range.da
2022-10-17 15:54:31 +11:00
bool ignoreAnchored ;
2022-04-02 16:11:16 +13:00
if ( wallMount . Arc > = Math . Tau )
ignoreAnchored = true ;
else
{
2022-10-07 00:37:21 +11:00
var angle = Angle . FromWorldVec ( origin . Position - targetCoords . Position ) ;
var angleDelta = ( wallMount . Direction + targetRotation - angle ) . Reduced ( ) . FlipPositive ( ) ;
2022-04-02 16:11:16 +13:00
ignoreAnchored = angleDelta < wallMount . Arc / 2 | | Math . Tau - angleDelta < wallMount . Arc / 2 ;
}
2023-05-28 23:22:44 +10:00
if ( ignoreAnchored & & _mapManager . TryFindGridAt ( targetCoords , out _ , out var grid ) )
2022-10-07 00:37:21 +11:00
ignored . UnionWith ( grid . GetAnchoredEntities ( targetCoords ) ) ;
2022-02-17 15:40:03 +13:00
}
2022-04-02 16:11:16 +13:00
Ignored combinedPredicate = e = >
{
return e = = target
2022-10-07 00:37:21 +11:00
| | ( predicate ? . Invoke ( e ) ? ? false )
| | ignored . Contains ( e ) ;
2022-04-02 16:11:16 +13:00
} ;
2022-10-07 00:37:21 +11:00
return combinedPredicate ;
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 ,
2022-05-13 19:54:37 -07:00
CollisionGroup collisionMask = InRangeUnobstructedMask ,
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 ,
2022-05-13 19:54:37 -07:00
CollisionGroup collisionMask = InRangeUnobstructedMask ,
2021-03-15 21:55:49 +01:00
Ignored ? predicate = null ,
2020-08-30 11:37:06 +02:00
bool popup = false )
{
2022-10-07 00:37:21 +11:00
Ignored combinedPredicate = e = > e = = origin | | ( predicate ? . Invoke ( e ) ? ? false ) ;
2021-12-16 23:42:02 +13:00
var originPosition = Transform ( origin ) . MapPosition ;
2023-06-01 19:50:17 -05:00
var inRange = InRangeUnobstructed ( originPosition , other , range , collisionMask , combinedPredicate , ShouldCheckAccess ( origin ) ) ;
2020-08-30 11:37:06 +02:00
2022-03-12 12:53:42 +13:00
if ( ! inRange & & popup & & _gameTiming . IsFirstTimePredicted )
2020-08-30 11:37:06 +02:00
{
2022-02-15 17:06:52 +13:00
var message = Loc . GetString ( "interaction-system-user-interaction-cannot-reach" ) ;
2022-12-19 10:41:47 +13:00
_popupSystem . PopupEntity ( message , origin , 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 ) ;
2022-10-17 15:54:31 +11:00
RaiseLocalEvent ( used , ev ) ;
2022-10-26 14:15:48 +13:00
// We contact the USED entity, but not the target.
DoContactInteraction ( user , used , ev ) ;
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-08-14 07:28:34 +02:00
public void InteractUsing (
2022-02-15 17:06:52 +13:00
EntityUid user ,
EntityUid used ,
EntityUid target ,
EntityCoordinates clickLocation ,
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-03-31 20:08:30 +13:00
var interactUsingEvent = new InteractUsingEvent ( user , used , target , clickLocation ) ;
2022-06-22 09:53:41 +10:00
RaiseLocalEvent ( target , interactUsingEvent , true ) ;
2022-10-26 14:15:48 +13:00
DoContactInteraction ( user , used , interactUsingEvent ) ;
DoContactInteraction ( user , target , interactUsingEvent ) ;
DoContactInteraction ( used , target , interactUsingEvent ) ;
2021-10-25 20:06:12 +13:00
if ( interactUsingEvent . Handled )
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-08-14 07:28:34 +02:00
public 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 ) ;
2022-10-26 14:15:48 +13:00
RaiseLocalEvent ( used , afterInteractEvent ) ;
DoContactInteraction ( user , used , afterInteractEvent ) ;
if ( canReach )
{
DoContactInteraction ( user , target , afterInteractEvent ) ;
DoContactInteraction ( used , target , afterInteractEvent ) ;
}
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
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 ) ;
2022-10-17 15:54:31 +11:00
RaiseLocalEvent ( target . Value , afterInteractUsingEvent ) ;
2022-10-26 14:15:48 +13:00
DoContactInteraction ( user , used , afterInteractUsingEvent ) ;
if ( canReach )
{
DoContactInteraction ( user , target , afterInteractUsingEvent ) ;
DoContactInteraction ( used , target , afterInteractUsingEvent ) ;
}
2021-10-25 20:06:12 +13:00
}
#region ActivateItemInWorld
2022-03-09 20:12:17 +13:00
private bool HandleActivateItemInWorld ( ICommonSession ? session , EntityCoordinates coords , EntityUid uid )
{
if ( ! ValidateClientInput ( session , coords , uid , out var user ) )
{
Logger . InfoS ( "system.interaction" , $"ActivateItemInWorld input validation failed" ) ;
return false ;
}
if ( Deleted ( uid ) )
return false ;
2022-05-28 13:46:17 +12:00
InteractionActivate ( user . Value , uid , checkAccess : ShouldCheckAccess ( user . Value ) ) ;
2022-03-09 20:12:17 +13:00
return false ;
}
2021-10-25 20:06:12 +13:00
/// <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-03-12 12:53:42 +13:00
if ( checkAccess & & ! InRangeUnobstructed ( user , used ) )
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-10-17 15:54:31 +11:00
if ( checkAccess & & ! _containerSystem . IsInSameOrParentContainer ( user , used ) & & ! CanAccessViaStorage ( user , used ) )
2022-02-15 17:06:52 +13:00
return false ;
2021-11-29 12:25:22 +13:00
2022-03-25 22:10:50 +01:00
// Does the user have hands?
2023-04-07 11:21:12 -07:00
if ( ! HasComp < HandsComponent > ( user ) )
2022-03-25 22:10:50 +01:00
return false ;
2021-10-25 20:06:12 +13:00
var activateMsg = new ActivateInWorldEvent ( user , used ) ;
2022-06-22 09:53:41 +10:00
RaiseLocalEvent ( used , activateMsg , true ) ;
2022-07-14 22:29:29 +12:00
if ( ! activateMsg . Handled )
2022-02-15 17:06:52 +13:00
return false ;
2021-10-25 20:06:12 +13:00
2022-10-26 14:15:48 +13:00
DoContactInteraction ( user , used , activateMsg ) ;
2022-03-09 20:12:17 +13:00
_useDelay . BeginDelay ( used , delayComponent ) ;
2023-01-20 10:05:05 -06:00
if ( ! activateMsg . WasLogged )
_adminLogger . Add ( LogType . InteractActivate , LogImpact . Low , $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}" ) ;
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 ;
2022-03-13 01:33:23 +13:00
var useMsg = new UseInHandEvent ( user ) ;
2022-06-22 09:53:41 +10:00
RaiseLocalEvent ( used , useMsg , true ) ;
2021-10-25 20:06:12 +13:00
if ( useMsg . Handled )
2022-01-06 14:51:34 +13:00
{
2022-10-26 14:15:48 +13:00
DoContactInteraction ( user , used , useMsg ) ;
2022-03-09 20:12:17 +13:00
_useDelay . BeginDelay ( used , 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-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
}
/// <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 ) ;
2022-06-22 09:53:41 +10:00
RaiseLocalEvent ( thrown , throwMsg , true ) ;
2021-10-25 20:06:12 +13:00
if ( throwMsg . Handled )
2021-11-24 16:52:31 -06:00
{
2022-05-28 23:41:17 -07:00
_adminLogger . 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
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Throw , LogImpact . Low , $"{ToPrettyString(user):user} threw {ToPrettyString(thrown):entity}" ) ;
2021-10-25 20:06:12 +13:00
}
#endregion
2021-12-04 12:35:33 +01:00
public void DroppedInteraction ( EntityUid user , EntityUid item )
2021-10-25 20:06:12 +13:00
{
2022-03-13 21:47:28 +13:00
var dropMsg = new DroppedEvent ( user ) ;
2022-06-22 09:53:41 +10:00
RaiseLocalEvent ( item , dropMsg , true ) ;
2021-10-25 20:06:12 +13:00
if ( dropMsg . Handled )
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Drop , LogImpact . Low , $"{ToPrettyString(user):user} dropped {ToPrettyString(item):entity}" ) ;
2022-08-29 15:59:19 +10:00
// If the dropper is rotated then use their targetrelativerotation as the drop rotation
var rotation = Angle . Zero ;
if ( TryComp < InputMoverComponent > ( user , out var mover ) )
{
rotation = mover . TargetRelativeRotation ;
}
Transform ( item ) . LocalRotation = rotation ;
2021-10-25 20:06:12 +13:00
}
#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 ) ;
2023-03-06 06:12:08 +13:00
/// <summary>
/// Checks whether an entity currently equipped by another player is accessible to some user. This shouldn't
/// be used as a general interaction check, as these kinda of interactions should generally trigger a
/// do-after and a warning for the other player.
/// </summary>
public bool CanAccessEquipment ( EntityUid user , EntityUid target )
{
if ( Deleted ( target ) )
return false ;
if ( ! _containerSystem . TryGetContainingContainer ( target , out var container ) )
return false ;
var wearer = container . Owner ;
if ( ! _inventory . TryGetSlot ( wearer , container . ID , out var slotDef ) )
return false ;
if ( wearer = = user )
return true ;
if ( slotDef . StripHidden )
return false ;
return InRangeUnobstructed ( user , wearer ) & & _containerSystem . IsInSameOrParentContainer ( user , wearer ) ;
}
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 ;
}
2022-08-26 01:33:40 +12:00
if ( ! Exists ( userEntity ) )
{
Logger . WarningS ( "system.interaction" ,
$"Client attempted interaction with a non-existent attached entity. Session={session}, entity={userEntity}" ) ;
return false ;
}
2021-12-16 23:42:02 +13:00
return true ;
}
2022-10-26 14:15:48 +13:00
/// <summary>
/// Simple convenience function to raise contact events (disease, forensics, etc).
/// </summary>
public void DoContactInteraction ( EntityUid uidA , EntityUid ? uidB , HandledEntityEventArgs ? args = null )
{
if ( uidB = = null | | args ? . Handled = = false )
return ;
2022-10-31 19:35:35 +13:00
// Entities may no longer exist (banana was eaten, or human was exploded)?
if ( ! Exists ( uidA ) | | ! Exists ( uidB ) )
return ;
2023-08-05 00:29:52 -04:00
if ( Paused ( uidA ) | | Paused ( uidB . Value ) )
return ;
2022-10-26 14:15:48 +13:00
RaiseLocalEvent ( uidA , new ContactInteractionEvent ( uidB . Value ) ) ;
RaiseLocalEvent ( uidB . Value , new ContactInteractionEvent ( uidA ) ) ;
}
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
}