using System; using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.CombatMode; using Content.Shared.Database; using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Input; using Content.Shared.Interaction.Helpers; using Content.Shared.Inventory; using Content.Shared.Physics; using Content.Shared.Popups; using Content.Shared.Throwing; using Content.Shared.Timing; using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Input.Binding; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Players; using Robust.Shared.Serialization; #pragma warning disable 618 namespace Content.Shared.Interaction { /// /// Governs interactions during clicking on entities /// [UsedImplicitly] public abstract class SharedInteractionSystem : EntitySystem { [Dependency] private readonly SharedPhysicsSystem _sharedBroadphaseSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly SharedVerbSystem _verbSystem = default!; [Dependency] private readonly SharedAdminLogSystem _adminLogSystem = default!; [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!; public const float InteractionRange = 2; public const float InteractionRangeSquared = InteractionRange * InteractionRange; public delegate bool Ignored(EntityUid entity); public override void Initialize() { SubscribeAllEvent(HandleInteractInventorySlotEvent); CommandBinds.Builder .Bind(ContentKeyFunctions.AltActivateItemInWorld, new PointerInputCmdHandler(HandleAltUseInteraction)) .Register(); } public override void Shutdown() { CommandBinds.Unregister(); base.Shutdown(); } /// /// 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. /// private void HandleInteractInventorySlotEvent(InteractInventorySlotEvent msg, EntitySessionEventArgs args) { var coords = Transform(msg.ItemUid).Coordinates; // client sanitization if (!ValidateClientInput(args.SenderSession, coords, msg.ItemUid, out var user)) { Logger.InfoS("system.interaction", $"Inventory interaction validation failed. Session={args.SenderSession}"); return; } if (msg.AltInteract) // Use 'UserInteraction' function - behaves as if the user alt-clicked the item in the world. UserInteraction(user.Value, coords, msg.ItemUid, msg.AltInteract); else // User used 'E'. We want to activate it, not simulate clicking on the item InteractionActivate(user.Value, msg.ItemUid); } public bool HandleAltUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { // client sanitization if (!ValidateClientInput(session, coords, uid, out var user)) { Logger.InfoS("system.interaction", $"Alt-use input validation failed"); return true; } UserInteraction(user.Value, coords, uid, altInteract: true); return false; } /// /// Resolves user interactions with objects. /// /// /// Checks Whether combat mode is enabled and whether the user can actually interact with the given entity. /// /// 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. public async void UserInteraction(EntityUid user, EntityCoordinates coordinates, EntityUid? target, bool altInteract = false) { if (target != null && Deleted(target.Value)) return; // TODO COMBAT Consider using alt-interact for advanced combat? maybe alt-interact disarms? if (!altInteract && TryComp(user, out SharedCombatModeComponent? combatMode) && combatMode.IsInCombatMode) { DoAttack(user, coordinates, false, target); return; } if (!ValidateInteractAndFace(user, coordinates)) return; if (!_actionBlockerSystem.CanInteract(user)) return; // Check if interacted entity is in the same container, the direct child, or direct parent of the user. // This is bypassed IF the interaction happened through an item slot (e.g., backpack UI) if (target != null && !user.IsInSameOrParentContainer(target.Value) && !CanAccessViaStorage(user, target.Value)) return; // Verify user has a hand, and find what object they are currently holding in their active hand if (!TryComp(user, out SharedHandsComponent? hands)) return; // TODO remove invalid/default uid and use nullable. hands.TryGetActiveHeldEntity(out var heldEntity); EntityUid? held = heldEntity.Valid ? heldEntity : null; // TODO: Replace with body interaction range when we get something like arm length or telekinesis or something. var inRangeUnobstructed = user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true); if (target == null || !inRangeUnobstructed) { if (held == null) return; if (!await InteractUsingRanged(user, held.Value, target, coordinates, inRangeUnobstructed) && !inRangeUnobstructed) { var message = Loc.GetString("interaction-system-user-interaction-cannot-reach"); user.PopupMessage(message); } return; } else { // We are close to the nearby object. if (altInteract) // Perform alternative interactions, using context menu verbs. AltInteract(user, target.Value); else if (held != null && held != target) // We are performing a standard interaction with an item, and the target isn't the same as the item // currently in our hand. We will use the item in our hand on the nearby object via InteractUsing await InteractUsing(user, held.Value, target.Value, coordinates); else if (held == null) // Since our hand is empty we will use InteractHand/Activate InteractHand(user, target.Value); } } public virtual void InteractHand(EntityUid user, EntityUid target) { // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. } public virtual void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, EntityUid? targetUid = null) { // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. } public virtual async Task InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool inRangeUnobstructed) { // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. return await Task.FromResult(true); } protected bool ValidateInteractAndFace(EntityUid user, EntityCoordinates coordinates) { // Verify user is on the same map as the entity they clicked on if (coordinates.GetMapId(EntityManager) != Transform(user).MapID) return false; _rotateToFaceSystem.TryFaceCoordinates(user, coordinates.ToMapPos(EntityManager)); return true; } /// /// Traces a ray from coords to otherCoords and returns the length /// of the vector between coords and the ray's first hit. /// /// Set of coordinates to use. /// Other set of coordinates to use. /// the mask to check for collisions /// /// A predicate to check whether to ignore an entity or not. /// If it returns true, it will be ignored. /// /// Length of resulting ray. public float UnobstructedDistance( MapCoordinates origin, MapCoordinates other, int collisionMask = (int) CollisionGroup.Impassable, Ignored? predicate = null) { var dir = other.Position - origin.Position; if (dir.LengthSquared.Equals(0f)) return 0f; predicate ??= _ => false; var ray = new CollisionRay(origin.Position, dir.Normalized, collisionMask); var rayResults = _sharedBroadphaseSystem.IntersectRayWithPredicate(origin.MapId, ray, dir.Length, predicate.Invoke, false).ToList(); if (rayResults.Count == 0) return dir.Length; return (rayResults[0].HitPos - origin.Position).Length; } /// /// Traces a ray from coords to otherCoords and returns the length /// of the vector between coords and the ray's first hit. /// /// Set of coordinates to use. /// Other set of coordinates to use. /// The mask to check for collisions /// /// The entity to be ignored when checking for collisions. /// /// Length of resulting ray. public float UnobstructedDistance( MapCoordinates origin, MapCoordinates other, int collisionMask = (int) CollisionGroup.Impassable, EntityUid? ignoredEnt = null) { var predicate = ignoredEnt == null ? null : (Ignored) (e => e == ignoredEnt); return UnobstructedDistance(origin, other, collisionMask, predicate); } /// /// Checks that these coordinates are within a certain distance without any /// entity that matches the collision mask obstructing them. /// If the is zero or negative, /// this method will only check if nothing obstructs the two sets /// of coordinates. /// /// Set of coordinates to use. /// Other set of coordinates to use. /// /// Maximum distance between the two sets of coordinates. /// /// The mask to check for collisions. /// /// A predicate to check whether to ignore an entity or not. /// If it returns true, it will be ignored. /// /// /// If true and or are inside /// the obstruction, ignores the obstruction and considers the interaction /// unobstructed. /// Therefore, setting this to true makes this check more permissive, /// such as allowing an interaction to occur inside something impassable /// (like a wall). The default, false, makes the check more restrictive. /// /// /// True if the two points are within a given range without being obstructed. /// public bool InRangeUnobstructed( MapCoordinates origin, MapCoordinates other, float range = InteractionRange, CollisionGroup collisionMask = CollisionGroup.Impassable, Ignored? predicate = null, bool ignoreInsideBlocker = false) { if (!origin.InRange(other, range)) return false; var dir = other.Position - origin.Position; if (dir.LengthSquared.Equals(0f)) return true; if (range > 0f && !(dir.LengthSquared <= range * range)) return false; predicate ??= _ => false; var ray = new CollisionRay(origin.Position, dir.Normalized, (int) collisionMask); var rayResults = _sharedBroadphaseSystem.IntersectRayWithPredicate(origin.MapId, ray, dir.Length, predicate.Invoke, false).ToList(); if (rayResults.Count == 0) return true; // TODO: Wot? This should just be in the predicate. if (!ignoreInsideBlocker) return false; foreach (var result in rayResults) { if (!TryComp(result.HitEntity, out IPhysBody? p)) { continue; } var bBox = p.GetWorldAABB(); if (bBox.Contains(origin.Position) || bBox.Contains(other.Position)) { continue; } return false; } return true; } /// /// Checks that two entities are within a certain distance without any /// entity that matches the collision mask obstructing them. /// If the is zero or negative, /// this method will only check if nothing obstructs the two entities. /// /// The first entity to use. /// Other entity to use. /// /// Maximum distance between the two entities. /// /// The mask to check for collisions. /// /// A predicate to check whether to ignore an entity or not. /// If it returns true, it will be ignored. /// /// /// If true and or are inside /// the obstruction, ignores the obstruction and considers the interaction /// unobstructed. /// Therefore, setting this to true makes this check more permissive, /// such as allowing an interaction to occur inside something impassable /// (like a wall). The default, false, makes the check more restrictive. /// /// /// Whether or not to popup a feedback message on the origin entity for /// it to see. /// /// /// True if the two points are within a given range without being obstructed. /// public bool InRangeUnobstructed( EntityUid origin, EntityUid other, float range = InteractionRange, CollisionGroup collisionMask = CollisionGroup.Impassable, Ignored? predicate = null, bool ignoreInsideBlocker = false, bool popup = false) { predicate ??= e => e == origin || e == other; return InRangeUnobstructed(origin, Transform(other).MapPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup); } /// /// Checks that an entity and a component are within a certain /// distance without any entity that matches the collision mask /// obstructing them. /// If the is zero or negative, /// this method will only check if nothing obstructs the entity and component. /// /// The entity to use. /// The component to use. /// /// Maximum distance between the entity and component. /// /// The mask to check for collisions. /// /// A predicate to check whether to ignore an entity or not. /// If it returns true, it will be ignored. /// /// /// If true and or are inside /// the obstruction, ignores the obstruction and considers the interaction /// unobstructed. /// Therefore, setting this to true makes this check more permissive, /// such as allowing an interaction to occur inside something impassable /// (like a wall). The default, false, makes the check more restrictive. /// /// /// Whether or not to popup a feedback message on the origin entity for /// it to see. /// /// /// True if the two points are within a given range without being obstructed. /// public bool InRangeUnobstructed( EntityUid origin, IComponent other, float range = InteractionRange, CollisionGroup collisionMask = CollisionGroup.Impassable, Ignored? predicate = null, bool ignoreInsideBlocker = false, bool popup = false) { return InRangeUnobstructed(origin, other.Owner, range, collisionMask, predicate, ignoreInsideBlocker, popup); } /// /// 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 is zero or negative, /// this method will only check if nothing obstructs the entity and component. /// /// The entity to use. /// The grid coordinates to use. /// /// Maximum distance between the two entity and set of grid coordinates. /// /// The mask to check for collisions. /// /// A predicate to check whether to ignore an entity or not. /// If it returns true, it will be ignored. /// /// /// If true and or are inside /// the obstruction, ignores the obstruction and considers the interaction /// unobstructed. /// Therefore, setting this to true makes this check more permissive, /// such as allowing an interaction to occur inside something impassable /// (like a wall). The default, false, makes the check more restrictive. /// /// /// Whether or not to popup a feedback message on the origin entity for /// it to see. /// /// /// True if the two points are within a given range without being obstructed. /// public bool InRangeUnobstructed( EntityUid origin, EntityCoordinates other, float range = InteractionRange, CollisionGroup collisionMask = CollisionGroup.Impassable, Ignored? predicate = null, bool ignoreInsideBlocker = false, bool popup = false) { return InRangeUnobstructed(origin, other.ToMap(EntityManager), range, collisionMask, predicate, ignoreInsideBlocker, popup); } /// /// 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 is zero or negative, /// this method will only check if nothing obstructs the entity and component. /// /// The entity to use. /// The map coordinates to use. /// /// Maximum distance between the two entity and set of map coordinates. /// /// The mask to check for collisions. /// /// A predicate to check whether to ignore an entity or not. /// If it returns true, it will be ignored. /// /// /// If true and or are inside /// the obstruction, ignores the obstruction and considers the interaction /// unobstructed. /// Therefore, setting this to true makes this check more permissive, /// such as allowing an interaction to occur inside something impassable /// (like a wall). The default, false, makes the check more restrictive. /// /// /// Whether or not to popup a feedback message on the origin entity for /// it to see. /// /// /// True if the two points are within a given range without being obstructed. /// public bool InRangeUnobstructed( EntityUid origin, MapCoordinates other, float range = InteractionRange, CollisionGroup collisionMask = CollisionGroup.Impassable, Ignored? predicate = null, bool ignoreInsideBlocker = false, bool popup = false) { var originPosition = Transform(origin).MapPosition; predicate ??= e => e == origin; var inRange = InRangeUnobstructed(originPosition, other, range, collisionMask, predicate, ignoreInsideBlocker); if (!inRange && popup) { var message = Loc.GetString("shared-interaction-system-in-range-unobstructed-cannot-reach"); origin.PopupMessage(message); } return inRange; } public bool InteractDoBefore( EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool canReach) { var ev = new BeforeInteractEvent(user, used, target, clickLocation, canReach); RaiseLocalEvent(used, ev, false); return ev.Handled; } /// /// Uses a item/object on an entity /// Finds components with the InteractUsing interface and calls their function /// NOTE: Does not have an InRangeUnobstructed check /// public async Task InteractUsing(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation) { if (!_actionBlockerSystem.CanInteract(user)) return; if (InteractDoBefore(user, used, target, clickLocation, true)) return; // all interactions should only happen when in range / unobstructed, so no range check is needed var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation); RaiseLocalEvent(target, interactUsingEvent); if (interactUsingEvent.Handled) return; var interactUsingEventArgs = new InteractUsingEventArgs(user, clickLocation, used, target); var interactUsings = AllComps(target).OrderByDescending(x => x.Priority); foreach (var interactUsing in interactUsings) { // If an InteractUsing returns a status completion we finish our interaction if (await interactUsing.InteractUsing(interactUsingEventArgs)) return; } // If we aren't directly interacting with the nearby object, lets see if our item has an after interact we can do await InteractDoAfter(user, used, target, clickLocation, true); } /// /// We didn't click on any entity, try doing an AfterInteract on the click location /// public async Task InteractDoAfter(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool canReach) { if (target is {Valid: false}) target = null; var afterInteractEvent = new AfterInteractEvent(user, used, target, clickLocation, canReach); RaiseLocalEvent(used, afterInteractEvent, false); if (afterInteractEvent.Handled) return true; var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, target, canReach); var afterInteracts = AllComps(used).OrderByDescending(x => x.Priority).ToList(); foreach (var afterInteract in afterInteracts) { if (await afterInteract.AfterInteract(afterInteractEventArgs)) return true; } return false; } #region ActivateItemInWorld /// /// Activates the IActivate behavior of an object /// Verifies that the user is capable of doing the use interaction first /// public void TryInteractionActivate(EntityUid? user, EntityUid? used) { if (user == null || used == null) return; InteractionActivate(user.Value, used.Value); } protected void InteractionActivate(EntityUid user, EntityUid used) { if (TryComp(used, out UseDelayComponent? delayComponent)) { if (delayComponent.ActiveDelay) return; delayComponent.BeginDelay(); } if (!_actionBlockerSystem.CanInteract(user) || !_actionBlockerSystem.CanUse(user)) return; // all activates should only fire when in range / unobstructed if (!InRangeUnobstructed(user, used, ignoreInsideBlocker: true, popup: true)) return; // Check if interacted entity is in the same container, the direct child, or direct parent of the user. // This is bypassed IF the interaction happened through an item slot (e.g., backpack UI) if (!user.IsInSameOrParentContainer(used) && !CanAccessViaStorage(user, used)) return; var activateMsg = new ActivateInWorldEvent(user, used); RaiseLocalEvent(used, activateMsg); if (activateMsg.Handled) { _adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}"); return; } if (!TryComp(used, out IActivate? activateComp)) return; var activateEventArgs = new ActivateEventArgs(user, used); activateComp.Activate(activateEventArgs); _adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}"); // No way to check success. } #endregion #region Hands #region Use /// /// Activates the IUse behaviors of an entity /// Verifies that the user is capable of doing the use interaction first /// /// /// public void TryUseInteraction(EntityUid user, EntityUid used, bool altInteract = false) { if (_actionBlockerSystem.CanUse(user)) { if (altInteract) AltInteract(user, used); else UseInteraction(user, used); } } /// /// Activates the IUse behaviors of an entity without first checking /// if the user is capable of doing the use interaction. /// public void UseInteraction(EntityUid user, EntityUid used) { if (TryComp(used, out UseDelayComponent? delayComponent)) { if (delayComponent.ActiveDelay) return; delayComponent.BeginDelay(); } var useMsg = new UseInHandEvent(user, used); RaiseLocalEvent(used, useMsg); if (useMsg.Handled) return; var uses = AllComps(used).ToList(); // Try to use item on any components which have the interface foreach (var use in uses) { // If a Use returns a status completion we finish our interaction if (use.UseEntity(new UseEntityEventArgs(user))) return; } } /// /// Alternative interactions on an entity. /// /// /// Uses the context menu verb list, and acts out the highest priority alternative interaction verb. /// public void AltInteract(EntityUid user, EntityUid target) { // Get list of alt-interact verbs var verbs = _verbSystem.GetLocalVerbs(target, user, VerbType.Alternative)[VerbType.Alternative]; if (verbs.Any()) _verbSystem.ExecuteVerb(verbs.First(), user, target); } #endregion #region Throw /// /// Calls Thrown on all components that implement the IThrown interface /// on an entity that has been thrown. /// public void ThrownInteraction(EntityUid user, EntityUid thrown) { var throwMsg = new ThrownEvent(user, thrown); RaiseLocalEvent(thrown, throwMsg); if (throwMsg.Handled) { _adminLogSystem.Add(LogType.Throw, LogImpact.Low,$"{ToPrettyString(user):user} threw {ToPrettyString(thrown):entity}"); return; } var comps = AllComps(thrown).ToList(); var args = new ThrownEventArgs(user); // Call Thrown on all components that implement the interface foreach (var comp in comps) { comp.Thrown(args); } _adminLogSystem.Add(LogType.Throw, LogImpact.Low,$"{ToPrettyString(user):user} threw {ToPrettyString(thrown):entity}"); } #endregion #region Equip /// /// Calls Equipped on all components that implement the IEquipped interface /// on an entity that has been equipped. /// public void EquippedInteraction(EntityUid user, EntityUid equipped, EquipmentSlotDefines.Slots slot) { var equipMsg = new EquippedEvent(user, equipped, slot); RaiseLocalEvent(equipped, equipMsg); if (equipMsg.Handled) return; var comps = AllComps(equipped).ToList(); // Call Thrown on all components that implement the interface foreach (var comp in comps) { comp.Equipped(new EquippedEventArgs(user, slot)); } } /// /// Calls Unequipped on all components that implement the IUnequipped interface /// on an entity that has been equipped. /// public void UnequippedInteraction(EntityUid user, EntityUid equipped, EquipmentSlotDefines.Slots slot) { var unequipMsg = new UnequippedEvent(user, equipped, slot); RaiseLocalEvent(equipped, unequipMsg); if (unequipMsg.Handled) return; var comps = AllComps(equipped).ToList(); // Call Thrown on all components that implement the interface foreach (var comp in comps) { comp.Unequipped(new UnequippedEventArgs(user, slot)); } } #region Equip Hand /// /// Calls EquippedHand on all components that implement the IEquippedHand interface /// on an item. /// public void EquippedHandInteraction(EntityUid user, EntityUid item, HandState hand) { var equippedHandMessage = new EquippedHandEvent(user, item, hand); RaiseLocalEvent(item, equippedHandMessage); if (equippedHandMessage.Handled) return; var comps = AllComps(item).ToList(); foreach (var comp in comps) { comp.EquippedHand(new EquippedHandEventArgs(user, hand)); } } /// /// Calls UnequippedHand on all components that implement the IUnequippedHand interface /// on an item. /// public void UnequippedHandInteraction(EntityUid user, EntityUid item, HandState hand) { var unequippedHandMessage = new UnequippedHandEvent(user, item, hand); RaiseLocalEvent(item, unequippedHandMessage); if (unequippedHandMessage.Handled) return; var comps = AllComps(item).ToList(); foreach (var comp in comps) { comp.UnequippedHand(new UnequippedHandEventArgs(user, hand)); } } #endregion #endregion #region Drop /// /// Activates the Dropped behavior of an object /// Verifies that the user is capable of doing the drop interaction first /// public bool TryDroppedInteraction(EntityUid user, EntityUid item) { if (!_actionBlockerSystem.CanDrop(user)) return false; DroppedInteraction(user, item); return true; } /// /// Calls Dropped on all components that implement the IDropped interface /// on an entity that has been dropped. /// public void DroppedInteraction(EntityUid user, EntityUid item) { var dropMsg = new DroppedEvent(user, item); RaiseLocalEvent(item, dropMsg); if (dropMsg.Handled) { _adminLogSystem.Add(LogType.Drop, LogImpact.Low, $"{ToPrettyString(user):user} dropped {ToPrettyString(item):entity}"); return; } Transform(item).LocalRotation = Angle.Zero; var comps = AllComps(item).ToList(); // Call Land on all components that implement the interface foreach (var comp in comps) { comp.Dropped(new DroppedEventArgs(user)); } _adminLogSystem.Add(LogType.Drop, LogImpact.Low, $"{ToPrettyString(user):user} dropped {ToPrettyString(item):entity}"); } #endregion #region Hand Selected /// /// Calls HandSelected on all components that implement the IHandSelected interface /// on an item entity on a hand that has just been selected. /// public void HandSelectedInteraction(EntityUid user, EntityUid item) { var handSelectedMsg = new HandSelectedEvent(user, item); RaiseLocalEvent(item, handSelectedMsg); if (handSelectedMsg.Handled) return; var comps = AllComps(item).ToList(); // Call Land on all components that implement the interface foreach (var comp in comps) { comp.HandSelected(new HandSelectedEventArgs(user)); } } /// /// Calls HandDeselected on all components that implement the IHandDeselected interface /// on an item entity on a hand that has just been deselected. /// public void HandDeselectedInteraction(EntityUid user, EntityUid item) { var handDeselectedMsg = new HandDeselectedEvent(user, item); RaiseLocalEvent(item, handDeselectedMsg); if (handDeselectedMsg.Handled) return; var comps = AllComps(item).ToList(); // Call Land on all components that implement the interface foreach (var comp in comps) { comp.HandDeselected(new HandDeselectedEventArgs(user)); } } #endregion /// /// 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. /// public abstract bool CanAccessViaStorage(EntityUid user, EntityUid target); protected bool ValidateClientInput(ICommonSession? session, EntityCoordinates coords, EntityUid uid, [NotNullWhen(true)] out EntityUid? userEntity) { userEntity = null; if (!coords.IsValid(EntityManager)) { Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}"); return false; } if (uid.IsClientSide()) { Logger.WarningS("system.interaction", $"Client sent interaction with client-side entity. Session={session}, Uid={uid}"); return false; } userEntity = session?.AttachedEntity; if (userEntity == null || !userEntity.Value.Valid) { Logger.WarningS("system.interaction", $"Client sent interaction with no attached entity. Session={session}"); return false; } return true; } #endregion } /// /// Raised when a player attempts to activate an item in an inventory slot or hand slot /// [Serializable, NetSerializable] public class InteractInventorySlotEvent : EntityEventArgs { /// /// Entity that was interacted with. /// public EntityUid ItemUid { get; } /// /// Whether the interaction used the alt-modifier to trigger alternative interactions. /// public bool AltInteract { get; } public InteractInventorySlotEvent(EntityUid itemUid, bool altInteract = false) { ItemUid = itemUid; AltInteract = altInteract; } } }