using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.Hands; using Content.Shared.Hands.Components; 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.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Players; using Robust.Shared.Random; 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!; public const float InteractionRange = 2; public const float InteractionRangeSquared = InteractionRange * InteractionRange; public delegate bool Ignored(EntityUid entity); /// /// 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 (!EntityManager.TryGetComponent(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, EntityManager.GetComponent(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 = EntityManager.GetComponent(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 = EntityManager.GetComponents(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 = EntityManager.GetComponents(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 (EntityManager.TryGetComponent(used, out var 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)} activated {ToPrettyString(used)}"); return; } if (!EntityManager.TryGetComponent(used, out IActivate? activateComp)) return; var activateEventArgs = new ActivateEventArgs(user, used); activateComp.Activate(activateEventArgs); _adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user)} activated {ToPrettyString(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 (user != null && used != null && _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 (EntityManager.TryGetComponent(used, out var delayComponent)) { if (delayComponent.ActiveDelay) return; delayComponent.BeginDelay(); } var useMsg = new UseInHandEvent(user, used); RaiseLocalEvent(used, useMsg); if (useMsg.Handled) return; var uses = EntityManager.GetComponents(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)} threw {ToPrettyString(thrown)}"); return; } var comps = EntityManager.GetComponents(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)} threw {ToPrettyString(thrown)}"); } #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 = EntityManager.GetComponents(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 = EntityManager.GetComponents(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 = EntityManager.GetComponents(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 = EntityManager.GetComponents(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 (user == null || item == null || !_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)} dropped {ToPrettyString(item)}"); return; } EntityManager.GetComponent(item).LocalRotation = Angle.Zero; var comps = EntityManager.GetComponents(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)} dropped {ToPrettyString(item)}"); } #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 = EntityManager.GetComponents(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 = EntityManager.GetComponents(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); #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; } } }