using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.Interaction; using Content.Shared.Item; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Content.Shared.Hands.Components { [NetworkedComponent] public abstract class SharedHandsComponent : Component { [Dependency] private readonly IEntityManager _entMan = default!; /// /// The name of the currently active hand. /// [ViewVariables] public string? ActiveHand; [ViewVariables] public List Hands = new(); /// /// The amount of throw impulse per distance the player is from the throw target. /// [DataField("throwForceMultiplier")] [ViewVariables(VVAccess.ReadWrite)] public float ThrowForceMultiplier { get; set; } = 10f; //should be tuned so that a thrown item lands about under the player's cursor /// /// Distance after which longer throw targets stop increasing throw impulse. /// [DataField("throwRange")] [ViewVariables(VVAccess.ReadWrite)] public float ThrowRange { get; set; } = 8f; private bool PlayerCanDrop => EntitySystem.Get().CanDrop(Owner); private bool PlayerCanPickup => EntitySystem.Get().CanPickup(Owner); public void AddHand(string handName, HandLocation handLocation) { if (HasHand(handName)) return; var container = Owner.EnsureContainer(handName); container.OccludesLight = false; Hands.Add(new Hand(handName, handLocation, container)); if (ActiveHand == null) EntitySystem.Get().TrySetActiveHand(Owner, handName, this); HandCountChanged(); Dirty(); } public void RemoveHand(string handName) { if (!TryGetHand(handName, out var hand)) return; RemoveHand(hand); } private void RemoveHand(Hand hand) { DropHeldEntityToFloor(hand); hand.Container?.Shutdown(); Hands.Remove(hand); if (ActiveHand == hand.Name) EntitySystem.Get().TrySetActiveHand(Owner, Hands.FirstOrDefault()?.Name, this); HandCountChanged(); Dirty(); } public Hand? GetActiveHand() { if (ActiveHand == null) return null; return GetHandOrNull(ActiveHand); } public bool HasHand(string handName) { return TryGetHand(handName, out _); } public Hand? GetHandOrNull(string handName) { return TryGetHand(handName, out var hand) ? hand : null; } public Hand GetHand(string handName) { if (!TryGetHand(handName, out var hand)) throw new KeyNotFoundException($"Unable to find hand with name {handName}"); return hand; } public bool TryGetHand(string handName, [NotNullWhen(true)] out Hand? foundHand) { foreach (var hand in Hands) { if (hand.Name == handName) { foundHand = hand; return true; }; } foundHand = null; return false; } public bool TryGetActiveHand([NotNullWhen(true)] out Hand? activeHand) { activeHand = GetActiveHand(); return activeHand != null; } #region Held Entities public bool ActiveHandIsHoldingEntity() { if (!TryGetActiveHand(out var hand)) return false; return hand.HeldEntity != null; } public bool TryGetHeldEntity(string handName, [NotNullWhen(true)] out EntityUid? heldEntity) { heldEntity = null; if (!TryGetHand(handName, out var hand)) return false; heldEntity = hand.HeldEntity; return heldEntity != null; } public bool TryGetActiveHeldEntity([NotNullWhen(true)] out EntityUid? heldEntity) { heldEntity = GetActiveHand()?.HeldEntity; return heldEntity != null; } public bool IsHolding(EntityUid entity) { foreach (var hand in Hands) { if (hand.HeldEntity == entity) return true; } return false; } public IEnumerable GetAllHeldEntities() { foreach (var hand in Hands) { if (hand.HeldEntity != null) yield return hand.HeldEntity.Value; } } /// /// Returns the number of hands that have no items in them. /// /// public int GetFreeHands() { int acc = 0; foreach (var hand in Hands) { if (hand.HeldEntity == null) acc += 1; } return acc; } public bool TryGetHandHoldingEntity(EntityUid entity, [NotNullWhen(true)] out Hand? handFound) { handFound = null; foreach (var hand in Hands) { if (hand.HeldEntity == entity) { handFound = hand; return true; } } return false; } #endregion #region Dropping /// /// Checks all the conditions relevant to a player being able to drop an item. /// public bool CanDrop(string handName, bool checkActionBlocker = true) { if (!TryGetHand(handName, out var hand)) return false; if (!CanRemoveHeldEntityFromHand(hand)) return false; if (checkActionBlocker && !PlayerCanDrop) return false; return true; } /// /// Tries to drop the contents of the active hand to the target location. /// public bool TryDropActiveHand(EntityCoordinates targetDropLocation, bool doMobChecks = true) { if (!TryGetActiveHand(out var hand)) return false; return TryDropHeldEntity(hand, targetDropLocation, doMobChecks); } /// /// Tries to drop the contents of a hand to the target location. /// public bool TryDropHand(string handName, EntityCoordinates targetDropLocation, bool checkActionBlocker = true) { if (!TryGetHand(handName, out var hand)) return false; return TryDropHeldEntity(hand, targetDropLocation, checkActionBlocker); } /// /// Tries to drop a held entity to the target location. /// public bool TryDropEntity(EntityUid entity, EntityCoordinates coords, bool doMobChecks = true) { if (!TryGetHandHoldingEntity(entity, out var hand)) return false; return TryDropHeldEntity(hand, coords, doMobChecks); } /// /// Attempts to move the contents of a hand into a container that is not another hand, without dropping it on the floor inbetween. /// public bool TryPutHandIntoContainer(string handName, BaseContainer targetContainer, bool checkActionBlocker = true) { if (!TryGetHand(handName, out var hand)) return false; if (!CanPutHeldEntityIntoContainer(hand, targetContainer, checkActionBlocker)) return false; PutHeldEntityIntoContainer(hand, targetContainer); return true; } /// /// Attempts to move a held item from a hand into a container that is not another hand, without dropping it on the floor in-between. /// public bool Drop(EntityUid entity, BaseContainer targetContainer, bool checkActionBlocker = true) { if (!TryGetHandHoldingEntity(entity, out var hand)) return false; if (!CanPutHeldEntityIntoContainer(hand, targetContainer, checkActionBlocker)) return false; PutHeldEntityIntoContainer(hand, targetContainer); return true; } /// /// Tries to drop the contents of a hand directly under the player. /// public bool Drop(string handName, bool checkActionBlocker = true) { if (!TryGetHand(handName, out var hand)) return false; return TryDropHeldEntity(hand, _entMan.GetComponent(Owner).Coordinates, checkActionBlocker); } /// /// Tries to drop a held entity directly under the player. /// public bool Drop(EntityUid entity, bool checkActionBlocker = true) { if (!TryGetHandHoldingEntity(entity, out var hand)) return false; return TryDropHeldEntity(hand, _entMan.GetComponent(Owner).Coordinates, checkActionBlocker); } /// /// Tries to remove the item in the active hand, without dropping it. /// For transferring the held item to another location, like an inventory slot, /// which shouldn't trigger the drop interaction /// public bool TryDropNoInteraction() { if (!TryGetActiveHand(out var hand)) return false; if (!CanRemoveHeldEntityFromHand(hand)) return false; EntitySystem.Get().RemoveHeldEntityFromHand(Owner, hand, this); return true; } /// /// Checks if the contents of a hand is able to be removed from its container. /// private bool CanRemoveHeldEntityFromHand(Hand hand) { if (hand.HeldEntity == null) return false; if (!hand.Container!.CanRemove(hand.HeldEntity.Value)) return false; return true; } /// /// Drops a hands contents to the target location. /// public void DropHeldEntity(Hand hand, EntityCoordinates targetDropLocation) { if (hand.HeldEntity == null) return; var heldEntity = hand.HeldEntity.Value; EntitySystem.Get().RemoveHeldEntityFromHand(Owner, hand, this); EntitySystem.Get().DroppedInteraction(Owner, heldEntity); _entMan.GetComponent(heldEntity).WorldPosition = GetFinalDropCoordinates(targetDropLocation); } /// /// Calculates the final location a dropped item will end up at, accounting for max drop range and collision along the targeted drop path. /// private Vector2 GetFinalDropCoordinates(EntityCoordinates targetCoords) { var origin = _entMan.GetComponent(Owner).MapPosition; var target = targetCoords.ToMap(_entMan); var dropVector = target.Position - origin.Position; var requestedDropDistance = dropVector.Length; if (dropVector.Length > SharedInteractionSystem.InteractionRange) { dropVector = dropVector.Normalized * SharedInteractionSystem.InteractionRange; target = new MapCoordinates(origin.Position + dropVector, target.MapId); } var dropLength = EntitySystem.Get().UnobstructedDistance(origin, target, ignoredEnt: Owner); if (dropLength < requestedDropDistance) return origin.Position + dropVector.Normalized * dropLength; return target.Position; } /// /// Tries to drop a hands contents to the target location. /// private bool TryDropHeldEntity(Hand hand, EntityCoordinates location, bool checkActionBlocker) { if (!CanRemoveHeldEntityFromHand(hand)) return false; if (checkActionBlocker && !PlayerCanDrop) return false; DropHeldEntity(hand, location); return true; } /// /// Drops the contents of a hand directly under the player. /// private void DropHeldEntityToFloor(Hand hand) { DropHeldEntity(hand, _entMan.GetComponent(Owner).Coordinates); } private bool CanPutHeldEntityIntoContainer(Hand hand, IContainer targetContainer, bool checkActionBlocker) { if (hand.HeldEntity == null) return false; var heldEntity = hand.HeldEntity.Value; if (checkActionBlocker && !PlayerCanDrop) return false; if (!targetContainer.CanInsert(heldEntity)) return false; return true; } /// /// For putting the contents of a hand into a container that is not another hand. /// private void PutHeldEntityIntoContainer(Hand hand, IContainer targetContainer) { if (hand.HeldEntity == null) return; var heldEntity = hand.HeldEntity.Value; EntitySystem.Get().RemoveHeldEntityFromHand(Owner, hand, this); if (!targetContainer.Insert(heldEntity)) { Logger.Error($"{nameof(SharedHandsComponent)} on {Owner} could not insert {heldEntity} into {targetContainer}."); return; } } #endregion #region Pickup public bool CanPickupEntity(string handName, EntityUid entity, bool checkActionBlocker = true) { if (!TryGetHand(handName, out var hand)) return false; if (checkActionBlocker && !PlayerCanPickup) return false; if (!CanInsertEntityIntoHand(hand, entity)) return false; return true; } public bool CanPickupEntityToActiveHand(EntityUid entity, bool checkActionBlocker = true) { return ActiveHand != null && CanPickupEntity(ActiveHand, entity, checkActionBlocker); } /// /// Tries to pick up an entity to a specific hand. /// public bool TryPickupEntity(string handName, EntityUid entity, bool checkActionBlocker = true, bool animateUser = false) { if (!TryGetHand(handName, out var hand)) return false; return TryPickupEntity(hand, entity, checkActionBlocker, animateUser); } public bool TryPickupEntityToActiveHand(EntityUid entity, bool checkActionBlocker = true, bool animateUser = false) { return ActiveHand != null && TryPickupEntity(ActiveHand, entity, checkActionBlocker, animateUser); } /// /// Checks if an entity can be put into a hand's container. /// protected bool CanInsertEntityIntoHand(Hand hand, EntityUid entity) { var handContainer = hand.Container; if (handContainer == null) return false; if (!_entMan.HasComponent(entity)) return false; if (_entMan.TryGetComponent(entity, out IPhysBody? physics) && physics.BodyType == BodyType.Static) return false; if (!handContainer.CanInsert(entity)) return false; var @event = new AttemptItemPickupEvent(); _entMan.EventBus.RaiseLocalEvent(entity, @event); if (@event.Cancelled) return false; return true; } private bool TryPickupEntity(Hand hand, EntityUid entity, bool checkActionBlocker = true, bool animateUser = false) { if (!CanInsertEntityIntoHand(hand, entity)) return false; if (checkActionBlocker && !PlayerCanPickup) return false; // animation var handSys = EntitySystem.Get(); var coordinateEntity = _entMan.GetComponent(Owner).Parent?.Owner ?? Owner; var initialPosition = EntityCoordinates.FromMap(coordinateEntity, _entMan.GetComponent(entity).MapPosition); var finalPosition = _entMan.GetComponent(Owner).LocalPosition; handSys.PickupAnimation(entity, initialPosition, finalPosition, animateUser ? null : Owner); handSys.PutEntityIntoHand(Owner, hand, entity, this); return true; } #endregion #region Hand Interactions /// /// Get the name of the hand that a swap hands would result in. /// public bool TryGetSwapHandsResult([NotNullWhen(true)] out string? nextHand) { nextHand = null; if (!TryGetActiveHand(out var activeHand) || Hands.Count == 1) return false; var newActiveIndex = Hands.IndexOf(activeHand) + 1; var finalHandIndex = Hands.Count - 1; if (newActiveIndex > finalHandIndex) newActiveIndex = 0; nextHand = Hands[newActiveIndex].Name; return true; } /// /// Attempts to interact with the item in a hand using the active held item. /// public void InteractHandWithActiveHand(string handName) { if (!TryGetActiveHeldEntity(out var activeHeldEntity)) return; if (!TryGetHeldEntity(handName, out var heldEntity)) return; if (activeHeldEntity == heldEntity) return; EntitySystem.Get() .InteractUsing(Owner, activeHeldEntity.Value, heldEntity.Value, EntityCoordinates.Invalid); } public void ActivateItem(bool altInteract = false) { if (!TryGetActiveHeldEntity(out var heldEntity)) return; var sys = EntitySystem.Get(); if (altInteract) sys.AltInteract(Owner, heldEntity.Value); else sys.UseInHandInteraction(Owner, heldEntity.Value); } public void ActivateHeldEntity(string handName) { if (!TryGetHeldEntity(handName, out var heldEntity)) return; EntitySystem.Get() .InteractionActivate(Owner, heldEntity.Value); } /// /// Moves an entity from one hand to the active hand. /// public bool TryMoveHeldEntityToActiveHand(string handName, bool checkActionBlocker = true) { if (!TryGetHand(handName, out var hand) || !TryGetActiveHand(out var activeHand)) return false; if (!TryGetHeldEntity(handName, out var heldEntity)) return false; if (!CanInsertEntityIntoHand(activeHand, heldEntity.Value) || !CanRemoveHeldEntityFromHand(hand)) return false; if (checkActionBlocker && (!PlayerCanDrop || !PlayerCanPickup)) return false; EntitySystem.Get().RemoveHeldEntityFromHand(Owner, hand, this); EntitySystem.Get().PutEntityIntoHand(Owner, activeHand, heldEntity.Value, this); return true; } #endregion private void HandCountChanged() { _entMan.EventBus.RaiseEvent(EventSource.Local, new HandCountChangedEvent(Owner)); } /// /// Tries to pick up an entity into the active hand. If it cannot, tries to pick up the entity into each other hand. /// public bool PutInHand(SharedItemComponent item, bool checkActionBlocker = true) { return PutInHand(item.Owner, checkActionBlocker); } /// /// Puts an item any hand, prefering the active hand, or puts it on the floor under the player. /// public void PutInHandOrDrop(EntityUid entity, bool checkActionBlocker = true) { if (!PutInHand(entity, checkActionBlocker)) _entMan.GetComponent(entity).Coordinates = _entMan.GetComponent(Owner).Coordinates; } public void PutInHandOrDrop(SharedItemComponent item, bool checkActionBlocker = true) { PutInHandOrDrop(item.Owner, checkActionBlocker); } /// /// Tries to pick up an entity into the active hand. If it cannot, tries to pick up the entity into each other hand. /// public bool PutInHand(EntityUid entity, bool checkActionBlocker = true) { return PutInHand(entity, GetActiveHand(), checkActionBlocker); } /// /// Tries to pick up an entity into the priority hand, if provided. If it cannot, tries to pick up the entity into each other hand. /// public bool TryPutInAnyHand(EntityUid entity, string? priorityHandName = null, bool checkActionBlocker = true) { Hand? priorityHand = null; if (priorityHandName != null) priorityHand = GetHandOrNull(priorityHandName); return PutInHand(entity, priorityHand, checkActionBlocker); } /// /// Tries to pick up an entity into the priority hand, if provided. If it cannot, tries to pick up the entity into each other hand. /// private bool PutInHand(EntityUid entity, Hand? priorityHand = null, bool checkActionBlocker = true) { if (priorityHand != null) { if (TryPickupEntity(priorityHand, entity, checkActionBlocker)) return true; } foreach (var hand in Hands) { if (TryPickupEntity(hand, entity, checkActionBlocker)) return true; } return false; } /// /// Checks if any hand can pick up an item. /// public bool CanPutInHand(SharedItemComponent item, bool mobCheck = true) { var entity = item.Owner; if (mobCheck && !PlayerCanPickup) return false; foreach (var hand in Hands) { if (CanInsertEntityIntoHand(hand, entity)) return true; } return false; } } #region visualizerData [Serializable, NetSerializable] public enum HandsVisuals : byte { VisualState } [Serializable, NetSerializable] public class HandsVisualState : ICloneable { public List Hands { get; } = new(); public HandsVisualState(List hands) { Hands = hands; } public object Clone() { return new HandsVisualState(new List(Hands)); } } [Serializable, NetSerializable] public struct HandVisualState { public string RsiPath { get; } public string? EquippedPrefix { get; } public HandLocation Location { get; } public Color Color { get; } public HandVisualState(string rsiPath, string? equippedPrefix, HandLocation location, Color color) { RsiPath = rsiPath; EquippedPrefix = equippedPrefix; Location = location; Color = color; } } #endregion [Serializable, NetSerializable] public class Hand { [ViewVariables] public string Name { get; } [ViewVariables] public HandLocation Location { get; } /// /// The container used to hold the contents of this hand. Nullable because the client must get the containers via , /// which may not be synced with the server when the client hands are created. /// [ViewVariables, NonSerialized] public ContainerSlot? Container; [ViewVariables] public EntityUid? HeldEntity => Container?.ContainedEntity; public bool IsEmpty => HeldEntity == null; public Hand(string name, HandLocation location, ContainerSlot? container = null) { Name = name; Location = location; Container = container; } } [Serializable, NetSerializable] public sealed class HandsComponentState : ComponentState { public List Hands { get; } public string? ActiveHand { get; } public HandsComponentState(List hands, string? activeHand = null) { Hands = hands; ActiveHand = activeHand; } } /// /// A message that calls the use interaction on an item in hand, presumed for now the interaction will occur only on the active hand. /// [Serializable, NetSerializable] public sealed class UseInHandMsg : EntityEventArgs { } /// /// A message that calls the activate interaction on the item in the specified hand. /// [Serializable, NetSerializable] public class ActivateInHandMsg : EntityEventArgs { public string HandName { get; } public ActivateInHandMsg(string handName) { HandName = handName; } } /// /// Uses the item in the active hand on the item in the specified hand. /// [Serializable, NetSerializable] public class ClientInteractUsingInHandMsg : EntityEventArgs { public string HandName { get; } public ClientInteractUsingInHandMsg(string handName) { HandName = handName; } } /// /// Moves an item from one hand to the active hand. /// [Serializable, NetSerializable] public class MoveItemFromHandMsg : EntityEventArgs { public string HandName { get; } public MoveItemFromHandMsg(string handName) { HandName = handName; } } /// /// What side of the body this hand is on. /// public enum HandLocation : byte { Left, Middle, Right } public class HandCountChangedEvent : EntityEventArgs { public HandCountChangedEvent(EntityUid sender) { Sender = sender; } public EntityUid Sender { get; } } }