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;
}
}
}