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 readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] protected readonly SharedContainerSystem ContainerSystem = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly SharedStorageSystem _storage = default!; [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; [Dependency] private readonly SharedVirtualItemSystem _virtualSystem = default!; [Dependency] private readonly 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) { ContainerSystem.EnsureContainer(ent, id, container); } } 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); 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 (!ent.Comp.Hands.Remove(handName)) return; if (ContainerSystem.TryGetContainer(ent, handName, out var container)) ContainerSystem.ShutdownContainer(container); 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; } 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; } /// /// Gets the item currently held in the entity's specified hand. Returns null if no hands are present or there is no item. /// public EntityUid? GetHeldItem(Entity ent, string? handId) { TryGetHeldItem(ent, handId, out var held); return held; } /// /// Gets the item currently held in the entity's specified hand. Returns false if no hands are present or there is no item. /// public bool TryGetHeldItem(Entity ent, string? handId, [NotNullWhen(true)] out EntityUid? held) { 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(); return held != null; } public bool HandIsEmpty(Entity ent, string handId) { return GetHeldItem(ent, handId) == null; } public int GetHandCount(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return 0; return ent.Comp.Hands.Count; } 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; } public int CountFreeableHands(Entity hands) { var freeable = 0; foreach (var name in hands.Comp.Hands.Keys) { if (HandIsEmpty(hands.AsNullable(), name) || CanDropHeld(hands, name)) freeable++; } return freeable; } }