using System; using System.Linq; using Content.Shared.Physics; using Content.Shared.Popups; using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Serialization; namespace Content.Shared.Interaction { /// /// Governs interactions during clicking on entities /// [UsedImplicitly] public abstract class SharedInteractionSystem : EntitySystem { [Dependency] private readonly SharedPhysicsSystem _sharedBroadphaseSystem = default!; public const float InteractionRange = 2; public const float InteractionRangeSquared = InteractionRange * InteractionRange; public delegate bool Ignored(IEntity 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, IEntity? 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 (!result.HitEntity.TryGetComponent(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( IEntity origin, IEntity 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, other.Transform.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( IEntity 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( IEntity 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( IEntity origin, MapCoordinates other, float range = InteractionRange, CollisionGroup collisionMask = CollisionGroup.Impassable, Ignored? predicate = null, bool ignoreInsideBlocker = false, bool popup = false) { var originPosition = origin.Transform.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; } } /// /// 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; } } }