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.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!; public sealed override string Name => "Hands"; public event Action? OnItemChanged; //TODO: Try to replace C# event /// /// The name of the currently active hand. /// [ViewVariables(VVAccess.ReadWrite)] public string? ActiveHand { get => _activeHand; set { if (value != null && !HasHand(value)) { Logger.Warning($"{nameof(SharedHandsComponent)} on {Owner} tried to set its active hand to {value}, which was not a hand."); return; } if (value == null && Hands.Count != 0) { Logger.Error($"{nameof(SharedHandsComponent)} on {Owner} tried to set its active hand to null, when it still had another hand."); _activeHand = Hands[0].Name; return; } if (value != ActiveHand) { DeselectActiveHeldEntity(); _activeHand = value; SelectActiveHeldEntity(); HandsModified(); } } } private string? _activeHand; [ViewVariables] public readonly 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; public override ComponentState GetComponentState() { var hands = new HandState[Hands.Count]; for (var i = 0; i < Hands.Count; i++) { var hand = Hands[i].ToHandState(); hands[i] = hand; } return new HandsComponentState(hands, ActiveHand); } public virtual void HandsModified() { // todo axe all this for ECS. // todo burn it all down. UpdateHandVisualizer(); Dirty(); _entMan.EventBus.RaiseEvent(EventSource.Local, new HandsModifiedMessage { Hands = this }); } public void UpdateHandVisualizer() { var entMan = _entMan; if (!entMan.TryGetComponent(Owner, out AppearanceComponent? appearance)) return; var hands = new List(); foreach (var hand in Hands) { if (hand.HeldEntity == null) continue; if (!entMan.TryGetComponent(hand.HeldEntity, out SharedItemComponent? item) || item.RsiPath == null) continue; var handState = new HandVisualState(item.RsiPath, item.EquippedPrefix, hand.Location, item.Color); hands.Add(handState); } appearance.SetData(HandsVisuals.VisualState, new HandsVisualState(hands)); } public void AddHand(string handName, HandLocation handLocation) { if (HasHand(handName)) return; var container = Owner.CreateContainer(handName); container.OccludesLight = false; Hands.Add(new Hand(handName, handLocation, container)); ActiveHand ??= handName; HandCountChanged(); HandsModified(); } 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) ActiveHand = Hands.FirstOrDefault()?.Name; HandCountChanged(); HandsModified(); } private 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 hand.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.HasValue) 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; RemoveHeldEntityFromHand(hand); 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; return hand.Container?.CanRemove(hand.HeldEntity.Value) ?? false; } /// /// Checks if the player is allowed to perform drops. /// private bool PlayerCanDrop() { if (!IoCManager.Resolve().GetEntitySystem().CanDrop(Owner)) return false; return true; } /// /// Removes the contents of a hand from its container. Assumes that the removal is allowed. /// private void RemoveHeldEntityFromHand(Hand hand) { if (hand.HeldEntity is not { } heldEntity) return; var handContainer = hand.Container; if (handContainer == null) return; if (hand.Name == ActiveHand) DeselectActiveHeldEntity(); if (!handContainer.Remove(heldEntity)) { Logger.Error($"{nameof(SharedHandsComponent)} on {Owner} could not remove {heldEntity} from {handContainer}."); return; } OnHeldEntityRemovedFromHand(heldEntity, hand.ToHandState()); HandsModified(); } /// /// Drops a hands contents to the target location. /// public void DropHeldEntity(Hand hand, EntityCoordinates targetDropLocation) { if (hand.HeldEntity is not { } heldEntity) return; RemoveHeldEntityFromHand(hand); EntitySystem.Get().DroppedInteraction(Owner, heldEntity); _entMan.GetComponent(heldEntity).WorldPosition = GetFinalDropCoordinates(targetDropLocation); OnItemChanged?.Invoke(); } /// /// 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 is not { } heldEntity) return false; 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 is not { } heldEntity) return; RemoveHeldEntityFromHand(hand); 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) { if (!TryGetHand(handName, out var hand)) return false; return TryPickupEntity(hand, entity, checkActionBlocker); } public bool TryPickupEntityToActiveHand(EntityUid entity, bool checkActionBlocker = true) { return ActiveHand != null && TryPickupEntity(ActiveHand, entity, checkActionBlocker); } /// /// 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 (!handContainer.CanInsert(entity)) return false; var @event = new AttemptItemPickupEvent(); _entMan.EventBus.RaiseLocalEvent(entity, @event); if (@event.Cancelled) return false; return true; } /// /// Checks if the player is allowed to perform pickup actions. /// /// protected bool PlayerCanPickup() { if (!EntitySystem.Get().CanPickup(Owner)) return false; return true; } /// /// Puts an entity into the player's hand, assumes that the insertion is allowed. /// public void PutEntityIntoHand(Hand hand, EntityUid entity) { var handContainer = hand.Container; if (handContainer == null) return; if (!handContainer.Insert(entity)) { Logger.Error($"{nameof(SharedHandsComponent)} on {Owner} could not insert {entity} into {handContainer}."); return; } EntitySystem.Get().EquippedHandInteraction(Owner, entity, hand.ToHandState()); if (hand.Name == ActiveHand) SelectActiveHeldEntity(); _entMan.GetComponent(entity).LocalPosition = Vector2.Zero; OnItemChanged?.Invoke(); HandsModified(); } private bool TryPickupEntity(Hand hand, EntityUid entity, bool checkActionBlocker = true) { if (!CanInsertEntityIntoHand(hand, entity)) return false; if (checkActionBlocker && !PlayerCanPickup()) return false; HandlePickupAnimation(entity); PutEntityIntoHand(hand, entity); EntitySystem.Get().Add(LogType.Pickup, LogImpact.Low, $"{_entMan.ToPrettyString(Owner):user} picked up {_entMan.ToPrettyString(entity):entity}"); 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 async void InteractHandWithActiveHand(string handName) { if (!TryGetActiveHeldEntity(out var activeHeldEntity)) return; if (!TryGetHeldEntity(handName, out var heldEntity)) return; if (activeHeldEntity == heldEntity) return; await 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.TryUseInteraction(Owner, heldEntity.Value); } public void ActivateHeldEntity(string handName) { if (!TryGetHeldEntity(handName, out var heldEntity)) return; EntitySystem.Get() .TryInteractionActivate(Owner, heldEntity); } /// /// 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; RemoveHeldEntityFromHand(hand); PutEntityIntoHand(activeHand, heldEntity.Value); return true; } #endregion private void DeselectActiveHeldEntity() { if (TryGetActiveHeldEntity(out var entity)) EntitySystem.Get().HandDeselectedInteraction(Owner, entity.Value); } private void SelectActiveHeldEntity() { if (TryGetActiveHeldEntity(out var entity)) EntitySystem.Get().HandSelectedInteraction(Owner, entity.Value); } 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 TryPutInActiveHandOrAny(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 uid, bool checkActionBlocker = true) { return TryPutInActiveHandOrAny(uid, checkActionBlocker); } /// /// Puts an item any hand, prefering the active hand, or puts it on the floor under the player. /// public void PutInHandOrDrop(SharedItemComponent item, bool checkActionBlocker = true) => PutInHandOrDrop(item.Owner, checkActionBlocker); public void PutInHandOrDrop(EntityUid uid, bool checkActionBlocker = true) { if (!TryPutInActiveHandOrAny(uid, checkActionBlocker)) _entMan.GetComponent(uid).Coordinates = _entMan.GetComponent(Owner).Coordinates; } /// /// 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 TryPutInActiveHandOrAny(EntityUid entity, bool checkActionBlocker = true) { return TryPutInAnyHand(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 TryPutInAnyHand(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 TryPutInAnyHand(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; } protected virtual void OnHeldEntityRemovedFromHand(EntityUid heldEntity, HandState handState) { } protected virtual void HandlePickupAnimation(EntityUid entity) { } } #region visualizerData [Serializable, NetSerializable] public enum HandsVisuals : byte { VisualState } [Serializable, NetSerializable] public class HandsVisualState { public List Hands { get; } = new(); public HandsVisualState(List hands) { Hands = hands; } } [Serializable, NetSerializable] public class 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 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] public IContainer? Container { get; set; } [ViewVariables] public EntityUid? HeldEntity => Container?.ContainedEntities?.Count > 0 ? Container.ContainedEntities[0] : null; public Hand(string name, HandLocation location, IContainer? container = null) { Name = name; Location = location; Container = container; } public HandState ToHandState() { return new(Name, Location); } } [Serializable, NetSerializable] public struct HandState { public string Name { get; } public HandLocation Location { get; } public HandState(string name, HandLocation location) { Name = name; Location = location; } } [Serializable, NetSerializable] public sealed class HandsComponentState : ComponentState { public HandState[] Hands { get; } public string? ActiveHand { get; } public HandsComponentState(HandState[] 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; } } [Serializable, NetSerializable] public class PickupAnimationMessage : EntityEventArgs { public EntityUid EntityUid { get; } public EntityCoordinates InitialPosition { get; } public Vector2 FinalPosition { get; } public PickupAnimationMessage(EntityUid entityUid, Vector2 finalPosition, EntityCoordinates initialPosition) { EntityUid = entityUid; FinalPosition = finalPosition; InitialPosition = initialPosition; } } [Serializable, NetSerializable] public struct HandsModifiedMessage { public SharedHandsComponent Hands; } }