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 ;
2021-11-24 16:52:31 -06:00
using Content.Shared.Administration.Logs ;
2024-09-30 01:19:00 +13:00
using Content.Shared.CCVar ;
using Content.Shared.Chat ;
2021-12-16 23:42:02 +13:00
using Content.Shared.CombatMode ;
2021-11-28 14:56:53 +01:00
using Content.Shared.Database ;
2024-05-24 17:03:03 +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 ;
2024-03-19 14:30:56 +11:00
using Content.Shared.Movement.Pulling.Systems ;
2020-04-25 11:37:59 +02:00
using Content.Shared.Physics ;
2024-09-30 01:19:00 +13:00
using Content.Shared.Players.RateLimiting ;
2021-09-26 15:18:45 +02:00
using Content.Shared.Popups ;
2024-05-24 17:03:03 +12:00
using Content.Shared.Storage ;
2024-11-21 18:56:05 -08:00
using Content.Shared.Strip ;
2023-06-01 19:50:17 -05:00
using Content.Shared.Tag ;
2021-10-25 20:06:12 +13:00
using Content.Shared.Timing ;
2024-05-24 17:03:03 +12:00
using Content.Shared.UserInterface ;
2021-10-25 20:06:12 +13:00
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 ;
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 ;
2023-10-28 09:59:53 +11:00
using Robust.Shared.Player ;
2021-08-22 03:20:18 +10:00
using Robust.Shared.Serialization ;
2022-03-12 12:53:42 +13:00
using Robust.Shared.Timing ;
2024-05-24 17:03:03 +12:00
using Robust.Shared.Utility ;
2020-04-22 00:58:31 +10:00
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 ! ;
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 ! ;
2024-05-24 17:03:03 +12:00
[Dependency] private readonly SharedPhysicsSystem _broadphase = default ! ;
2022-10-17 15:54:31 +11:00
[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 ! ;
2024-03-19 14:30:56 +11:00
[Dependency] private readonly PullingSystem _pullSystem = default ! ;
2023-03-06 06:12:08 +13:00
[Dependency] private readonly InventorySystem _inventory = default ! ;
2023-06-01 19:50:17 -05:00
[Dependency] private readonly TagSystem _tagSystem = default ! ;
2024-05-24 17:03:03 +12:00
[Dependency] private readonly SharedUserInterfaceSystem _ui = default ! ;
2024-11-21 18:56:05 -08:00
[Dependency] private readonly SharedStrippableSystem _strippable = default ! ;
2024-09-30 01:19:00 +13:00
[Dependency] private readonly SharedPlayerRateLimitManager _rateLimit = default ! ;
[Dependency] private readonly ISharedChatManager _chat = default ! ;
2024-05-24 17:03:03 +12:00
private EntityQuery < IgnoreUIRangeComponent > _ignoreUiRangeQuery ;
private EntityQuery < FixturesComponent > _fixtureQuery ;
private EntityQuery < ItemComponent > _itemQuery ;
private EntityQuery < PhysicsComponent > _physicsQuery ;
private EntityQuery < HandsComponent > _handsQuery ;
private EntityQuery < InteractionRelayComponent > _relayQuery ;
private EntityQuery < CombatModeComponent > _combatQuery ;
private EntityQuery < WallMountComponent > _wallMountQuery ;
private EntityQuery < UseDelayComponent > _delayQuery ;
private EntityQuery < ActivatableUIComponent > _uiQuery ;
2021-07-26 12:58:17 +02:00
2024-01-03 21:33:09 -04:00
private const CollisionGroup InRangeUnobstructedMask = CollisionGroup . Impassable | CollisionGroup . InteractImpassable ;
2022-05-13 19:54:37 -07:00
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 ;
2024-09-30 01:19:00 +13:00
public const string RateLimitKey = "Interaction" ;
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 ( )
{
2024-05-24 17:03:03 +12:00
_ignoreUiRangeQuery = GetEntityQuery < IgnoreUIRangeComponent > ( ) ;
_fixtureQuery = GetEntityQuery < FixturesComponent > ( ) ;
_itemQuery = GetEntityQuery < ItemComponent > ( ) ;
_physicsQuery = GetEntityQuery < PhysicsComponent > ( ) ;
_handsQuery = GetEntityQuery < HandsComponent > ( ) ;
_relayQuery = GetEntityQuery < InteractionRelayComponent > ( ) ;
_combatQuery = GetEntityQuery < CombatModeComponent > ( ) ;
_wallMountQuery = GetEntityQuery < WallMountComponent > ( ) ;
_delayQuery = GetEntityQuery < UseDelayComponent > ( ) ;
_uiQuery = GetEntityQuery < ActivatableUIComponent > ( ) ;
2024-04-26 18:16:24 +10:00
SubscribeLocalEvent < BoundUserInterfaceCheckRangeEvent > ( HandleUserInterfaceRangeCheck ) ;
2022-01-31 04:26:07 +13:00
SubscribeLocalEvent < BoundUserInterfaceMessageAttempt > ( OnBoundInterfaceInteractAttempt ) ;
2024-04-26 18:16:24 +10:00
2021-12-16 23:42:02 +13:00
SubscribeAllEvent < InteractInventorySlotEvent > ( HandleInteractInventorySlotEvent ) ;
2024-04-26 18:16:24 +10:00
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
2024-09-30 01:19:00 +13:00
_rateLimit . Register ( RateLimitKey ,
new RateLimitRegistration ( CCVars . InteractionRateLimitPeriod ,
CCVars . InteractionRateLimitCount ,
null ,
CCVars . InteractionRateLimitAnnounceAdminsDelay ,
RateLimitAlertAdmins )
) ;
2023-08-12 17:39:58 -04:00
InitializeBlocking ( ) ;
2021-12-16 23:42:02 +13:00
}
2024-09-30 01:19:00 +13:00
private void RateLimitAlertAdmins ( ICommonSession session )
{
_chat . SendAdminAlert ( Loc . GetString ( "interaction-rate-limit-admin-announcement" , ( "player" , session . Name ) ) ) ;
}
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 )
{
2024-05-24 17:03:03 +12:00
_uiQuery . TryComp ( ev . Target , out var uiComp ) ;
if ( ! _actionBlockerSystem . CanInteract ( ev . Actor , ev . Target ) )
{
// We permit ghosts to open uis unless explicitly blocked
if ( ev . Message is not OpenBoundInterfaceMessage | | ! HasComp < GhostComponent > ( ev . Actor ) | | uiComp ? . BlockSpectators = = true )
{
ev . Cancel ( ) ;
return ;
}
}
var range = _ui . GetUiRange ( ev . Target , ev . UiKey ) ;
2024-04-26 18:16:24 +10:00
2024-05-24 17:03:03 +12:00
// As long as range>0, the UI frame updates should have auto-closed the UI if it is out of range.
DebugTools . Assert ( range < = 0 | | UiRangeCheck ( ev . Actor , ev . Target , range ) ) ;
if ( range < = 0 & & ! IsAccessible ( ev . Actor , ev . Target ) )
2022-01-31 04:26:07 +13:00
{
ev . Cancel ( ) ;
return ;
}
2024-05-24 17:03:03 +12:00
if ( uiComp = = null )
return ;
2024-05-29 20:25:16 +10:00
if ( uiComp . SingleUser & & uiComp . CurrentSingleUser ! = null & & uiComp . CurrentSingleUser ! = ev . Actor )
2022-01-31 04:26:07 +13:00
{
ev . Cancel ( ) ;
return ;
}
2024-08-28 10:57:12 +10:00
if ( uiComp . RequiresComplex & & ! _actionBlockerSystem . CanComplexInteract ( ev . Actor ) )
2022-01-31 04:26:07 +13:00
ev . Cancel ( ) ;
2024-05-24 17:03:03 +12:00
}
private bool UiRangeCheck ( Entity < TransformComponent ? > user , Entity < TransformComponent ? > target , float range )
{
if ( ! Resolve ( target , ref target . Comp ) )
return false ;
if ( user . Owner = = target . Owner )
return true ;
// Fast check: if the user is the parent of the entity (e.g., holding it), we always assume that it is in range
if ( target . Comp . ParentUid = = user . Owner )
return true ;
return InRangeAndAccessible ( user , target , range ) | | _ignoreUiRangeQuery . HasComp ( user ) ;
2022-01-31 04:26:07 +13:00
}
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 ) )
{
2024-03-10 01:15:13 +01:00
Log . Info ( $"TryPullObject input validation failed" ) ;
2022-10-19 01:06:44 +02:00
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 ;
2024-05-24 17:03:03 +12:00
_pullSystem . TogglePull ( uid , userEntity . Value ) ;
2022-10-19 01:06:44 +02:00
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 )
{
2023-09-11 09:42:41 +10:00
var item = GetEntity ( msg . ItemUid ) ;
2021-12-16 23:42:02 +13:00
// client sanitization
2023-09-11 09:42:41 +10:00
if ( ! TryComp ( item , out TransformComponent ? itemXform ) | | ! ValidateClientInput ( args . SenderSession , itemXform . Coordinates , item , out var user ) )
2021-12-16 23:42:02 +13:00
{
2024-03-10 01:15:13 +01:00
Log . Info ( $"Inventory interaction validation failed. Session={args.SenderSession}" ) ;
2021-12-16 23:42:02 +13:00
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.
2023-09-11 09:42:41 +10:00
UserInteraction ( user . Value , itemXform . Coordinates , item , 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
2023-09-11 09:42:41 +10:00
InteractionActivate ( user . Value , item ) ;
2021-12-16 23:42:02 +13:00
}
public bool HandleAltUseInteraction ( ICommonSession ? session , EntityCoordinates coords , EntityUid uid )
{
// client sanitization
if ( ! ValidateClientInput ( session , coords , uid , out var user ) )
{
2024-03-10 01:15:13 +01:00
Log . Info ( $"Alt-use input validation failed" ) ;
2021-12-16 23:42:02 +13:00
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 ) )
{
2024-03-10 01:15:13 +01:00
Log . Info ( $"Use input validation failed" ) ;
2022-03-09 20:12:17 +13:00
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
}
2023-09-24 12:47:42 -07:00
/// <summary>
/// Returns true if the specified entity should hand interact with the target instead of attacking
/// </summary>
/// <param name="user">The user interacting in combat mode</param>
/// <param name="target">The target of the interaction</param>
/// <returns></returns>
public bool CombatModeCanHandInteract ( EntityUid user , EntityUid ? target )
{
// Always allow attack in these cases
2024-05-24 17:03:03 +12:00
if ( target = = null | | ! _handsQuery . TryComp ( user , out var hands ) | | hands . ActiveHand ? . HeldEntity is not null )
2023-09-24 12:47:42 -07:00
return false ;
// Only eat input if:
// - Target isn't an item
// - Target doesn't cancel should-interact event
// This is intended to allow items to be picked up in combat mode,
// but to also allow items to force attacks anyway (like mobs which are items, e.g. mice)
2024-05-24 17:03:03 +12:00
if ( ! _itemQuery . HasComp ( target ) )
2023-09-24 12:47:42 -07:00
return false ;
var combatEv = new CombatModeShouldHandInteractEvent ( ) ;
RaiseLocalEvent ( target . Value , ref combatEv ) ;
if ( combatEv . Cancelled )
return false ;
return true ;
}
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
{
2024-05-24 17:03:03 +12:00
if ( _relayQuery . TryComp ( user , out var relay ) & & relay . RelayEntity is not null )
2022-12-10 12:05:39 -05:00
{
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 ) )
{
2024-05-31 16:26:19 -04:00
UserInteraction ( relay . RelayEntity . Value ,
coordinates ,
target ,
altInteract ,
checkCanInteract ,
checkAccess ,
checkCanUse ) ;
2023-03-24 14:42:43 +13:00
return ;
}
2022-12-10 12:05:39 -05:00
}
2021-12-16 23:42:02 +13:00
if ( target ! = null & & Deleted ( target . Value ) )
return ;
2024-05-24 17:03:03 +12:00
if ( ! altInteract & & _combatQuery . TryComp ( user , out var combatMode ) & & combatMode . IsInCombatMode )
2021-12-16 23:42:02 +13:00
{
2023-09-24 12:47:42 -07:00
if ( ! CombatModeCanHandInteract ( user , target ) )
return ;
2021-12-16 23:42:02 +13:00
}
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)
2024-05-24 17:03:03 +12:00
if ( checkAccess & & target ! = null & & ! IsAccessible ( 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
// empty-hand interactions
2023-09-24 12:47:42 -07:00
// combat mode hand interactions will always be true here -- since
// they check this earlier before returning in
2024-05-31 16:26:19 -04:00
if ( ! TryGetUsedEntity ( user , out var used , checkCanUse ) )
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
2024-05-31 16:26:19 -04:00
if ( target = = used )
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 ,
2024-05-31 16:26:19 -04:00
used . Value ,
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 ,
2024-05-31 16:26:19 -04:00
used . Value ,
2022-02-15 17:06:52 +13:00
target ,
coordinates ,
inRangeUnobstructed ) ;
2021-12-16 23:42:02 +13:00
}
2024-10-14 17:13:35 +13:00
private bool IsDeleted ( EntityUid uid )
{
return TerminatingOrDeleted ( uid ) | | EntityManager . IsQueuedForDeletion ( uid ) ;
}
private bool IsDeleted ( EntityUid ? uid )
{
//optional / null entities can pass this validation check. I.e., is-deleted returns false for null uids
return uid ! = null & & IsDeleted ( uid . Value ) ;
}
2022-03-09 20:12:17 +13:00
public void InteractHand ( EntityUid user , EntityUid target )
2021-12-16 23:42:02 +13:00
{
2024-10-14 17:13:35 +13:00
if ( IsDeleted ( user ) | | IsDeleted ( target ) )
return ;
2024-08-28 10:57:12 +10:00
var complexInteractions = _actionBlockerSystem . CanComplexInteract ( user ) ;
2024-05-31 16:26:19 -04:00
if ( ! complexInteractions )
{
InteractionActivate ( user ,
target ,
checkCanInteract : false ,
checkUseDelay : true ,
checkAccess : false ,
2024-10-14 17:13:35 +13:00
complexInteractions : complexInteractions ,
checkDeletion : false ) ;
2024-05-31 16:26:19 -04:00
return ;
}
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 ;
}
2024-10-14 17:13:35 +13:00
DebugTools . Assert ( ! IsDeleted ( user ) & & ! IsDeleted ( target ) ) ;
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 ;
2024-10-14 17:13:35 +13:00
DebugTools . Assert ( ! IsDeleted ( user ) & & ! IsDeleted ( target ) ) ;
2022-03-09 20:12:17 +13:00
// Else we run Activate.
2024-05-31 16:26:19 -04:00
InteractionActivate ( user ,
target ,
2022-03-09 20:12:17 +13:00
checkCanInteract : false ,
checkUseDelay : true ,
2024-05-31 16:26:19 -04:00
checkAccess : false ,
2024-10-14 17:13:35 +13:00
complexInteractions : complexInteractions ,
checkDeletion : 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 )
{
2024-10-14 17:13:35 +13:00
if ( IsDeleted ( user ) | | IsDeleted ( used ) | | IsDeleted ( target ) )
return ;
2024-06-27 16:58:42 +02:00
if ( target ! = null )
{
_adminLogger . Add (
LogType . InteractUsing ,
LogImpact . Low ,
$"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target} using {ToPrettyString(used):used}" ) ;
}
else
{
_adminLogger . Add (
LogType . InteractUsing ,
LogImpact . Low ,
$"{ToPrettyString(user):user} interacted with *nothing* using {ToPrettyString(used):used}" ) ;
}
2024-10-14 17:13:35 +13:00
if ( RangedInteractDoBefore ( user , used , target , clickLocation , inRangeUnobstructed , checkDeletion : false ) )
2022-03-09 20:12:17 +13:00
return ;
2024-10-14 17:13:35 +13:00
DebugTools . Assert ( ! IsDeleted ( user ) & & ! IsDeleted ( used ) & & ! IsDeleted ( target ) ) ;
2022-03-09 20:12:17 +13:00
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 ;
}
2024-10-14 17:13:35 +13:00
DebugTools . Assert ( ! IsDeleted ( user ) & & ! IsDeleted ( used ) & & ! IsDeleted ( target ) ) ;
InteractDoAfter ( user , used , target , clickLocation , inRangeUnobstructed , checkDeletion : false ) ;
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
2024-07-18 02:41:15 +02:00
if ( _transform . GetMapId ( coordinates ) ! = Transform ( user ) . MapID )
2021-12-16 23:42:02 +13:00
return false ;
2023-09-22 22:45:13 -07:00
if ( ! HasComp < NoRotateOnInteractComponent > ( user ) )
2024-07-18 02:41:15 +02:00
_rotateToFaceSystem . TryFaceCoordinates ( user , _transform . ToMapCoordinates ( coordinates ) . Position ) ;
2021-12-16 23:42:02 +13:00
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 ) ;
2024-05-24 17:03:03 +12:00
var rayResults = _broadphase . 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 )
{
2024-03-10 01:15:13 +01:00
Log . Warning ( "InRangeUnobstructed check performed over extreme range. Limiting CollisionRay size." ) ;
2022-02-09 07:08:07 +13:00
length = MaxRaycastRange ;
}
2023-07-08 14:08:32 +10:00
var ray = new CollisionRay ( origin . Position , dir . Normalized ( ) , ( int ) collisionMask ) ;
2024-05-24 17:03:03 +12:00
var rayResults = _broadphase . 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 (
2024-05-24 17:03:03 +12:00
Entity < TransformComponent ? > origin ,
Entity < TransformComponent ? > other ,
2022-10-17 15:54:31 +11:00
float range = InteractionRange ,
CollisionGroup collisionMask = InRangeUnobstructedMask ,
Ignored ? predicate = null ,
2025-02-02 23:03:31 +01:00
bool popup = false ,
bool overlapCheck = true )
2022-10-17 15:54:31 +11:00
{
2024-05-24 17:03:03 +12:00
if ( ! Resolve ( other , ref other . Comp ) )
2022-10-17 15:54:31 +11:00
return false ;
2024-08-28 10:57:12 +10:00
var ev = new InRangeOverrideEvent ( origin , other ) ;
RaiseLocalEvent ( origin , ref ev ) ;
if ( ev . Handled )
{
return ev . InRange ;
}
2024-05-24 17:03:03 +12:00
return InRangeUnobstructed ( origin ,
other ,
other . Comp . Coordinates ,
other . Comp . LocalRotation ,
range ,
collisionMask ,
predicate ,
2025-02-02 23:03:31 +01:00
popup ,
overlapCheck ) ;
2022-10-17 15:54:31 +11: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>
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>
2025-02-02 23:03:31 +01:00
/// <param name="overlapCheck">If true, if the broadphase query returns an overlap (0f distance) this function will early out true with no raycast made.</param>
2020-08-30 11:37:06 +02:00
public bool InRangeUnobstructed (
2024-05-24 17:03:03 +12:00
Entity < TransformComponent ? > origin ,
Entity < TransformComponent ? > 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 ,
2025-02-02 23:03:31 +01:00
bool popup = false ,
bool overlapCheck = true )
2022-10-07 00:37:21 +11:00
{
2024-05-24 17:03:03 +12:00
Ignored combinedPredicate = e = > e = = origin . Owner | | ( predicate ? . Invoke ( e ) ? ? false ) ;
2022-10-07 00:37:21 +11:00
var inRange = true ;
MapCoordinates originPos = default ;
2024-05-24 17:03:03 +12:00
var targetPos = _transform . ToMapCoordinates ( otherCoordinates ) ;
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 & &
2024-05-24 17:03:03 +12:00
_fixtureQuery . TryComp ( 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 & &
2024-05-24 17:03:03 +12:00
_fixtureQuery . TryComp ( other , out var fixtureB ) & &
2022-10-07 17:12:46 +11:00
fixtureB . FixtureCount > 0 & &
2024-05-24 17:03:03 +12:00
Resolve ( origin , ref origin . Comp ) )
2022-10-07 00:37:21 +11:00
{
2025-02-10 19:22:41 -08:00
var ( worldPosA , worldRotA ) = _transform . GetWorldPositionRotation ( origin . Comp ) ;
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.
2024-05-24 17:03:03 +12:00
if ( ! _broadphase . TryGetNearest (
origin ,
other ,
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.
2025-02-02 23:03:31 +01:00
else if ( overlapCheck & & distance . Equals ( 0f ) )
2022-10-07 00:37:21 +11:00
{
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.
2024-05-24 17:03:03 +12:00
originPos = _transform . GetMapCoordinates ( origin , xform : origin . Comp ) ;
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
{
2024-05-24 17:03:03 +12:00
originPos = _transform . GetMapCoordinates ( origin , origin ) ;
var otherParent = ( other . Comp ? ? Transform ( other ) ) . ParentUid ;
2022-11-27 19:19:41 +13:00
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" ) ;
2024-04-03 06:19:43 -07:00
_popupSystem . PopupClient ( 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 ) ;
2025-02-10 19:22:41 -08:00
var ( position , rotation ) = _transform . GetWorldPositionRotation ( transform ) ;
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
2024-05-24 17:03:03 +12:00
if ( _itemQuery . HasComp ( target ) & & _physicsQuery . TryComp ( target , out var 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.
2024-05-24 17:03:03 +12:00
ignored . UnionWith ( _broadphase . GetEntitiesIntersectingBody ( target , ( int ) collisionMask , false , physics ) ) ;
2022-02-17 15:40:03 +13:00
}
2024-05-24 17:03:03 +12:00
else if ( _wallMountQuery . TryComp ( target , out var 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
}
2024-05-24 17:03:03 +12:00
Ignored combinedPredicate = e = > e = = target | | ( predicate ? . Invoke ( e ) ? ? false ) | | ignored . Contains ( e ) ;
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 )
{
2024-07-18 02:41:15 +02:00
return InRangeUnobstructed ( origin , _transform . ToMapCoordinates ( other ) , 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 ) ;
2024-05-12 07:31:54 -07:00
var originPosition = _transform . GetMapCoordinates ( origin ) ;
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 ,
2024-10-14 17:13:35 +13:00
bool canReach ,
bool checkDeletion = true )
2021-10-25 20:06:12 +13:00
{
2024-10-14 17:13:35 +13:00
if ( checkDeletion & & ( IsDeleted ( user ) | | IsDeleted ( used ) | | IsDeleted ( target ) ) )
return false ;
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
2024-10-14 17:13:35 +13:00
if ( ! ev . Handled )
return false ;
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>
2024-08-10 19:29:44 -07:00
/// Uses an item/object on an entity
2021-10-25 20:06:12 +13:00
/// Finds components with the InteractUsing interface and calls their function
/// NOTE: Does not have an InRangeUnobstructed check
/// </summary>
2024-08-10 19:29:44 -07:00
/// <param name="user">User doing the interaction.</param>
/// <param name="used">Item being used on the <paramref name="target"/>.</param>
/// <param name="target">Entity getting interacted with by the <paramref name="user"/> using the
/// <paramref name="used"/> entity.</param>
/// <param name="clickLocation">The location that the <paramref name="user"/> clicked.</param>
/// <param name="checkCanInteract">Whether to check that the <paramref name="user"/> can interact with the
/// <paramref name="target"/>.</param>
/// <param name="checkCanUse">Whether to check that the <paramref name="user"/> can use the
/// <paramref name="used"/> entity.</param>
/// <returns>True if the interaction was handled. Otherwise, false.</returns>
public bool 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
{
2024-10-14 17:13:35 +13:00
if ( IsDeleted ( user ) | | IsDeleted ( used ) | | IsDeleted ( target ) )
return false ;
2022-02-15 17:06:52 +13:00
if ( checkCanInteract & & ! _actionBlockerSystem . CanInteract ( user , target ) )
2024-08-10 19:29:44 -07:00
return false ;
2021-10-25 20:06:12 +13:00
2024-05-31 16:26:19 -04:00
if ( checkCanUse & & ! _actionBlockerSystem . CanUseHeldEntity ( user , used ) )
2024-08-10 19:29:44 -07:00
return false ;
2022-02-15 17:06:52 +13:00
2024-06-27 16:58:42 +02:00
_adminLogger . Add (
LogType . InteractUsing ,
LogImpact . Low ,
$"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target} using {ToPrettyString(used):used}" ) ;
2024-10-14 17:13:35 +13:00
if ( RangedInteractDoBefore ( user , used , target , clickLocation , canReach : true , checkDeletion : false ) )
2024-08-10 19:29:44 -07:00
return true ;
2021-10-25 20:06:12 +13:00
2024-10-14 17:13:35 +13:00
DebugTools . Assert ( ! IsDeleted ( user ) & & ! IsDeleted ( used ) & & ! IsDeleted ( target ) ) ;
2021-10-25 20:06:12 +13:00
// 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 ) ;
2024-07-30 04:35:30 -04:00
// Contact interactions are currently only used for forensics, so we don't raise used -> target
2021-10-25 20:06:12 +13:00
if ( interactUsingEvent . Handled )
2024-08-10 19:29:44 -07:00
return true ;
2021-10-25 20:06:12 +13:00
2024-10-14 17:13:35 +13:00
if ( InteractDoAfter ( user , used , target , clickLocation , canReach : true , checkDeletion : false ) )
2024-08-10 19:29:44 -07:00
return true ;
2024-10-14 17:13:35 +13:00
DebugTools . Assert ( ! IsDeleted ( user ) & & ! IsDeleted ( used ) & & ! IsDeleted ( target ) ) ;
2024-08-10 19:29:44 -07:00
return false ;
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>
2024-08-10 19:29:44 -07:00
/// <param name="user"><inheritdoc cref="InteractUsing"/></param>
/// <param name="used"><inheritdoc cref="InteractUsing"/></param>
/// <param name="target"><inheritdoc cref="InteractUsing"/></param>
/// <param name="clickLocation"><inheritdoc cref="InteractUsing"/></param>
/// <param name="canReach">Whether the <paramref name="user"/> is in range of the <paramref name="target"/>.
/// </param>
/// <returns>True if the interaction was handled. Otherwise, false.</returns>
2024-10-14 17:13:35 +13:00
public bool InteractDoAfter ( EntityUid user , EntityUid used , EntityUid ? target , EntityCoordinates clickLocation , bool canReach , bool checkDeletion = true )
2021-10-25 20:06:12 +13:00
{
2024-07-18 02:41:15 +02:00
if ( target is { Valid : false } )
2021-12-11 15:36:50 +01:00
target = null ;
2024-10-14 17:13:35 +13:00
if ( checkDeletion & & ( IsDeleted ( user ) | | IsDeleted ( used ) | | IsDeleted ( target ) ) )
return false ;
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 ) ;
2024-07-30 04:35:30 -04:00
// Contact interactions are currently only used for forensics, so we don't raise used -> target
2022-10-26 14:15:48 +13:00
}
2021-10-25 20:06:12 +13:00
if ( afterInteractEvent . Handled )
2024-08-10 19:29:44 -07:00
return true ;
2021-10-25 20:06:12 +13:00
2022-02-05 15:39:01 +13:00
if ( target = = null )
2024-08-10 19:29:44 -07:00
return false ;
2022-02-05 15:39:01 +13:00
2024-10-14 17:13:35 +13:00
DebugTools . Assert ( ! IsDeleted ( user ) & & ! IsDeleted ( used ) & & ! IsDeleted ( target ) ) ;
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 ) ;
2024-07-30 04:35:30 -04:00
// Contact interactions are currently only used for forensics, so we don't raise used -> target
2022-10-26 14:15:48 +13:00
}
2024-08-10 19:29:44 -07:00
2024-10-14 17:13:35 +13:00
return afterInteractUsingEvent . Handled ;
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 ) )
{
2024-03-10 01:15:13 +01:00
Log . Info ( $"ActivateItemInWorld input validation failed" ) ;
2022-03-09 20:12:17 +13:00
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 ,
2024-05-31 16:26:19 -04:00
bool checkAccess = true ,
2024-10-14 17:13:35 +13:00
bool? complexInteractions = null ,
bool checkDeletion = true )
2021-10-25 20:06:12 +13:00
{
2024-10-14 17:13:35 +13:00
if ( checkDeletion & & ( IsDeleted ( user ) | | IsDeleted ( used ) ) )
return false ;
DebugTools . Assert ( ! IsDeleted ( user ) & & ! IsDeleted ( used ) ) ;
2024-05-24 17:03:03 +12:00
_delayQuery . TryComp ( used , out var delayComponent ) ;
if ( checkUseDelay & & delayComponent ! = null & & _useDelay . IsDelayed ( ( used , delayComponent ) ) )
2022-02-15 17:06:52 +13:00
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)
2024-05-24 17:03:03 +12:00
if ( checkAccess & & ! IsAccessible ( user , used ) )
2022-02-15 17:06:52 +13:00
return false ;
2021-11-29 12:25:22 +13:00
2024-10-14 17:13:35 +13:00
complexInteractions ? ? = _actionBlockerSystem . CanComplexInteract ( user ) ;
2024-05-31 16:26:19 -04:00
var activateMsg = new ActivateInWorldEvent ( user , used , complexInteractions . Value ) ;
2022-06-22 09:53:41 +10:00
RaiseLocalEvent ( used , activateMsg , true ) ;
2024-10-14 17:13:35 +13:00
if ( activateMsg . Handled )
{
DoContactInteraction ( user , used ) ;
if ( ! activateMsg . WasLogged )
_adminLogger . Add ( LogType . InteractActivate , LogImpact . Low , $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}" ) ;
if ( delayComponent ! = null )
_useDelay . TryResetDelay ( used , component : delayComponent ) ;
return true ;
}
DebugTools . Assert ( ! IsDeleted ( user ) & & ! IsDeleted ( used ) ) ;
2024-05-31 16:26:19 -04:00
var userEv = new UserActivateInWorldEvent ( user , used , complexInteractions . Value ) ;
RaiseLocalEvent ( user , userEv , true ) ;
2024-10-14 17:13:35 +13:00
if ( ! userEv . Handled )
2022-02-15 17:06:52 +13:00
return false ;
2021-10-25 20:06:12 +13:00
2024-10-14 17:13:35 +13:00
DoContactInteraction ( user , used ) ;
2024-04-26 17:58:06 +10:00
// Still need to call this even without checkUseDelay in case this gets relayed from Activate.
2024-05-24 17:03:03 +12:00
if ( delayComponent ! = null )
_useDelay . TryResetDelay ( used , component : delayComponent ) ;
2024-10-14 17:13:35 +13:00
_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
{
2024-10-14 17:13:35 +13:00
if ( IsDeleted ( user ) | | IsDeleted ( used ) )
return false ;
2024-05-24 17:03:03 +12:00
_delayQuery . TryComp ( used , out var delayComponent ) ;
if ( checkUseDelay & & delayComponent ! = null & & _useDelay . IsDelayed ( ( used , delayComponent ) ) )
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 ;
2024-05-31 16:26:19 -04:00
if ( checkCanUse & & ! _actionBlockerSystem . CanUseHeldEntity ( user , used ) )
2022-02-15 17:06:52 +13:00
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 ) ;
2024-01-03 21:33:09 -04:00
if ( delayComponent ! = null & & useMsg . ApplyDelay )
_useDelay . TryResetDelay ( ( 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
2024-10-14 17:13:35 +13:00
DebugTools . Assert ( ! IsDeleted ( user ) & & ! IsDeleted ( used ) ) ;
2022-02-15 17:06:52 +13:00
// else, default to activating the item
2024-10-14 17:13:35 +13:00
return InteractionActivate ( user , used , false , false , false , checkDeletion : 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
2024-08-28 10:57:12 +10:00
if ( verbs . Count = = 0 )
2022-01-05 02:23:01 +13:00
return false ;
_verbSystem . ExecuteVerb ( verbs . First ( ) , user , target ) ;
return true ;
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
{
2024-10-14 17:13:35 +13:00
if ( IsDeleted ( user ) | | IsDeleted ( item ) )
return ;
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 ) ;
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 ;
}
2024-02-28 00:51:20 +11:00
Transform ( item ) . LocalRotation = rotation ;
2021-10-25 20:06:12 +13:00
}
#endregion
2021-11-29 12:25:22 +13:00
2024-05-24 17:03:03 +12:00
/// <summary>
/// Check if a user can access a target (stored in the same containers) and is in range without obstructions.
/// </summary>
public bool InRangeAndAccessible (
Entity < TransformComponent ? > user ,
Entity < TransformComponent ? > target ,
float range = InteractionRange ,
CollisionGroup collisionMask = InRangeUnobstructedMask ,
Ignored ? predicate = null )
{
if ( user = = target )
return true ;
if ( ! Resolve ( user , ref user . Comp ) )
return false ;
if ( ! Resolve ( target , ref target . Comp ) )
return false ;
return IsAccessible ( user , target ) & & InRangeUnobstructed ( user , target , range , collisionMask , predicate ) ;
}
/// <summary>
/// Check if a user can access a target or if they are stored in different containers.
/// </summary>
public bool IsAccessible ( Entity < TransformComponent ? > user , Entity < TransformComponent ? > target )
{
2024-08-28 10:57:12 +10:00
var ev = new AccessibleOverrideEvent ( user , target ) ;
RaiseLocalEvent ( user , ref ev ) ;
if ( ev . Handled )
return ev . Accessible ;
2024-05-24 17:03:03 +12:00
if ( _containerSystem . IsInSameOrParentContainer ( user , target , out _ , out var container ) )
return true ;
return container ! = null & & CanAccessViaStorage ( user , target , container ) ;
}
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>
2024-05-24 17:03:03 +12:00
public bool CanAccessViaStorage ( EntityUid user , EntityUid target )
{
2024-08-04 07:38:53 +02:00
if ( ! _containerSystem . TryGetContainingContainer ( ( target , null , null ) , out var container ) )
2024-05-24 17:03:03 +12:00
return false ;
return CanAccessViaStorage ( user , target , container ) ;
}
/// <inheritdoc cref="CanAccessViaStorage(Robust.Shared.GameObjects.EntityUid,Robust.Shared.GameObjects.EntityUid)"/>
public bool CanAccessViaStorage ( EntityUid user , EntityUid target , BaseContainer container )
{
if ( StorageComponent . ContainerId ! = container . ID )
return false ;
// we don't check if the user can access the storage entity itself. This should be handed by the UI system.
2024-05-26 14:11:37 +12:00
return _ui . IsUiOpen ( container . Owner , StorageComponent . StorageUiKey . Key , user ) ;
2024-05-24 17:03:03 +12:00
}
2021-11-29 12:25:22 +13:00
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 ;
2024-08-04 07:38:53 +02:00
if ( ! _containerSystem . TryGetContainingContainer ( ( target , null , null ) , out var container ) )
2023-03-06 06:12:08 +13:00
return false ;
var wearer = container . Owner ;
if ( ! _inventory . TryGetSlot ( wearer , container . ID , out var slotDef ) )
return false ;
if ( wearer = = user )
return true ;
2024-11-21 18:56:05 -08:00
if ( _strippable . IsStripHidden ( slotDef , user ) )
2023-03-06 06:12:08 +13:00
return false ;
return InRangeUnobstructed ( user , wearer ) & & _containerSystem . IsInSameOrParentContainer ( user , wearer ) ;
}
2024-09-30 01:19:00 +13:00
protected bool ValidateClientInput (
ICommonSession ? session ,
EntityCoordinates coords ,
EntityUid uid ,
[NotNullWhen(true)] out EntityUid ? userEntity )
2021-12-16 23:42:02 +13:00
{
userEntity = null ;
if ( ! coords . IsValid ( EntityManager ) )
{
2024-03-10 01:15:13 +01:00
Log . Info ( $"Invalid Coordinates: client={session}, coords={coords}" ) ;
2021-12-16 23:42:02 +13:00
return false ;
}
2023-09-11 09:42:41 +10:00
if ( IsClientSide ( uid ) )
2021-12-16 23:42:02 +13:00
{
2024-03-10 01:15:13 +01:00
Log . Warning ( $"Client sent interaction with client-side entity. Session={session}, Uid={uid}" ) ;
2021-12-16 23:42:02 +13:00
return false ;
}
userEntity = session ? . AttachedEntity ;
if ( userEntity = = null | | ! userEntity . Value . Valid )
{
2024-03-10 01:15:13 +01:00
Log . Warning ( $"Client sent interaction with no attached entity. Session={session}" ) ;
2021-12-16 23:42:02 +13:00
return false ;
}
2022-08-26 01:33:40 +12:00
if ( ! Exists ( userEntity ) )
{
2024-03-10 01:15:13 +01:00
Log . Warning ( $"Client attempted interaction with a non-existent attached entity. Session={session}, entity={userEntity}" ) ;
2022-08-26 01:33:40 +12:00
return false ;
}
2024-09-30 01:19:00 +13:00
return _rateLimit . CountAction ( session ! , RateLimitKey ) = = RateLimitStatus . Allowed ;
2021-12-16 23:42:02 +13:00
}
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 ;
2024-10-18 18:40:36 +13:00
if ( uidA = = uidB . Value )
return ;
2022-10-31 19:35:35 +13:00
2024-10-14 17:13:35 +13:00
if ( ! TryComp ( uidA , out MetaDataComponent ? metaA ) | | metaA . EntityPaused )
2023-08-05 00:29:52 -04:00
return ;
2024-10-14 17:13:35 +13:00
if ( ! TryComp ( uidB , out MetaDataComponent ? metaB ) | | metaB . EntityPaused )
return ;
// TODO Struct event
var ev = new ContactInteractionEvent ( uidB . Value ) ;
RaiseLocalEvent ( uidA , ev ) ;
ev . Other = uidA ;
RaiseLocalEvent ( uidB . Value , ev ) ;
2022-10-26 14:15:48 +13:00
}
2024-04-26 18:16:24 +10:00
2024-05-24 17:03:03 +12:00
2024-04-26 18:16:24 +10:00
private void HandleUserInterfaceRangeCheck ( ref BoundUserInterfaceCheckRangeEvent ev )
{
if ( ev . Result = = BoundUserInterfaceRangeResult . Fail )
return ;
2024-05-24 17:03:03 +12:00
ev . Result = UiRangeCheck ( ev . Actor ! , ev . Target , ev . Data . InteractionRange )
? BoundUserInterfaceRangeResult . Pass
: BoundUserInterfaceRangeResult . Fail ;
2024-04-26 18:16:24 +10:00
}
2024-05-31 16:26:19 -04:00
/// <summary>
/// Gets the entity that is currently being "used" for the interaction.
/// In most cases, this refers to the entity in the character's active hand.
/// </summary>
/// <returns>If there is an entity being used.</returns>
public bool TryGetUsedEntity ( EntityUid user , [ NotNullWhen ( true ) ] out EntityUid ? used , bool checkCanUse = true )
{
2025-02-10 22:29:37 -05:00
var ev = new GetUsedEntityEvent ( user ) ;
2024-05-31 16:26:19 -04:00
RaiseLocalEvent ( user , ref ev ) ;
used = ev . Used ;
if ( ! ev . Handled )
return false ;
// Can the user use the held entity?
if ( checkCanUse & & ! _actionBlockerSystem . CanUseHeldEntity ( user , ev . Used ! . Value ) )
{
used = null ;
return false ;
}
return ev . Handled ;
}
2024-08-28 10:57:12 +10:00
[Obsolete("Use ActionBlockerSystem")]
2024-05-31 16:26:19 -04:00
public bool SupportsComplexInteractions ( EntityUid user )
{
2024-08-28 10:57:12 +10:00
return _actionBlockerSystem . CanComplexInteract ( user ) ;
2024-05-31 16:26:19 -04:00
}
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>
2023-09-11 09:42:41 +10:00
public NetEntity ItemUid { get ; }
2021-08-22 03:20:18 +10:00
/// <summary>
/// Whether the interaction used the alt-modifier to trigger alternative interactions.
/// </summary>
public bool AltInteract { get ; }
2023-09-11 09:42:41 +10:00
public InteractInventorySlotEvent ( NetEntity itemUid , bool altInteract = false )
2021-08-22 03:20:18 +10:00
{
ItemUid = itemUid ;
AltInteract = altInteract ;
}
}
2023-09-24 12:47:42 -07:00
2024-05-31 16:26:19 -04:00
/// <summary>
/// Raised directed by-ref on an entity to determine what item will be used in interactions.
/// </summary>
[ByRefEvent]
2025-02-10 22:29:37 -05:00
public record struct GetUsedEntityEvent ( EntityUid User )
2024-05-31 16:26:19 -04:00
{
2025-02-10 22:29:37 -05:00
public EntityUid User = User ;
2024-05-31 16:26:19 -04:00
public EntityUid ? Used = null ;
public bool Handled = > Used ! = null ;
} ;
/// <summary>
2024-08-28 10:57:12 +10:00
/// Raised directed by-ref on an item to determine if hand interactions should go through.
/// Defaults to allowing hand interactions to go through. Cancel to force the item to be attacked instead.
2024-05-31 16:26:19 -04:00
/// </summary>
/// <param name="Cancelled">Whether the hand interaction should be cancelled.</param>
[ByRefEvent]
2024-08-28 10:57:12 +10:00
public record struct CombatModeShouldHandInteractEvent ( bool Cancelled = false ) ;
2024-05-31 16:26:19 -04:00
2023-09-24 12:47:42 -07:00
/// <summary>
2024-08-28 10:57:12 +10:00
/// Override event raised directed on the user to say the target is accessible.
2023-09-24 12:47:42 -07:00
/// </summary>
2024-08-28 10:57:12 +10:00
/// <param name="User"></param>
/// <param name="Target"></param>
2023-09-24 12:47:42 -07:00
[ByRefEvent]
2024-08-28 10:57:12 +10:00
public record struct AccessibleOverrideEvent ( EntityUid User , EntityUid Target )
{
public readonly EntityUid User = User ;
public readonly EntityUid Target = Target ;
public bool Handled ;
public bool Accessible = false ;
}
/// <summary>
/// Override event raised directed on a user to check InRangeUnoccluded AND InRangeUnobstructed to the target if you require custom logic.
/// </summary>
[ByRefEvent]
public record struct InRangeOverrideEvent ( EntityUid User , EntityUid Target )
{
public readonly EntityUid User = User ;
public readonly EntityUid Target = Target ;
public bool Handled ;
public bool InRange = false ;
}
2020-04-22 00:58:31 +10:00
}