using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Inventory.VirtualItem; using Content.Shared.Storage.EntitySystems; using Content.Shared.Whitelist; using Robust.Shared.Containers; using Robust.Shared.Input.Binding; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Shared.Hands.EntitySystems; public abstract partial class SharedHandsSystem { [Dependency] private ISharedAdminLogManager _adminLogger = default!; [Dependency] private ActionBlockerSystem _actionBlocker = default!; [Dependency] protected SharedContainerSystem ContainerSystem = default!; [Dependency] private SharedInteractionSystem _interactionSystem = default!; [Dependency] private InventorySystem _inventory = default!; [Dependency] private SharedStorageSystem _storage = default!; [Dependency] protected SharedTransformSystem TransformSystem = default!; [Dependency] private SharedVirtualItemSystem _virtualSystem = default!; [Dependency] private EntityWhitelistSystem _entityWhitelist = default!; public event Action, string, HandLocation>? OnPlayerAddHand; public event Action, string>? OnPlayerRemoveHand; protected event Action?>? OnHandSetActive; public override void Initialize() { base.Initialize(); InitializeInteractions(); InitializeDrop(); InitializePickup(); InitializeRelay(); InitializeEventListeners(); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnMapInit); } public override void Shutdown() { base.Shutdown(); CommandBinds.Unregister(); } private void OnInit(Entity ent, ref ComponentInit args) { var container = EnsureComp(ent); foreach (var id in ent.Comp.Hands.Keys) { var slot = ContainerSystem.EnsureContainer(ent, id, container); slot.OccludesLight = false; } } private void OnMapInit(Entity ent, ref MapInitEvent args) { if (ent.Comp.ActiveHandId == null) SetActiveHand(ent.AsNullable(), ent.Comp.SortedHands.FirstOrDefault()); } /// /// Adds a hand with the given container id and supplied location to the specified entity. /// public void AddHand(Entity ent, string handName, HandLocation handLocation, LocId? emptyLabel = null, EntProtoId? emptyRepresentative = null, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null) { AddHand(ent, handName, new Hand(handLocation, emptyLabel, emptyRepresentative, whitelist, blacklist)); } /// /// Adds a hand with the given container id and supplied hand definition to the given entity. /// public void AddHand(Entity ent, string handName, Hand hand) { if (!Resolve(ent, ref ent.Comp, false)) return; if (ent.Comp.Hands.ContainsKey(handName)) return; var container = ContainerSystem.EnsureContainer(ent, handName); container.OccludesLight = false; ent.Comp.Hands.Add(handName, hand); ent.Comp.SortedHands.Add(handName); // we use LINQ + ToList instead of the list sort because it's a stable sort vs the list sort ent.Comp.SortedHands = ent.Comp.SortedHands.OrderBy(handId => ent.Comp.Hands[handId].Location).ToList(); Dirty(ent); OnPlayerAddHand?.Invoke((ent, ent.Comp), handName, hand.Location); if (ent.Comp.ActiveHandId == null) SetActiveHand(ent, handName); RaiseLocalEvent(ent, new HandCountChangedEvent(ent)); } /// /// Removes the specified hand from the specified entity /// public virtual void RemoveHand(Entity ent, string handName) { if (!Resolve(ent, ref ent.Comp, false)) return; OnPlayerRemoveHand?.Invoke((ent, ent.Comp), handName); TryDrop(ent, handName, null, false); if (ContainerSystem.TryGetContainer(ent, handName, out var container)) ContainerSystem.ShutdownContainer(container); if (!ent.Comp.Hands.Remove(handName)) return; ent.Comp.SortedHands.Remove(handName); if (ent.Comp.ActiveHandId == handName) TrySetActiveHand(ent, ent.Comp.SortedHands.FirstOrDefault()); RaiseLocalEvent(ent, new HandCountChangedEvent(ent)); Dirty(ent); } /// /// Gets rid of all the entity's hands. /// public void RemoveHands(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return; var handIds = new List(ent.Comp.Hands.Keys); foreach (var handId in handIds) { RemoveHand(ent, handId); } } private void HandleSetHand(RequestSetHandEvent msg, EntitySessionEventArgs eventArgs) { if (eventArgs.SenderSession.AttachedEntity == null) return; TrySetActiveHand(eventArgs.SenderSession.AttachedEntity.Value, msg.HandName); } /// /// Get any empty hand. Prioritizes the currently active hand. /// public bool TryGetEmptyHand(Entity ent, [NotNullWhen(true)] out string? emptyHand) { emptyHand = null; if (!Resolve(ent, ref ent.Comp, false)) return false; foreach (var hand in EnumerateHands(ent)) { if (HandIsEmpty(ent, hand)) { emptyHand = hand; return true; } } return false; } /// /// Does this entity have any empty hands, and how many? /// public int GetEmptyHandCount(Entity entity) { if (!Resolve(entity, ref entity.Comp, false) || entity.Comp.Count == 0) return 0; var hands = 0; foreach (var hand in EnumerateHands(entity)) { if (!HandIsEmpty(entity, hand)) continue; hands++; } return hands; } /// /// Attempts to retrieve the item held in the entity's active hand. /// public bool TryGetActiveItem(Entity entity, [NotNullWhen(true)] out EntityUid? item) { item = null; if (!Resolve(entity, ref entity.Comp, false)) return false; if (!TryGetHeldItem(entity, entity.Comp.ActiveHandId, out var held)) return false; item = held; return true; } /// /// Gets active hand item if relevant otherwise gets the entity itself. /// public EntityUid GetActiveItemOrSelf(Entity entity) { if (!TryGetActiveItem(entity, out var item)) { return entity.Owner; } return item.Value; } /// /// Gets the current active hand's Id for the specified entity /// /// /// public string? GetActiveHand(Entity entity) { if (!Resolve(entity, ref entity.Comp, false)) return null; return entity.Comp.ActiveHandId; } /// /// Gets the current active hand's held entity for the specified entity /// /// /// public EntityUid? GetActiveItem(Entity entity) { if (!Resolve(entity, ref entity.Comp, false)) return null; return GetHeldItem(entity, entity.Comp.ActiveHandId); } public bool ActiveHandIsEmpty(Entity entity) { return GetActiveItem(entity) == null; } /// /// Enumerate over hands, starting with the currently active hand. /// public IEnumerable EnumerateHands(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) yield break; if (ent.Comp.ActiveHandId != null) yield return ent.Comp.ActiveHandId; foreach (var name in ent.Comp.SortedHands) { if (name != ent.Comp.ActiveHandId) yield return name; } } /// /// Enumerate over held items, starting with the item in the currently active hand (if there is one). /// public IEnumerable EnumerateHeld(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) yield break; if (TryGetActiveItem(ent, out var activeHeld)) yield return activeHeld.Value; foreach (var name in ent.Comp.SortedHands) { if (name == ent.Comp.ActiveHandId) continue; if (TryGetHeldItem(ent, name, out var held)) yield return held.Value; } } /// /// Set the currently active hand and raise hand (de)selection events directed at the held entities. /// /// True if the active hand was set to a NEW value. Setting it to the same value returns false and does /// not trigger interactions. public bool TrySetActiveHand(Entity ent, string? name) { if (!Resolve(ent, ref ent.Comp, false)) return false; if (name == ent.Comp.ActiveHandId) return false; if (name != null && !ent.Comp.Hands.ContainsKey(name)) return false; return SetActiveHand(ent, name); } /// /// Set the currently active hand and raise hand (de)selection events directed at the held entities. /// /// True if the active hand was set to a NEW value. Setting it to the same value returns false and does /// not trigger interactions. public bool SetActiveHand(Entity ent, string? handId) { if (!Resolve(ent, ref ent.Comp)) return false; if (handId == ent.Comp.ActiveHandId) return false; if (TryGetActiveItem(ent, out var oldHeld)) RaiseLocalEvent(oldHeld.Value, new HandDeselectedEvent(ent)); if (handId == null) { ent.Comp.ActiveHandId = null; return true; } ent.Comp.ActiveHandId = handId; OnHandSetActive?.Invoke((ent, ent.Comp)); if (TryGetHeldItem(ent, handId, out var newHeld)) RaiseLocalEvent(newHeld.Value, new HandSelectedEvent(ent)); Dirty(ent); return true; } /// /// Cycles the currently active hand. /// public void SwapHands(Entity ent, bool checkActionBlocker = false, bool reverse = false) { if (!Resolve(ent, ref ent.Comp)) return; if (checkActionBlocker && !_actionBlocker.CanInteract(ent.Owner, null)) return; if (ent.Comp.ActiveHandId == null || ent.Comp.Hands.Count < 2) return; var currentIndex = ent.Comp.SortedHands.IndexOf(ent.Comp.ActiveHandId); var newActiveIndex = (currentIndex + (reverse ? -1 : 1) + ent.Comp.Hands.Count) % ent.Comp.Hands.Count; var nextHand = ent.Comp.SortedHands[newActiveIndex]; TrySetActiveHand(ent, nextHand); } /// /// Checks if an item is being held by another entity. /// /// Item that we are checking. /// User who is holding our item. /// Returns true if our item is being held. public bool IsHeld(Entity item, [NotNullWhen(true)] out EntityUid? user) { user = null; item.Comp ??= Transform(item); if (!IsHolding(item.Comp.ParentUid, item)) return false; user = item.Comp.ParentUid; return true; } public bool IsHolding(Entity entity, [NotNullWhen(true)] EntityUid? item) { return IsHolding(entity, item, out _); } public bool IsHolding(Entity ent, [NotNullWhen(true)] EntityUid? entity, [NotNullWhen(true)] out string? inHand) { inHand = null; if (entity == null) return false; if (!Resolve(ent, ref ent.Comp, false)) return false; foreach (var hand in ent.Comp.Hands.Keys) { if (GetHeldItem(ent, hand) == entity) { inHand = hand; return true; } } return false; } /// /// Attempts to retrieve the associated hand struct corresponding to a hand ID on a given entity. /// public bool TryGetHand(Entity ent, [NotNullWhen(true)] string? handId, [NotNullWhen(true)] out Hand? hand) { hand = null; if (handId == null) return false; if (!Resolve(ent, ref ent.Comp, false)) return false; if (!ent.Comp.Hands.TryGetValue(handId, out var handsHand)) return false; hand = handsHand; return true; } /// /// Retrieves the item currently held in the specified hand of an entity. /// /// The entity whose hands are being checked. /// The ID of the specific hand to check. /// If true, treats hands holding virtual items as if they are empty. /// The UID of the held item, or null if the hand is empty, the hand ID is invalid, or the entity has no hands. public EntityUid? GetHeldItem(Entity ent, string? handId, bool hideVirtualItems = false) { TryGetHeldItem(ent, handId, out var held, hideVirtualItems); return held; } /// /// Attempts to retrieve the item currently held in the specified hand of an entity. /// /// The entity whose hands are being checked. /// The ID of the specific hand to check. /// Outputs the UID of the held item, if one is found. /// If true, ignores virtual items and returns false if only a virtual item is present. /// True if a valid item is found in the hand; otherwise, false. public bool TryGetHeldItem(Entity ent, string? handId, [NotNullWhen(true)] out EntityUid? held, bool hideVirtualItems = false) { held = null; if (!Resolve(ent, ref ent.Comp, false)) return false; // Sanity check to make sure this is actually a hand. if (handId == null || !ent.Comp.Hands.ContainsKey(handId)) return false; if (!ContainerSystem.TryGetContainer(ent, handId, out var container)) return false; held = container.ContainedEntities.FirstOrNull(); if (hideVirtualItems && HasComp(held)) held = null; return held != null; } public bool HandIsEmpty(Entity ent, string handId) { return GetHeldItem(ent, handId) == null; } /// /// Counts the number of hands on this entity. /// public int GetHandCount(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return 0; return ent.Comp.Hands.Count; } /// /// Counts the number of hands that are empty. /// public int CountFreeHands(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return 0; var free = 0; foreach (var name in ent.Comp.Hands.Keys) { if (HandIsEmpty(ent, name)) free++; } return free; } /// /// Counts the number of hands that are empty or can be emptied by dropping an item. /// Unremoveable items will cause a hand to not be freeable. /// /// The hand this entity is in will be ignored when counting. public int CountFreeableHands(Entity hands, EntityUid? except = null) { var freeable = 0; foreach (var name in hands.Comp.Hands.Keys) { if (except != null && GetHeldItem(hands.AsNullable(), name) == except) continue; if (HandIsEmpty(hands.AsNullable(), name) || CanDropHeld(hands, name)) freeable++; } return freeable; } }