using System.Linq; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Physics; using JetBrains.Annotations; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Physics; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Maths; namespace Content.Shared.GameObjects.EntitySystems { /// /// Governs interactions during clicking on entities /// [UsedImplicitly] public class SharedInteractionSystem : EntitySystem { [Dependency] private readonly IPhysicsManager _physicsManager = 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 = _physicsManager.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 = _physicsManager.IntersectRayWithPredicate(origin.MapId, ray, dir.Length, predicate.Invoke, false).ToList(); if (rayResults.Count == 0) return true; if (!ignoreInsideBlocker) return false; if (rayResults.Count <= 0) return false; return (rayResults[0].HitPos - other.Position).Length < 1f; } /// /// 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) { var originPosition = origin.Transform.MapPosition; var otherPosition = other.Transform.MapPosition; predicate ??= e => e == origin || e == other; var inRange = InRangeUnobstructed(originPosition, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker); if (!inRange && popup) { var message = Loc.GetString("You can't reach there!"); origin.PopupMessage(message); } return inRange; } /// /// 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) { var originPosition = origin.Transform.MapPosition; var otherPosition = other.Owner.Transform.MapPosition; predicate ??= e => e == origin || e == other.Owner; var inRange = InRangeUnobstructed(originPosition, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker); if (!inRange && popup) { var message = Loc.GetString("You can't reach there!"); origin.PopupMessage(message); } return inRange; } /// /// 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) { var originPosition = origin.Transform.MapPosition; var otherPosition = other.ToMap(EntityManager); predicate ??= e => e == origin; var inRange = InRangeUnobstructed(originPosition, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker); if (!inRange && popup) { var message = Loc.GetString("You can't reach there!"); origin.PopupMessage(message); } return inRange; } /// /// 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("You can't reach there!"); origin.PopupMessage(message); } return inRange; } /// /// Checks that the user and target of a /// 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 event args 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 both the user and target 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 user entity for /// it to see. /// /// /// True if the two points are within a given range without being obstructed. /// public bool InRangeUnobstructed( ITargetedInteractEventArgs args, float range = InteractionRange, CollisionGroup collisionMask = CollisionGroup.Impassable, Ignored predicate = null, bool ignoreInsideBlocker = false, bool popup = false) { var origin = args.User; var other = args.Target; return InRangeUnobstructed(origin, other, range, collisionMask, predicate, ignoreInsideBlocker, popup); } /// /// Checks that the user of a is within a /// certain distance of the target and dropped entities 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 event args 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 both the user and target 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 user entity for /// it to see. /// /// /// True if the two points are within a given range without being obstructed. /// public bool InRangeUnobstructed( DragDropEventArgs args, float range = InteractionRange, CollisionGroup collisionMask = CollisionGroup.Impassable, Ignored predicate = null, bool ignoreInsideBlocker = false, bool popup = false) { var user = args.User; var dropped = args.Dropped; var target = args.Target; if (!InRangeUnobstructed(user, target, range, collisionMask, predicate, ignoreInsideBlocker)) { if (popup) { var message = Loc.GetString("You can't reach there!"); target.PopupMessage(user, message); } return false; } if (!InRangeUnobstructed(user, dropped, range, collisionMask, predicate, ignoreInsideBlocker)) { if (popup) { var message = Loc.GetString("You can't reach there!"); dropped.PopupMessage(user, message); } return false; } return true; } /// /// Checks that the user and target of a /// 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 event args 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 both the user and target 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 user entity for /// it to see. /// /// /// True if the two points are within a given range without being obstructed. /// public bool InRangeUnobstructed( AfterInteractEventArgs args, float range = InteractionRange, CollisionGroup collisionMask = CollisionGroup.Impassable, Ignored predicate = null, bool ignoreInsideBlocker = false, bool popup = false) { var user = args.User; var target = args.Target; predicate ??= e => e == user; MapCoordinates otherPosition; if (target == null) { otherPosition = args.ClickLocation.ToMap(EntityManager); } else { otherPosition = target.Transform.MapPosition; predicate += e => e == target; } return InRangeUnobstructed(user, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup); } } }