]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
HandsSystem Refactor (#38438)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Wed, 25 Jun 2025 13:13:03 +0000 (09:13 -0400)
committerGitHub <noreply@github.com>
Wed, 25 Jun 2025 13:13:03 +0000 (15:13 +0200)
* checkpoint

* pt 2

* pt... i forgot

* pt 4

* patch

* More test fixes

* optimization!!!

* the REAL hand system

* fix RetractableItemActionSystem.cs oversight

* the review

* test

* remove test usage of body prototype

* Update Content.IntegrationTests/Tests/Interaction/InteractionTest.cs

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
* hellcode

* hellcode 2

* Minor cleanup

* test

* Chasing the last of the bugs

* changes

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
79 files changed:
Content.Client/Hands/Systems/HandsSystem.cs
Content.Client/Inventory/StrippableBoundUserInterface.cs
Content.Client/RCD/AlignRCDConstruction.cs
Content.Client/RCD/RCDConstructionGhostSystem.cs
Content.Client/SubFloor/TrayScannerSystem.cs
Content.Client/UserInterface/Systems/Hands/HandsUIController.cs
Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
Content.IntegrationTests/Tests/Actions/RetractableItemActionTest.cs
Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs
Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
Content.IntegrationTests/Tests/Construction/Interaction/WallConstruction.cs
Content.IntegrationTests/Tests/Hands/HandTests.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
Content.Server/Administration/Commands/StripAllCommand.cs
Content.Server/Administration/Systems/AdminSystem.cs
Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
Content.Server/Bed/Cryostorage/CryostorageSystem.cs
Content.Server/Botany/Systems/PlantHolderSystem.cs
Content.Server/Chat/SuicideSystem.cs
Content.Server/Construction/ConstructionSystem.Initial.cs
Content.Server/Hands/Systems/HandsSystem.cs
Content.Server/HotPotato/HotPotatoSystem.cs
Content.Server/NPC/HTN/Preconditions/ActiveHandComponentPrecondition.cs
Content.Server/NPC/HTN/Preconditions/ActiveHandEntityPrecondition.cs
Content.Server/NPC/HTN/PrimitiveTasks/Operators/Interactions/DropOperator.cs
Content.Server/NPC/NPCBlackboard.cs
Content.Server/NPC/Systems/NPCUtilitySystem.cs
Content.Server/Shuttles/Commands/FTLDiskCommand.cs
Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
Content.Server/Tabletop/TabletopSystem.cs
Content.Server/Tools/Innate/InnateToolSystem.cs
Content.Server/Verbs/VerbSystem.cs
Content.Server/VoiceTrigger/StorageVoiceControlSystem.cs
Content.Server/Wires/WiresSystem.cs
Content.Shared/Access/Systems/SharedIdCardSystem.cs
Content.Shared/Blocking/BlockingSystem.cs
Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs
Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
Content.Shared/Cuffs/SharedCuffableSystem.cs
Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs
Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs
Content.Shared/DoAfter/SharedDoAfterSystem.cs
Content.Shared/Foldable/DeployFoldableSystem.cs
Content.Shared/Hands/Components/HandHelpers.cs [deleted file]
Content.Shared/Hands/Components/HandsComponent.cs
Content.Shared/Hands/EntitySystems/SharedHandsSystem.AI.cs
Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs
Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs
Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs
Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs
Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs
Content.Shared/Interaction/SharedInteractionSystem.cs
Content.Shared/Interaction/SmartEquipSystem.cs
Content.Shared/Inventory/InventorySystem.Equip.cs
Content.Shared/Inventory/InventorySystem.Helpers.cs
Content.Shared/Inventory/VirtualItem/SharedVirtualItemSystem.cs
Content.Shared/Item/MultiHandedItemSystem.cs
Content.Shared/Item/SharedItemSystem.cs
Content.Shared/Magic/SharedMagicSystem.cs
Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
Content.Shared/Ninja/Systems/SharedNinjaGlovesSystem.cs
Content.Shared/Nutrition/EntitySystems/FoodSystem.cs
Content.Shared/RCD/Systems/RCDSystem.cs
Content.Shared/RetractableItemAction/RetractableItemActionSystem.cs
Content.Shared/Stacks/SharedStackSystem.cs
Content.Shared/Station/SharedStationSpawningSystem.cs
Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs
Content.Shared/Strip/SharedStrippableSystem.cs
Content.Shared/UserInterface/ActivatableUISystem.cs
Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs
Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
Content.Shared/Wieldable/SharedWieldableSystem.cs
Resources/Prototypes/Body/Prototypes/a_ghost.yml [deleted file]
Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml

index ab389a49d591b029a9e9c914a1ce39889e2592e6..37bc9b1232de152989abcd2a500a22d05f1dc073 100644 (file)
@@ -16,7 +16,6 @@ using Robust.Client.UserInterface;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 using Robust.Shared.Player;
-using Robust.Shared.Timing;
 using Robust.Shared.Utility;
 
 namespace Content.Client.Hands.Systems
@@ -27,16 +26,13 @@ namespace Content.Client.Hands.Systems
         [Dependency] private readonly IPlayerManager _playerManager = default!;
         [Dependency] private readonly IUserInterfaceManager _ui = default!;
 
-        [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
         [Dependency] private readonly StrippableSystem _stripSys = default!;
         [Dependency] private readonly SpriteSystem _sprite = default!;
         [Dependency] private readonly ExamineSystem _examine = default!;
         [Dependency] private readonly DisplacementMapSystem _displacement = default!;
 
-        public event Action<string, HandLocation>? OnPlayerAddHand;
-        public event Action<string>? OnPlayerRemoveHand;
         public event Action<string?>? OnPlayerSetActiveHand;
-        public event Action<HandsComponent>? OnPlayerHandsAdded;
+        public event Action<Entity<HandsComponent>>? OnPlayerHandsAdded;
         public event Action? OnPlayerHandsRemoved;
         public event Action<string, EntityUid>? OnPlayerItemAdded;
         public event Action<string, EntityUid>? OnPlayerItemRemoved;
@@ -58,67 +54,28 @@ namespace Content.Client.Hands.Systems
         }
 
         #region StateHandling
-        private void HandleComponentState(EntityUid uid, HandsComponent component, ref ComponentHandleState args)
+        private void HandleComponentState(Entity<HandsComponent> ent, ref ComponentHandleState args)
         {
             if (args.Current is not HandsComponentState state)
                 return;
 
-            var handsModified = component.Hands.Count != state.Hands.Count;
-            // we need to check that, even if we have the same amount, that the individual hands didn't change.
-            if (!handsModified)
+            var newHands = state.Hands.Keys.Except(ent.Comp.Hands.Keys); // hands that were added between states
+            var oldHands = ent.Comp.Hands.Keys.Except(state.Hands.Keys); // hands that were removed between states
+
+            foreach (var handId in oldHands)
             {
-                foreach (var hand in component.Hands.Values)
-                {
-                    if (state.Hands.Contains(hand))
-                        continue;
-                    handsModified = true;
-                    break;
-                }
+                RemoveHand(ent.AsNullable(), handId);
             }
 
-            var manager = EnsureComp<ContainerManagerComponent>(uid);
-
-            if (handsModified)
+            foreach (var handId in state.SortedHands.Intersect(newHands))
             {
-                List<Hand> addedHands = new();
-                foreach (var hand in state.Hands)
-                {
-                    if (component.Hands.ContainsKey(hand.Name))
-                        continue;
-
-                    var container = _containerSystem.EnsureContainer<ContainerSlot>(uid, hand.Name, manager);
-                    var newHand = new Hand(hand.Name, hand.Location, container);
-                    component.Hands.Add(hand.Name, newHand);
-                    addedHands.Add(newHand);
-                }
-
-                foreach (var name in component.Hands.Keys)
-                {
-                    if (!state.HandNames.Contains(name))
-                    {
-                        RemoveHand(uid, name, component);
-                    }
-                }
-
-                component.SortedHands.Clear();
-                component.SortedHands.AddRange(state.HandNames);
-                var sorted = addedHands.OrderBy(hand => component.SortedHands.IndexOf(hand.Name));
-
-                foreach (var hand in sorted)
-                {
-                    AddHand(uid, hand, component);
-                }
+                AddHand(ent.AsNullable(), handId, state.Hands[handId]);
             }
+            ent.Comp.SortedHands = new (state.SortedHands);
 
-            _stripSys.UpdateUi(uid);
-
-            if (component.ActiveHand == null && state.ActiveHand == null)
-                return; //edge case
+            SetActiveHand(ent.AsNullable(), state.ActiveHandId);
 
-            if (component.ActiveHand != null && state.ActiveHand != component.ActiveHand.Name)
-            {
-                SetActiveHand(uid, component.Hands[state.ActiveHand!], component);
-            }
+            _stripSys.UpdateUi(ent);
         }
         #endregion
 
@@ -129,47 +86,52 @@ namespace Content.Client.Hands.Systems
                 return;
             }
 
-            OnPlayerHandsAdded?.Invoke(hands);
+            OnPlayerHandsAdded?.Invoke(hands.Value);
         }
 
-        public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? hands = null, bool log = true)
+        public override void DoDrop(Entity<HandsComponent?> ent,
+            string handId,
+            bool doDropInteraction = true,
+            bool log = true)
         {
-            base.DoDrop(uid, hand, doDropInteraction, hands, log);
+            base.DoDrop(ent, handId, doDropInteraction, log);
 
-            if (TryComp(hand.HeldEntity, out SpriteComponent? sprite))
+            if (TryGetHeldItem(ent, handId, out var held) && TryComp(held, out SpriteComponent? sprite))
                 sprite.RenderOrder = EntityManager.CurrentTick.Value;
         }
 
         public EntityUid? GetActiveHandEntity()
         {
-            return TryGetPlayerHands(out var hands) ? hands.ActiveHandEntity : null;
+            return TryGetPlayerHands(out var hands) ? GetActiveItem(hands.Value.AsNullable()) : null;
         }
 
         /// <summary>
         ///     Get the hands component of the local player
         /// </summary>
-        public bool TryGetPlayerHands([NotNullWhen(true)] out HandsComponent? hands)
+        public bool TryGetPlayerHands([NotNullWhen(true)] out Entity<HandsComponent>? hands)
         {
             var player = _playerManager.LocalEntity;
             hands = null;
-            return player != null && TryComp(player.Value, out hands);
+            if (player == null || !TryComp<HandsComponent>(player.Value, out var handsComp))
+                return false;
+
+            hands = (player.Value, handsComp);
+            return true;
         }
 
         /// <summary>
         ///     Called when a user clicked on their hands GUI
         /// </summary>
-        public void UIHandClick(HandsComponent hands, string handName)
+        public void UIHandClick(Entity<HandsComponent> ent, string handName)
         {
-            if (!hands.Hands.TryGetValue(handName, out var pressedHand))
-                return;
-
-            if (hands.ActiveHand == null)
+            var hands = ent.Comp;
+            if (hands.ActiveHandId == null)
                 return;
 
-            var pressedEntity = pressedHand.HeldEntity;
-            var activeEntity = hands.ActiveHand.HeldEntity;
+            var pressedEntity = GetHeldItem(ent.AsNullable(), handName);
+            var activeEntity = GetActiveItem(ent.AsNullable());
 
-            if (pressedHand == hands.ActiveHand && activeEntity != null)
+            if (handName == hands.ActiveHandId && activeEntity != null)
             {
                 // use item in hand
                 // it will always be attack_self() in my heart.
@@ -177,24 +139,24 @@ namespace Content.Client.Hands.Systems
                 return;
             }
 
-            if (pressedHand != hands.ActiveHand && pressedEntity == null)
+            if (handName != hands.ActiveHandId && pressedEntity == null)
             {
                 // change active hand
                 EntityManager.RaisePredictiveEvent(new RequestSetHandEvent(handName));
                 return;
             }
 
-            if (pressedHand != hands.ActiveHand && pressedEntity != null && activeEntity != null)
+            if (handName != hands.ActiveHandId && pressedEntity != null && activeEntity != null)
             {
                 // use active item on held item
-                EntityManager.RaisePredictiveEvent(new RequestHandInteractUsingEvent(pressedHand.Name));
+                EntityManager.RaisePredictiveEvent(new RequestHandInteractUsingEvent(handName));
                 return;
             }
 
-            if (pressedHand != hands.ActiveHand && pressedEntity != null && activeEntity == null)
+            if (handName != hands.ActiveHandId && pressedEntity != null && activeEntity == null)
             {
                 // move the item to the active hand
-                EntityManager.RaisePredictiveEvent(new RequestMoveHandItemEvent(pressedHand.Name));
+                EntityManager.RaisePredictiveEvent(new RequestMoveHandItemEvent(handName));
             }
         }
 
@@ -210,13 +172,12 @@ namespace Content.Client.Hands.Systems
         public void UIInventoryExamine(string handName)
         {
             if (!TryGetPlayerHands(out var hands) ||
-                !hands.Hands.TryGetValue(handName, out var hand) ||
-                hand.HeldEntity is not { Valid: true } entity)
+                !TryGetHeldItem(hands.Value.AsNullable(), handName, out var heldEntity))
             {
                 return;
             }
 
-            _examine.DoExamine(entity);
+            _examine.DoExamine(heldEntity.Value);
         }
 
         /// <summary>
@@ -226,13 +187,12 @@ namespace Content.Client.Hands.Systems
         public void UIHandOpenContextMenu(string handName)
         {
             if (!TryGetPlayerHands(out var hands) ||
-                !hands.Hands.TryGetValue(handName, out var hand) ||
-                hand.HeldEntity is not { Valid: true } entity)
+                !TryGetHeldItem(hands.Value.AsNullable(), handName, out var heldEntity))
             {
                 return;
             }
 
-            _ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(entity);
+            _ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(heldEntity.Value);
         }
 
         public void UIHandAltActivateItem(string handName)
@@ -246,60 +206,67 @@ namespace Content.Client.Hands.Systems
         {
             base.HandleEntityInserted(uid, hands, args);
 
-            if (!hands.Hands.TryGetValue(args.Container.ID, out var hand))
+            if (!hands.Hands.ContainsKey(args.Container.ID))
                 return;
-            UpdateHandVisuals(uid, args.Entity, hand);
+
+            UpdateHandVisuals(uid, args.Entity, args.Container.ID);
             _stripSys.UpdateUi(uid);
 
             if (uid != _playerManager.LocalEntity)
                 return;
 
-            OnPlayerItemAdded?.Invoke(hand.Name, args.Entity);
+            OnPlayerItemAdded?.Invoke(args.Container.ID, args.Entity);
 
             if (HasComp<VirtualItemComponent>(args.Entity))
-                OnPlayerHandBlocked?.Invoke(hand.Name);
+                OnPlayerHandBlocked?.Invoke(args.Container.ID);
         }
 
         protected override void HandleEntityRemoved(EntityUid uid, HandsComponent hands, EntRemovedFromContainerMessage args)
         {
             base.HandleEntityRemoved(uid, hands, args);
 
-            if (!hands.Hands.TryGetValue(args.Container.ID, out var hand))
+            if (!hands.Hands.ContainsKey(args.Container.ID))
                 return;
-            UpdateHandVisuals(uid, args.Entity, hand);
+
+            UpdateHandVisuals(uid, args.Entity, args.Container.ID);
             _stripSys.UpdateUi(uid);
 
             if (uid != _playerManager.LocalEntity)
                 return;
 
-            OnPlayerItemRemoved?.Invoke(hand.Name, args.Entity);
+            OnPlayerItemRemoved?.Invoke(args.Container.ID, args.Entity);
 
             if (HasComp<VirtualItemComponent>(args.Entity))
-                OnPlayerHandUnblocked?.Invoke(hand.Name);
+                OnPlayerHandUnblocked?.Invoke(args.Container.ID);
         }
 
         /// <summary>
         ///     Update the players sprite with new in-hand visuals.
         /// </summary>
-        private void UpdateHandVisuals(EntityUid uid, EntityUid held, Hand hand, HandsComponent? handComp = null, SpriteComponent? sprite = null)
+        private void UpdateHandVisuals(Entity<HandsComponent?, SpriteComponent?> ent, EntityUid held, string handId)
         {
-            if (!Resolve(uid, ref handComp, ref sprite, false))
+            if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
+                return;
+            var handComp = ent.Comp1;
+            var sprite = ent.Comp2;
+
+            if (!TryGetHand((ent, handComp), handId, out var hand))
                 return;
 
             // visual update might involve changes to the entity's effective sprite -> need to update hands GUI.
-            if (uid == _playerManager.LocalEntity)
-                OnPlayerItemAdded?.Invoke(hand.Name, held);
+            if (ent == _playerManager.LocalEntity)
+                OnPlayerItemAdded?.Invoke(handId, held);
 
             if (!handComp.ShowInHands)
                 return;
 
             // Remove old layers. We could also just set them to invisible, but as items may add arbitrary layers, this
             // may eventually bloat the player with lots of layers.
-            if (handComp.RevealedLayers.TryGetValue(hand.Location, out var revealedLayers))
+            if (handComp.RevealedLayers.TryGetValue(hand.Value.Location, out var revealedLayers))
             {
                 foreach (var key in revealedLayers)
                 {
-                    _sprite.RemoveLayer((uid, sprite), key);
+                    _sprite.RemoveLayer((ent, sprite), key);
                 }
 
                 revealedLayers.Clear();
@@ -307,22 +274,22 @@ namespace Content.Client.Hands.Systems
             else
             {
                 revealedLayers = new();
-                handComp.RevealedLayers[hand.Location] = revealedLayers;
+                handComp.RevealedLayers[hand.Value.Location] = revealedLayers;
             }
 
-            if (hand.HeldEntity == null)
+            if (HandIsEmpty((ent, handComp), handId))
             {
                 // the held item was removed.
-                RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);
+                RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(ent, revealedLayers), true);
                 return;
             }
 
-            var ev = new GetInhandVisualsEvent(uid, hand.Location);
+            var ev = new GetInhandVisualsEvent(ent, hand.Value.Location);
             RaiseLocalEvent(held, ev);
 
             if (ev.Layers.Count == 0)
             {
-                RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);
+                RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(ent, revealedLayers), true);
                 return;
             }
 
@@ -335,7 +302,7 @@ namespace Content.Client.Hands.Systems
                     continue;
                 }
 
-                var index = _sprite.LayerMapReserve((uid, sprite), key);
+                var index = _sprite.LayerMapReserve((ent, sprite), key);
 
                 // In case no RSI is given, use the item's base RSI as a default. This cuts down on a lot of unnecessary yaml entries.
                 if (layerData.RsiPath == null
@@ -343,35 +310,34 @@ namespace Content.Client.Hands.Systems
                     && sprite[index].Rsi == null)
                 {
                     if (TryComp<ItemComponent>(held, out var itemComponent) && itemComponent.RsiPath != null)
-                        _sprite.LayerSetRsi((uid, sprite), index, new ResPath(itemComponent.RsiPath));
+                        _sprite.LayerSetRsi((ent, sprite), index, new ResPath(itemComponent.RsiPath));
                     else if (TryComp(held, out SpriteComponent? clothingSprite))
-                        _sprite.LayerSetRsi((uid, sprite), index, clothingSprite.BaseRSI);
+                        _sprite.LayerSetRsi((ent, sprite), index, clothingSprite.BaseRSI);
                 }
 
-                _sprite.LayerSetData((uid, sprite), index, layerData);
+                _sprite.LayerSetData((ent, sprite), index, layerData);
 
                 // Add displacement maps
-                var displacement = hand.Location switch
+                var displacement = hand.Value.Location switch
                 {
                     HandLocation.Left => handComp.LeftHandDisplacement,
                     HandLocation.Right => handComp.RightHandDisplacement,
                     _ => handComp.HandDisplacement
                 };
 
-                if (displacement is not null && _displacement.TryAddDisplacement(displacement, (uid, sprite), index, key, out var displacementKey))
+                if (displacement is not null && _displacement.TryAddDisplacement(displacement, (ent, sprite), index, key, out var displacementKey))
                     revealedLayers.Add(displacementKey);
             }
 
-            RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);
+            RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(ent, revealedLayers), true);
         }
 
         private void OnVisualsChanged(EntityUid uid, HandsComponent component, VisualsChangedEvent args)
         {
             // update hands visuals if this item is in a hand (rather then inventory or other container).
-            if (component.Hands.TryGetValue(args.ContainerId, out var hand))
-            {
-                UpdateHandVisuals(uid, GetEntity(args.Item), hand, component);
-            }
+            if (!component.Hands.ContainsKey(args.ContainerId))
+                return;
+            UpdateHandVisuals((uid, component), GetEntity(args.Item), args.ContainerId);
         }
         #endregion
 
@@ -379,7 +345,7 @@ namespace Content.Client.Hands.Systems
 
         private void HandlePlayerAttached(EntityUid uid, HandsComponent component, LocalPlayerAttachedEvent args)
         {
-            OnPlayerHandsAdded?.Invoke(component);
+            OnPlayerHandsAdded?.Invoke((uid, component));
         }
 
         private void HandlePlayerDetached(EntityUid uid, HandsComponent component, LocalPlayerDetachedEvent args)
@@ -390,7 +356,7 @@ namespace Content.Client.Hands.Systems
         private void OnHandsStartup(EntityUid uid, HandsComponent component, ComponentStartup args)
         {
             if (_playerManager.LocalEntity == uid)
-                OnPlayerHandsAdded?.Invoke(component);
+                OnPlayerHandsAdded?.Invoke((uid, component));
         }
 
         private void OnHandsShutdown(EntityUid uid, HandsComponent component, ComponentShutdown args)
@@ -400,36 +366,6 @@ namespace Content.Client.Hands.Systems
         }
         #endregion
 
-        private void AddHand(EntityUid uid, Hand newHand, HandsComponent? handsComp = null)
-        {
-            AddHand(uid, newHand.Name, newHand.Location, handsComp);
-        }
-
-        public override void AddHand(EntityUid uid, string handName, HandLocation handLocation, HandsComponent? handsComp = null)
-        {
-            base.AddHand(uid, handName, handLocation, handsComp);
-
-            if (uid == _playerManager.LocalEntity)
-                OnPlayerAddHand?.Invoke(handName, handLocation);
-
-            if (handsComp == null)
-                return;
-
-            if (handsComp.ActiveHand == null)
-                SetActiveHand(uid, handsComp.Hands[handName], handsComp);
-        }
-        public override void RemoveHand(EntityUid uid, string handName, HandsComponent? handsComp = null)
-        {
-            if (uid == _playerManager.LocalEntity && handsComp != null &&
-                handsComp.Hands.ContainsKey(handName) && uid ==
-                _playerManager.LocalEntity)
-            {
-                OnPlayerRemoveHand?.Invoke(handName);
-            }
-
-            base.RemoveHand(uid, handName, handsComp);
-        }
-
         private void OnHandActivated(Entity<HandsComponent>? ent)
         {
             if (ent is not { } hand)
@@ -438,13 +374,7 @@ namespace Content.Client.Hands.Systems
             if (_playerManager.LocalEntity != hand.Owner)
                 return;
 
-            if (hand.Comp.ActiveHand == null)
-            {
-                OnPlayerSetActiveHand?.Invoke(null);
-                return;
-            }
-
-            OnPlayerSetActiveHand?.Invoke(hand.Comp.ActiveHand.Name);
+            OnPlayerSetActiveHand?.Invoke(hand.Comp.ActiveHandId);
         }
     }
 }
index a9a937d5d85896ab1798f5c5100e5c1b05f36ed7..381406d2ac4db786cdafbca9dda19a5b9638ff94 100644 (file)
@@ -1,6 +1,7 @@
 using System.Linq;
 using System.Numerics;
 using Content.Client.Examine;
+using Content.Client.Hands.Systems;
 using Content.Client.Strip;
 using Content.Client.Stylesheets;
 using Content.Client.UserInterface.Controls;
@@ -34,6 +35,7 @@ namespace Content.Client.Inventory
         [Dependency] private readonly IUserInterfaceManager _ui = default!;
 
         private readonly ExamineSystem _examine;
+        private readonly HandsSystem _hands;
         private readonly InventorySystem _inv;
         private readonly SharedCuffableSystem _cuffable;
         private readonly StrippableSystem _strippable;
@@ -65,6 +67,7 @@ namespace Content.Client.Inventory
         public StrippableBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
         {
             _examine = EntMan.System<ExamineSystem>();
+            _hands = EntMan.System<HandsSystem>();
             _inv = EntMan.System<InventorySystem>();
             _cuffable = EntMan.System<SharedCuffableSystem>();
             _strippable = EntMan.System<StrippableSystem>();
@@ -120,28 +123,28 @@ namespace Content.Client.Inventory
             {
                 // good ol hands shit code. there is a GuiHands comparer that does the same thing... but these are hands
                 // and not gui hands... which are different...
-                foreach (var hand in handsComp.Hands.Values)
+                foreach (var (id, hand) in handsComp.Hands)
                 {
                     if (hand.Location != HandLocation.Right)
                         continue;
 
-                    AddHandButton(hand);
+                    AddHandButton((Owner, handsComp), id, hand);
                 }
 
-                foreach (var hand in handsComp.Hands.Values)
+                foreach (var (id, hand) in handsComp.Hands)
                 {
                     if (hand.Location != HandLocation.Middle)
                         continue;
 
-                    AddHandButton(hand);
+                    AddHandButton((Owner, handsComp), id, hand);
                 }
 
-                foreach (var hand in handsComp.Hands.Values)
+                foreach (var (id, hand) in handsComp.Hands)
                 {
                     if (hand.Location != HandLocation.Left)
                         continue;
 
-                    AddHandButton(hand);
+                    AddHandButton((Owner, handsComp), id, hand);
                 }
             }
 
@@ -177,20 +180,21 @@ namespace Content.Client.Inventory
             _strippingMenu.SetSize = new Vector2(horizontalMenuSize, verticalMenuSize);
         }
 
-        private void AddHandButton(Hand hand)
+        private void AddHandButton(Entity<HandsComponent> ent, string handId, Hand hand)
         {
-            var button = new HandButton(hand.Name, hand.Location);
+            var button = new HandButton(handId, hand.Location);
 
             button.Pressed += SlotPressed;
 
-            if (EntMan.TryGetComponent<VirtualItemComponent>(hand.HeldEntity, out var virt))
+            var heldEntity = _hands.GetHeldItem(ent.AsNullable(), handId);
+            if (EntMan.TryGetComponent<VirtualItemComponent>(heldEntity, out var virt))
             {
                 button.Blocked = true;
                 if (EntMan.TryGetComponent<CuffableComponent>(Owner, out var cuff) && _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity))
                     button.BlockedRect.MouseFilter = MouseFilterMode.Ignore;
             }
 
-            UpdateEntityIcon(button, hand.HeldEntity);
+            UpdateEntityIcon(button, heldEntity);
             _strippingMenu!.HandsContainer.AddChild(button);
             LayoutContainer.SetPosition(button, new Vector2i(_handCount, 0) * (SlotControl.DefaultButtonSize + ButtonSeparation));
             _handCount++;
index fbaf62c83d0edfc989a881a2e53b0f246311fff7..b1554588364ed3bed1a6562f7086ee3ca0c08bfc 100644 (file)
@@ -1,5 +1,6 @@
 using System.Numerics;
 using Content.Client.Gameplay;
+using Content.Client.Hands.Systems;
 using Content.Shared.Hands.Components;
 using Content.Shared.Interaction;
 using Content.Shared.RCD.Components;
@@ -17,6 +18,7 @@ public sealed class AlignRCDConstruction : PlacementMode
     [Dependency] private readonly IEntityManager _entityManager = default!;
     [Dependency] private readonly IMapManager _mapManager = default!;
     private readonly SharedMapSystem _mapSystem;
+    private readonly HandsSystem _handsSystem;
     private readonly RCDSystem _rcdSystem;
     private readonly SharedTransformSystem _transformSystem;
     [Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -34,6 +36,7 @@ public sealed class AlignRCDConstruction : PlacementMode
     {
         IoCManager.InjectDependencies(this);
         _mapSystem = _entityManager.System<SharedMapSystem>();
+        _handsSystem = _entityManager.System<HandsSystem>();
         _rcdSystem = _entityManager.System<RCDSystem>();
         _transformSystem = _entityManager.System<SharedTransformSystem>();
 
@@ -88,11 +91,9 @@ public sealed class AlignRCDConstruction : PlacementMode
         }
 
         // Determine if player is carrying an RCD in their active hand
-        if (!_entityManager.TryGetComponent<HandsComponent>(player, out var hands))
+        if (!_handsSystem.TryGetActiveItem(player.Value, out var heldEntity))
             return false;
 
-        var heldEntity = hands.ActiveHand?.HeldEntity;
-
         if (!_entityManager.TryGetComponent<RCDComponent>(heldEntity, out var rcd))
             return false;
 
index 23dcf6485feac7da801cfb0f9aebf268f0c23b26..8ec980268b9d4e421f195c4e988a67fc02df74ff 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Client.Hands.Systems;
 using Content.Shared.Hands.Components;
 using Content.Shared.Interaction;
 using Content.Shared.RCD;
@@ -15,6 +16,7 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
     [Dependency] private readonly IPlayerManager _playerManager = default!;
     [Dependency] private readonly IPlacementManager _placementManager = default!;
     [Dependency] private readonly IPrototypeManager _protoManager = default!;
+    [Dependency] private readonly HandsSystem _hands = default!;
 
     private string _placementMode = typeof(AlignRCDConstruction).Name;
     private Direction _placementDirection = default;
@@ -33,12 +35,11 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
             return;
 
         // Determine if player is carrying an RCD in their active hand
-        var player = _playerManager.LocalSession?.AttachedEntity;
-
-        if (!TryComp<HandsComponent>(player, out var hands))
+        if (_playerManager.LocalSession?.AttachedEntity is not { } player)
             return;
 
-        var heldEntity = hands.ActiveHand?.HeldEntity;
+        if (!_hands.TryGetActiveItem(player, out var heldEntity))
+            return;
 
         if (!TryComp<RCDComponent>(heldEntity, out var rcd))
         {
index 194453ab75a64b1db707d07c558bf8fc5faa979e..4c67890f6a56533844ec45e397f54b8b03fcc0d6 100644 (file)
@@ -68,11 +68,15 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
 
         foreach (var hand in _hands.EnumerateHands(player.Value))
         {
-            if (!scannerQuery.TryGetComponent(hand.HeldEntity, out var heldScanner) || !heldScanner.Enabled)
+            if (!_hands.TryGetHeldItem(player.Value, hand, out var heldEntity))
+                continue;
+
+            if (!scannerQuery.TryGetComponent(heldEntity, out var heldScanner) || !heldScanner.Enabled)
                 continue;
 
             range = MathF.Max(heldScanner.Range, range);
             canSee = true;
+            break;
         }
 
         inRange = new HashSet<Entity<SubFloorHideComponent>>();
index edc02600611c036daed5ec023afa003d8ed2eecc..ecaddd8f98bee68ecddd97e66a26855d35d6da16 100644 (file)
@@ -7,6 +7,7 @@ using Content.Shared.Hands.Components;
 using Content.Shared.Input;
 using Content.Shared.Inventory.VirtualItem;
 using Content.Shared.Timing;
+using JetBrains.Annotations;
 using Robust.Client.Player;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controllers;
@@ -28,7 +29,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
     private readonly Dictionary<string, int> _handContainerIndices = new();
     private readonly Dictionary<string, HandButton> _handLookup = new();
     private HandsComponent? _playerHandsComponent;
-    private HandButton? _activeHand = null;
+    private HandButton? _activeHand;
 
     // We only have two item status controls (left and right hand),
     // but we may have more than two hands.
@@ -38,7 +39,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
     private HandButton? _statusHandLeft;
     private HandButton? _statusHandRight;
 
-    private int _backupSuffix = 0; //this is used when autogenerating container names if they don't have names
+    private int _backupSuffix; //this is used when autogenerating container names if they don't have names
 
     private HotbarGui? HandsGui => UIManager.GetActiveUIWidgetOrNull<HotbarGui>();
 
@@ -48,7 +49,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
         _handsSystem.OnPlayerItemAdded += OnItemAdded;
         _handsSystem.OnPlayerItemRemoved += OnItemRemoved;
         _handsSystem.OnPlayerSetActiveHand += SetActiveHand;
-        _handsSystem.OnPlayerRemoveHand += RemoveHand;
+        _handsSystem.OnPlayerRemoveHand += OnRemoveHand;
         _handsSystem.OnPlayerHandsAdded += LoadPlayerHands;
         _handsSystem.OnPlayerHandsRemoved += UnloadPlayerHands;
         _handsSystem.OnPlayerHandBlocked += HandBlocked;
@@ -61,28 +62,35 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
         _handsSystem.OnPlayerItemAdded -= OnItemAdded;
         _handsSystem.OnPlayerItemRemoved -= OnItemRemoved;
         _handsSystem.OnPlayerSetActiveHand -= SetActiveHand;
-        _handsSystem.OnPlayerRemoveHand -= RemoveHand;
+        _handsSystem.OnPlayerRemoveHand -= OnRemoveHand;
         _handsSystem.OnPlayerHandsAdded -= LoadPlayerHands;
         _handsSystem.OnPlayerHandsRemoved -= UnloadPlayerHands;
         _handsSystem.OnPlayerHandBlocked -= HandBlocked;
         _handsSystem.OnPlayerHandUnblocked -= HandUnblocked;
     }
 
-    private void OnAddHand(string name, HandLocation location)
+    private void OnAddHand(Entity<HandsComponent> entity, string name, HandLocation location)
     {
+        if (entity.Owner != _player.LocalEntity)
+            return;
         AddHand(name, location);
     }
 
+    private void OnRemoveHand(Entity<HandsComponent> entity, string name)
+    {
+        if (entity.Owner != _player.LocalEntity)
+            return;
+        RemoveHand(name);
+    }
+
     private void HandPressed(GUIBoundKeyEventArgs args, SlotControl hand)
     {
-        if (_playerHandsComponent == null)
-        {
+        if (!_handsSystem.TryGetPlayerHands(out var hands))
             return;
-        }
 
         if (args.Function == EngineKeyFunctions.UIClick)
         {
-            _handsSystem.UIHandClick(_playerHandsComponent, hand.SlotName);
+            _handsSystem.UIHandClick(hands.Value, hand.SlotName);
             args.Handle();
         }
         else if (args.Function == EngineKeyFunctions.UseSecondary)
@@ -122,33 +130,33 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
         }
     }
 
-    private void LoadPlayerHands(HandsComponent handsComp)
+    private void LoadPlayerHands(Entity<HandsComponent> handsComp)
     {
         DebugTools.Assert(_playerHandsComponent == null);
         if (HandsGui != null)
             HandsGui.Visible = true;
 
         _playerHandsComponent = handsComp;
-        foreach (var (name, hand) in handsComp.Hands)
+        foreach (var (name, hand) in handsComp.Comp.Hands)
         {
             var handButton = AddHand(name, hand.Location);
 
-            if (_entities.TryGetComponent(hand.HeldEntity, out VirtualItemComponent? virt))
+            if (_handsSystem.TryGetHeldItem(handsComp.AsNullable(), name, out var held) &&
+                _entities.TryGetComponent(held, out VirtualItemComponent? virt))
             {
                 handButton.SetEntity(virt.BlockingEntity);
                 handButton.Blocked = true;
             }
             else
             {
-                handButton.SetEntity(hand.HeldEntity);
+                handButton.SetEntity(held);
                 handButton.Blocked = false;
             }
         }
 
-        var activeHand = handsComp.ActiveHand;
-        if (activeHand == null)
+        if (handsComp.Comp.ActiveHandId == null)
             return;
-        SetActiveHand(activeHand.Name);
+        SetActiveHand(handsComp.Comp.ActiveHandId);
     }
 
     private void HandBlocked(string handName)
@@ -260,19 +268,21 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
         if (HandsGui != null &&
             _playerHandsComponent != null &&
             _player.LocalSession?.AttachedEntity is { } playerEntity &&
-            _handsSystem.TryGetHand(playerEntity, handName, out var hand, _playerHandsComponent))
+            _handsSystem.TryGetHand((playerEntity, _playerHandsComponent), handName, out var hand))
         {
-            var foldedLocation = hand.Location.GetUILocation();
+            var heldEnt = _handsSystem.GetHeldItem((playerEntity, _playerHandsComponent), handName);
+
+            var foldedLocation = hand.Value.Location.GetUILocation();
             if (foldedLocation == HandUILocation.Left)
             {
                 _statusHandLeft = handControl;
-                HandsGui.UpdatePanelEntityLeft(hand.HeldEntity);
+                HandsGui.UpdatePanelEntityLeft(heldEnt);
             }
             else
             {
                 // Middle or right
                 _statusHandRight = handControl;
-                HandsGui.UpdatePanelEntityRight(hand.HeldEntity);
+                HandsGui.UpdatePanelEntityRight(heldEnt);
             }
 
             HandsGui.SetHighlightHand(foldedLocation);
@@ -292,7 +302,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
         button.Pressed += HandPressed;
 
         if (!_handLookup.TryAdd(handName, button))
-            throw new Exception("Tried to add hand with duplicate name to UI. Name:" + handName);
+            return _handLookup[handName];
 
         if (HandsGui != null)
         {
@@ -362,6 +372,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
         RemoveHand(handName, out _);
     }
 
+    [PublicAPI]
     private bool RemoveHand(string handName, out HandButton? handButton)
     {
         if (!_handLookup.TryGetValue(handName, out handButton))
@@ -377,7 +388,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
             _statusHandRight = null;
 
         _handLookup.Remove(handName);
-        handButton.Dispose();
+        handButton.Orphan();
         UpdateVisibleStatusPanels();
         return true;
     }
index e66ce2ae6c563aabe5df04a9e436d6e5e49eb79f..b2160c085a59ad507f3a78eff3d0684ec1c97e95 100644 (file)
@@ -329,9 +329,8 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
         var player = _playerUid;
 
         if (!control.MouseIsHovering ||
-            _playerInventory == null ||
-            !_entities.TryGetComponent<HandsComponent>(player, out var hands) ||
-            hands.ActiveHandEntity is not { } held ||
+            player == null ||
+            !_handsSystem.TryGetActiveItem(player.Value, out var held) ||
             !_entities.TryGetComponent(held, out SpriteComponent? sprite) ||
             !_inventorySystem.TryGetSlotContainer(player.Value, control.SlotName, out var container, out var slotDef))
         {
@@ -342,12 +341,12 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
         // Set green / red overlay at 50% transparency
         var hoverEntity = _entities.SpawnEntity("hoverentity", MapCoordinates.Nullspace);
         var hoverSprite = _entities.GetComponent<SpriteComponent>(hoverEntity);
-        var fits = _inventorySystem.CanEquip(player.Value, held, control.SlotName, out _, slotDef) &&
-                   _container.CanInsert(held, container);
+        var fits = _inventorySystem.CanEquip(player.Value, held.Value, control.SlotName, out _, slotDef) &&
+                   _container.CanInsert(held.Value, container);
 
         if (!fits && _entities.TryGetComponent<StorageComponent>(container.ContainedEntity, out var storage))
         {
-            fits = _entities.System<StorageSystem>().CanInsert(container.ContainedEntity.Value, held, out _, storage);
+            fits = _entities.System<StorageSystem>().CanInsert(container.ContainedEntity.Value, held.Value, out _, storage);
         }
         else if (!fits && _entities.TryGetComponent<ItemSlotsComponent>(container.ContainedEntity, out var itemSlots))
         {
@@ -357,14 +356,14 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
                 if (!slot.InsertOnInteract)
                     continue;
 
-                if (!itemSlotsSys.CanInsert(container.ContainedEntity.Value, held, null, slot))
+                if (!itemSlotsSys.CanInsert(container.ContainedEntity.Value, held.Value, null, slot))
                     continue;
                 fits = true;
                 break;
             }
         }
 
-        _sprite.CopySprite((held, sprite), (hoverEntity, hoverSprite));
+        _sprite.CopySprite((held.Value, sprite), (hoverEntity, hoverSprite));
         _sprite.SetColor((hoverEntity, hoverSprite), fits ? new Color(0, 255, 0, 127) : new Color(255, 0, 0, 127));
 
         control.HoverSpriteView.SetEntity(hoverEntity);
index 76c1a0367c7cdeedf636de4797d24805a544c67f..26f14d73230a34e9b6fa3db5607dc233e3f9dcfc 100644 (file)
@@ -25,7 +25,7 @@ public sealed class RetractableItemActionTest : InteractionTest
         await Server.WaitAssertion(() =>
         {
             // Make sure the player's hand starts empty
-            var heldItem = Hands.ActiveHandEntity;
+            var heldItem = handsSystem.GetActiveItem((playerUid, Hands));
             Assert.That(heldItem, Is.Null, $"Player is holding an item ({SEntMan.ToPrettyString(heldItem)}) at start of test.");
 
             // Inspect the action prototype to find the item it spawns
@@ -43,14 +43,14 @@ public sealed class RetractableItemActionTest : InteractionTest
             var actionEnt = actionsSystem.GetAction(actionUid);
 
             // Make sure the player's hand is still empty
-            heldItem = Hands.ActiveHandEntity;
+            heldItem = handsSystem.GetActiveItem((playerUid, Hands));
             Assert.That(heldItem, Is.Null, $"Player is holding an item ({SEntMan.ToPrettyString(heldItem)}) after adding action.");
 
             // Activate the arm blade
             actionsSystem.PerformAction(ToServer(Player), actionEnt!.Value);
 
             // Make sure the player is now holding the expected item
-            heldItem = Hands.ActiveHandEntity;
+            heldItem = handsSystem.GetActiveItem((playerUid, Hands));
             Assert.That(heldItem, Is.Not.Null, $"Expected player to be holding {spawnedProtoId} but was holding nothing.");
             AssertPrototype(spawnedProtoId, SEntMan.GetNetEntity(heldItem));
 
@@ -58,7 +58,7 @@ public sealed class RetractableItemActionTest : InteractionTest
             actionsSystem.PerformAction(ToServer(Player), actionEnt.Value);
 
             // Make sure the player's hand is empty again
-            heldItem = Hands.ActiveHandEntity;
+            heldItem = handsSystem.GetActiveItem((playerUid, Hands));
             Assert.That(heldItem, Is.Null, $"Player is still holding an item ({SEntMan.ToPrettyString(heldItem)}) after second use.");
         });
     }
index 1b31fe38c2899dae8f94a93166637153641cf22e..d6dec6fe151d142bf5034cc39ff2d7c7faa7ea49 100644 (file)
@@ -293,9 +293,9 @@ namespace Content.IntegrationTests.Tests.Buckle
                 Assert.That(buckle.Buckled);
 
                 // With items in all hands
-                foreach (var hand in hands.Hands.Values)
+                foreach (var hand in hands.Hands.Keys)
                 {
-                    Assert.That(hand.HeldEntity, Is.Not.Null);
+                    Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Not.Null);
                 }
 
                 var bodySystem = entityManager.System<BodySystem>();
@@ -316,9 +316,9 @@ namespace Content.IntegrationTests.Tests.Buckle
                 Assert.That(buckle.Buckled);
 
                 // Now with no item in any hand
-                foreach (var hand in hands.Hands.Values)
+                foreach (var hand in hands.Hands.Keys)
                 {
-                    Assert.That(hand.HeldEntity, Is.Null);
+                    Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Null);
                 }
 
                 buckleSystem.Unbuckle(human, human);
index 52b7e555a9d35720c08ab5a7e064b8e16b7bd4ef..4c5860f235abbf1d2f491e37076dbdf048a30da5 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Client.Chemistry.UI;
 using Content.IntegrationTests.Tests.Interaction;
 using Content.Shared.Chemistry;
-using Content.Server.Chemistry.Components;
 using Content.Shared.Containers.ItemSlots;
 
 namespace Content.IntegrationTests.Tests.Chemistry;
@@ -19,7 +18,7 @@ public sealed class DispenserTest : InteractionTest
 
         // Insert beaker
         await InteractUsing("Beaker");
-        Assert.That(Hands.ActiveHandEntity, Is.Null);
+        Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
 
         // Open BUI
         await Interact();
@@ -29,18 +28,18 @@ public sealed class DispenserTest : InteractionTest
         await SendBui(ReagentDispenserUiKey.Key, ev);
 
         // Beaker is back in the player's hands
-        Assert.That(Hands.ActiveHandEntity, Is.Not.Null);
-        AssertPrototype("Beaker", SEntMan.GetNetEntity(Hands.ActiveHandEntity));
+        Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Not.Null);
+        AssertPrototype("Beaker", SEntMan.GetNetEntity(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands))));
 
         // Re-insert the beaker
         await Interact();
-        Assert.That(Hands.ActiveHandEntity, Is.Null);
+        Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
 
         // Re-eject using the button directly instead of sending a BUI event. This test is really just a test of the
         // bui/window helper methods.
         await ClickControl<ReagentDispenserWindow>(nameof(ReagentDispenserWindow.EjectButton));
         await RunTicks(5);
-        Assert.That(Hands.ActiveHandEntity, Is.Not.Null);
-        AssertPrototype("Beaker", SEntMan.GetNetEntity(Hands.ActiveHandEntity));
+        Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Not.Null);
+        AssertPrototype("Beaker", SEntMan.GetNetEntity(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands))));
     }
 }
index 94dd98425baf4662d0e03bed2d48ebbac2e97c6b..b53b87dd5c1c7713c716479a360f263f13ce725b 100644 (file)
@@ -267,7 +267,7 @@ public sealed class SuicideCommandTests
         await server.WaitPost(() =>
         {
             var item = entManager.SpawnEntity("SharpTestObject", transformSystem.GetMapCoordinates(player));
-            Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
+            Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHandId!));
             entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
             Assert.That(executionComponent, Is.Not.EqualTo(null));
         });
@@ -342,7 +342,7 @@ public sealed class SuicideCommandTests
         await server.WaitPost(() =>
         {
             var item = entManager.SpawnEntity("MixedDamageTestObject", transformSystem.GetMapCoordinates(player));
-            Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
+            Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHandId!));
             entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
             Assert.That(executionComponent, Is.Not.EqualTo(null));
         });
index 292bf0c55abea580308d14feb38b16caa010cc8a..b94bda3d95a65d72e07bbb0a0d9186e1e0ef5c44 100644 (file)
@@ -13,10 +13,10 @@ public sealed class WallConstruction : InteractionTest
     {
         await StartConstruction(Wall);
         await InteractUsing(Steel, 2);
-        Assert.That(Hands.ActiveHandEntity, Is.Null);
+        Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
         ClientAssertPrototype(Girder, Target);
         await InteractUsing(Steel, 2);
-        Assert.That(Hands.ActiveHandEntity, Is.Null);
+        Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
         AssertPrototype(WallSolid);
     }
 
index b88f2dc9eb991cc408dfd8db20012515f84203ea..d5cf75c463446bf59ef0c48958f656db965afccb 100644 (file)
@@ -53,20 +53,20 @@ public sealed class HandTests
             var xform = entMan.GetComponent<TransformComponent>(player);
             item = entMan.SpawnEntity("Crowbar", tSys.GetMapCoordinates(player, xform: xform));
             hands = entMan.GetComponent<HandsComponent>(player);
-            sys.TryPickup(player, item, hands.ActiveHand!);
+            sys.TryPickup(player, item, hands.ActiveHandId!);
         });
 
         // run ticks here is important, as errors may happen within the container system's frame update methods.
         await pair.RunTicksSync(5);
-        Assert.That(hands.ActiveHandEntity, Is.EqualTo(item));
+        Assert.That(sys.GetActiveItem((player, hands)), Is.EqualTo(item));
 
         await server.WaitPost(() =>
         {
-            sys.TryDrop(player, item, null!);
+            sys.TryDrop(player, item);
         });
 
         await pair.RunTicksSync(5);
-        Assert.That(hands.ActiveHandEntity, Is.Null);
+        Assert.That(sys.GetActiveItem((player, hands)), Is.Null);
 
         await server.WaitPost(() => mapSystem.DeleteMap(data.MapId));
         await pair.CleanReturnAsync();
@@ -105,10 +105,10 @@ public sealed class HandTests
             player = playerMan.Sessions.First().AttachedEntity!.Value;
             tSys.PlaceNextTo(player, item);
             hands = entMan.GetComponent<HandsComponent>(player);
-            sys.TryPickup(player, item, hands.ActiveHand!);
+            sys.TryPickup(player, item, hands.ActiveHandId!);
         });
         await pair.RunTicksSync(5);
-        Assert.That(hands.ActiveHandEntity, Is.EqualTo(item));
+        Assert.That(sys.GetActiveItem((player, hands)), Is.EqualTo(item));
 
         // Open then close the box to place the player, who is holding the crowbar, inside of it
         var storage = server.System<EntityStorageSystem>();
@@ -125,12 +125,12 @@ public sealed class HandTests
         // with the item not being in the player's hands
         await server.WaitPost(() =>
         {
-            sys.TryDrop(player, item, null!);
+            sys.TryDrop(player, item);
         });
         await pair.RunTicksSync(5);
         var xform = entMan.GetComponent<TransformComponent>(player);
         var itemXform = entMan.GetComponent<TransformComponent>(item);
-        Assert.That(hands.ActiveHandEntity, Is.Not.EqualTo(item));
+        Assert.That(sys.GetActiveItem((player, hands)), Is.Not.EqualTo(item));
         Assert.That(containerSystem.IsInSameOrNoContainer((player, xform), (item, itemXform)));
 
         await server.WaitPost(() => mapSystem.DeleteMap(map.MapId));
index 0f9e9b5ebe804d44a6417ef16a38d03f69b4464e..3302b1bafc53bd3d231b9fb24ac11908c6da782d 100644 (file)
@@ -120,18 +120,18 @@ public abstract partial class InteractionTest
     /// </summary>
     protected async Task DeleteHeldEntity()
     {
-        if (Hands.ActiveHandEntity is { } held)
+        if (HandSys.GetActiveItem((ToServer(Player), Hands)) is { } held)
         {
             await Server.WaitPost(() =>
             {
-                Assert.That(HandSys.TryDrop(SEntMan.GetEntity(Player), null, false, true, Hands));
+                Assert.That(HandSys.TryDrop((SEntMan.GetEntity(Player), Hands), null, false, true));
                 SEntMan.DeleteEntity(held);
                 SLogger.Debug($"Deleting held entity");
             });
         }
 
         await RunTicks(1);
-        Assert.That(Hands.ActiveHandEntity, Is.Null);
+        Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.Null);
     }
 
     /// <summary>
@@ -152,7 +152,7 @@ public abstract partial class InteractionTest
     /// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
     protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableToggleable = true)
     {
-        if (Hands.ActiveHand == null)
+        if (Hands.ActiveHandId == null)
         {
             Assert.Fail("No active hand");
             return default;
@@ -169,7 +169,7 @@ public abstract partial class InteractionTest
         {
             var playerEnt = SEntMan.GetEntity(Player);
 
-            Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHand, false, false, Hands));
+            Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHandId, false, false, false, Hands));
 
             // turn on welders
             if (enableToggleable && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
@@ -179,7 +179,7 @@ public abstract partial class InteractionTest
         });
 
         await RunTicks(1);
-        Assert.That(Hands.ActiveHandEntity, Is.EqualTo(item));
+        Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.EqualTo(item));
         if (enableToggleable && itemToggle != null)
             Assert.That(itemToggle.Activated);
 
@@ -193,7 +193,7 @@ public abstract partial class InteractionTest
     {
         entity ??= Target;
 
-        if (Hands.ActiveHand == null)
+        if (Hands.ActiveHandId == null)
         {
             Assert.Fail("No active hand");
             return;
@@ -212,11 +212,11 @@ public abstract partial class InteractionTest
 
         await Server.WaitPost(() =>
         {
-            Assert.That(HandSys.TryPickup(SEntMan.GetEntity(Player), uid.Value, Hands.ActiveHand, false, false, Hands, item));
+            Assert.That(HandSys.TryPickup(ToServer(Player), uid.Value, Hands.ActiveHandId, false, false, false, Hands, item));
         });
 
         await RunTicks(1);
-        Assert.That(Hands.ActiveHandEntity, Is.EqualTo(uid));
+        Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.EqualTo(uid));
     }
 
     /// <summary>
@@ -224,7 +224,7 @@ public abstract partial class InteractionTest
     /// </summary>
     protected async Task Drop()
     {
-        if (Hands.ActiveHandEntity == null)
+        if (HandSys.GetActiveItem((ToServer(Player), Hands)) == null)
         {
             Assert.Fail("Not holding any entity to drop");
             return;
@@ -232,11 +232,11 @@ public abstract partial class InteractionTest
 
         await Server.WaitPost(() =>
         {
-            Assert.That(HandSys.TryDrop(SEntMan.GetEntity(Player), handsComp: Hands));
+            Assert.That(HandSys.TryDrop((ToServer(Player), Hands)));
         });
 
         await RunTicks(1);
-        Assert.That(Hands.ActiveHandEntity, Is.Null);
+        Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.Null);
     }
 
     #region Interact
@@ -246,7 +246,7 @@ public abstract partial class InteractionTest
     /// </summary>
     protected async Task UseInHand()
     {
-        if (Hands.ActiveHandEntity is not { } target)
+        if (HandSys.GetActiveItem((ToServer(Player), Hands)) is not { } target)
         {
             Assert.Fail("Not holding any entity");
             return;
index 47a1f748ebd08623c221c7e9d2761c186d1af9ad..79756ea5b419445f128f2042c519f87d45ff82ed 100644 (file)
@@ -1,15 +1,12 @@
 #nullable enable
-using System.Linq;
 using System.Numerics;
 using Content.Client.Construction;
 using Content.Client.Examine;
 using Content.Client.Gameplay;
 using Content.IntegrationTests.Pair;
-using Content.Server.Body.Systems;
 using Content.Server.Hands.Systems;
 using Content.Server.Stack;
 using Content.Server.Tools;
-using Content.Shared.Body.Part;
 using Content.Shared.DoAfter;
 using Content.Shared.Hands.Components;
 using Content.Shared.Interaction;
@@ -135,10 +132,13 @@ public abstract partial class InteractionTest
 - type: entity
   id: InteractionTestMob
   components:
-  - type: Body
-    prototype: Aghost
   - type: DoAfter
   - type: Hands
+    hands:
+      hand_right: # only one hand, so that they do not accidentally pick up deconstruction products
+        location: Right
+    sortedHands:
+    - hand_right
   - type: ComplexInteraction
   - type: MindContainer
   - type: Stripping
@@ -230,20 +230,6 @@ public abstract partial class InteractionTest
                 SEntMan.DeleteEntity(old.Value);
         });
 
-        // Ensure that the player only has one hand, so that they do not accidentally pick up deconstruction products
-        await Server.WaitPost(() =>
-        {
-            // I lost an hour of my life trying to track down how the hell interaction tests were breaking
-            // so greatz to this. Just make your own body prototype!
-            var bodySystem = SEntMan.System<BodySystem>();
-            var hands = bodySystem.GetBodyChildrenOfType(SEntMan.GetEntity(Player), BodyPartType.Hand).ToArray();
-
-            for (var i = 1; i < hands.Length; i++)
-            {
-                SEntMan.DeleteEntity(hands[i].Id);
-            }
-        });
-
         // Change UI state to in-game.
         var state = Client.ResolveDependency<IStateManager>();
         await Client.WaitPost(() => state.RequestStateChange<GameplayState>());
index eef420df2043d3e2e2a7f1dbbc7871e93e85fc01..0827e11b705804791ed186dfdb02bfa06023663a 100644 (file)
@@ -17,7 +17,7 @@ public sealed class TileConstructionTests : InteractionTest
         await SetTile(null);
         await InteractUsing(Rod);
         await AssertTile(Lattice);
-        Assert.That(Hands.ActiveHandEntity, Is.Null);
+        Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
         await InteractUsing(Cut);
         await AssertTile(null);
         await AssertEntityLookup((Rod, 1));
@@ -49,7 +49,7 @@ public sealed class TileConstructionTests : InteractionTest
         AssertGridCount(1);
 
         // Cut lattice
-        Assert.That(Hands.ActiveHandEntity, Is.Null);
+        Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
         await InteractUsing(Cut);
         await AssertTile(null);
         AssertGridCount(0);
@@ -83,13 +83,13 @@ public sealed class TileConstructionTests : InteractionTest
 
         // Lattice -> Plating
         await InteractUsing(FloorItem);
-        Assert.That(Hands.ActiveHandEntity, Is.Null);
+        Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
         await AssertTile(Plating);
         AssertGridCount(1);
 
         // Plating -> Tile
         await InteractUsing(FloorItem);
-        Assert.That(Hands.ActiveHandEntity, Is.Null);
+        Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
         await AssertTile(Floor);
         AssertGridCount(1);
 
index ee3e59559580eb5f74dc37e5181d847d690e3b26..90fcde136d8425bfdfc8ad530802731bc2574fdb 100644 (file)
@@ -42,13 +42,12 @@ public sealed class StripAllCommand : LocalizedEntityCommands
 
         if (EntityManager.TryGetComponent<HandsComponent>(targetEntity, out var hands))
         {
-            foreach (var hand in _handsSystem.EnumerateHands(targetEntity.Value, hands))
+            foreach (var hand in _handsSystem.EnumerateHands((targetEntity.Value, hands)))
             {
-                _handsSystem.TryDrop(targetEntity.Value,
+                _handsSystem.TryDrop((targetEntity.Value, hands),
                     hand,
                     checkActionBlocker: false,
-                    doDropInteraction: false,
-                    handsComp: hands);
+                    doDropInteraction: false);
             }
         }
     }
index b9916bc5b255cc6c4093009f26f0b84b342ee1f2..0e5138ba9680d844012219379a00974dc3c0765f 100644 (file)
@@ -433,9 +433,9 @@ public sealed class AdminSystem : EntitySystem
 
             if (TryComp(entity, out HandsComponent? hands))
             {
-                foreach (var hand in _hands.EnumerateHands(entity, hands))
+                foreach (var hand in _hands.EnumerateHands((entity, hands)))
                 {
-                    _hands.TryDrop(entity, hand, checkActionBlocker: false, doDropInteraction: false, handsComp: hands);
+                    _hands.TryDrop((entity, hands), hand, checkActionBlocker: false, doDropInteraction: false);
                 }
             }
 
index ccc7a98a1e81788a9f08bf291085bdb945bd438a..22a1ccadc92904d50cd0813bfe68f37c90165783 100644 (file)
@@ -820,7 +820,7 @@ public sealed partial class AdminVerbSystem
         }
         else if (TryComp<HandsComponent>(target, out var hands))
         {
-            foreach (var held in _handsSystem.EnumerateHeld(target, hands))
+            foreach (var held in _handsSystem.EnumerateHeld((target, hands)))
             {
                 if (HasComp<AccessComponent>(held))
                 {
index 21f43c31956db11b38518afc882dc9940e460b16..7cadc3bb8f7515b0fe41f4013efff985eee92494 100644 (file)
@@ -97,8 +97,7 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
         EntityUid? entity = null;
         if (args.Type == CryostorageRemoveItemBuiMessage.RemovalType.Hand)
         {
-            if (_hands.TryGetHand(cryoContained, args.Key, out var hand))
-                entity = hand.HeldEntity;
+            entity = _hands.GetHeldItem(cryoContained, args.Key);
         }
         else
         {
@@ -320,10 +319,10 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
 
         foreach (var hand in _hands.EnumerateHands(uid))
         {
-            if (hand.HeldEntity == null)
+            if (!_hands.TryGetHeldItem(uid, hand, out var heldEntity))
                 continue;
 
-            data.HeldItems.Add(hand.Name, Name(hand.HeldEntity.Value));
+            data.HeldItems.Add(hand, Name(heldEntity.Value));
         }
 
         return data;
index 0686f3209729c8047c0a0c9d97bea44b40fd01f4..d8381b2a790cf6ec5f9d62959773e04b8f7e2e70 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.Atmos.EntitySystems;
 using Content.Server.Botany.Components;
+using Content.Server.Hands.Systems;
 using Content.Server.Kitchen.Components;
 using Content.Server.Popups;
 using Content.Shared.Chemistry.EntitySystems;
@@ -37,6 +38,7 @@ public sealed class PlantHolderSystem : EntitySystem
     [Dependency] private readonly MutationSystem _mutation = default!;
     [Dependency] private readonly AppearanceSystem _appearance = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly HandsSystem _hands = default!;
     [Dependency] private readonly PopupSystem _popup = default!;
     [Dependency] private readonly IGameTiming _gameTiming = default!;
     [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
@@ -45,7 +47,7 @@ public sealed class PlantHolderSystem : EntitySystem
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-    
+
     public const float HydroponicsSpeedMultiplier = 1f;
     public const float HydroponicsConsumptionMultiplier = 2f;
 
@@ -706,9 +708,9 @@ public sealed class PlantHolderSystem : EntitySystem
 
         if (component.Harvest && !component.Dead)
         {
-            if (TryComp<HandsComponent>(user, out var hands))
+            if (_hands.TryGetActiveItem(user, out var activeItem))
             {
-                if (!_botany.CanHarvest(component.Seed, hands.ActiveHandEntity))
+                if (!_botany.CanHarvest(component.Seed, activeItem))
                 {
                     _popup.PopupCursor(Loc.GetString("plant-holder-component-ligneous-cant-harvest-message"), user);
                     return false;
index b9a3ede2bd7ef7157e8561f07aef8b0992c02144..8727429fc98e24a6368e322eece0811c9a436acf 100644 (file)
@@ -1,9 +1,9 @@
 using Content.Server.Ghost;
+using Content.Server.Hands.Systems;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Chat;
 using Content.Shared.Damage;
 using Content.Shared.Database;
-using Content.Shared.Hands.Components;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Item;
@@ -22,6 +22,7 @@ public sealed class SuicideSystem : EntitySystem
 {
     [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly HandsSystem _hands = default!;
     [Dependency] private readonly TagSystem _tagSystem = default!;
     [Dependency] private readonly MobStateSystem _mobState = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -116,10 +117,9 @@ public sealed class SuicideSystem : EntitySystem
         var suicideByEnvironmentEvent = new SuicideByEnvironmentEvent(victim);
 
         // Try to suicide by raising an event on the held item
-        if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent)
-            && handsComponent.ActiveHandEntity is { } item)
+        if (_hands.TryGetActiveItem(victim.Owner, out var item))
         {
-            RaiseLocalEvent(item, suicideByEnvironmentEvent);
+            RaiseLocalEvent(item.Value, suicideByEnvironmentEvent);
             if (suicideByEnvironmentEvent.Handled)
             {
                 args.Handled = suicideByEnvironmentEvent.Handled;
index e489cdf1007519b2d1c67bd0189ffcc88b45a50f..767399335ae15534306e2a285b3081eb133d0d45 100644 (file)
@@ -471,7 +471,7 @@ namespace Content.Server.Construction
             }
 
             if (!_actionBlocker.CanInteract(user, null)
-                || !EntityManager.TryGetComponent(user, out HandsComponent? hands) || hands.ActiveHandEntity == null)
+                || !EntityManager.TryGetComponent(user, out HandsComponent? hands) || _handsSystem.GetActiveItem((user, hands)) == null)
             {
                 Cleanup();
                 return;
@@ -496,7 +496,7 @@ namespace Content.Server.Construction
 
             var valid = false;
 
-            if (hands.ActiveHandEntity is not {Valid: true} holding)
+            if (_handsSystem.GetActiveItem((user, hands)) is not {Valid: true} holding)
             {
                 Cleanup();
                 return;
index 357046be56ce41d93caf7b0cc55c6114b9bf3204..1ccb80b32e3b6bac6ec416eb806b40b01bf935e9 100644 (file)
@@ -1,5 +1,4 @@
 using System.Numerics;
-using Content.Server.Inventory;
 using Content.Server.Stack;
 using Content.Server.Stunnable;
 using Content.Shared.ActionBlocker;
@@ -10,9 +9,7 @@ using Content.Shared.Explosion;
 using Content.Shared.Hands.Components;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Input;
-using Content.Shared.Inventory.VirtualItem;
 using Content.Shared.Movement.Pulling.Components;
-using Content.Shared.Movement.Pulling.Events;
 using Content.Shared.Movement.Pulling.Systems;
 using Content.Shared.Stacks;
 using Content.Shared.Standing;
@@ -24,7 +21,6 @@ using Robust.Shared.Physics.Components;
 using Robust.Shared.Player;
 using Robust.Shared.Random;
 using Robust.Shared.Timing;
-using Robust.Shared.Utility;
 
 namespace Content.Server.Hands.Systems
 {
@@ -87,10 +83,9 @@ namespace Content.Server.Hands.Systems
             if (ent.Comp.DisableExplosionRecursion)
                 return;
 
-            foreach (var hand in ent.Comp.Hands.Values)
+            foreach (var held in EnumerateHeld(ent.AsNullable()))
             {
-                if (hand.HeldEntity is { } uid)
-                    args.Contents.Add(uid);
+                args.Contents.Add(held);
             }
         }
 
@@ -112,7 +107,7 @@ namespace Content.Server.Hands.Systems
             args.Handled = true; // no shove/stun.
         }
 
-        private void HandleBodyPartAdded(EntityUid uid, HandsComponent component, ref BodyPartAddedEvent args)
+        private void HandleBodyPartAdded(Entity<HandsComponent> ent, ref BodyPartAddedEvent args)
         {
             if (args.Part.Comp.PartType != BodyPartType.Hand)
                 return;
@@ -127,7 +122,7 @@ namespace Content.Server.Hands.Systems
                 _ => throw new ArgumentOutOfRangeException(nameof(args.Part.Comp.Symmetry))
             };
 
-            AddHand(uid, args.Slot, location);
+            AddHand(ent.AsNullable(), args.Slot, location);
         }
 
         private void HandleBodyPartRemoved(EntityUid uid, HandsComponent component, ref BodyPartRemovedEvent args)
@@ -155,8 +150,8 @@ namespace Content.Server.Hands.Systems
         {
             if (ContainerSystem.IsEntityInContainer(player) ||
                 !TryComp(player, out HandsComponent? hands) ||
-                hands.ActiveHandEntity is not { } throwEnt ||
-                !_actionBlockerSystem.CanThrow(player, throwEnt))
+                !TryGetActiveItem((player, hands), out var throwEnt) ||
+                !_actionBlockerSystem.CanThrow(player, throwEnt.Value))
                 return false;
 
             if (_timing.CurTime < hands.NextThrowTime)
@@ -165,7 +160,7 @@ namespace Content.Server.Hands.Systems
 
             if (EntityManager.TryGetComponent(throwEnt, out StackComponent? stack) && stack.Count > 1 && stack.ThrowIndividually)
             {
-                var splitStack = _stackSystem.Split(throwEnt, 1, EntityManager.GetComponent<TransformComponent>(player).Coordinates, stack);
+                var splitStack = _stackSystem.Split(throwEnt.Value, 1, EntityManager.GetComponent<TransformComponent>(player).Coordinates, stack);
 
                 if (splitStack is not {Valid: true})
                     return false;
@@ -185,14 +180,14 @@ namespace Content.Server.Hands.Systems
 
             // Let other systems change the thrown entity (useful for virtual items)
             // or the throw strength.
-            var ev = new BeforeThrowEvent(throwEnt, direction, throwSpeed, player);
+            var ev = new BeforeThrowEvent(throwEnt.Value, direction, throwSpeed, player);
             RaiseLocalEvent(player, ref ev);
 
             if (ev.Cancelled)
                 return true;
 
             // This can grief the above event so we raise it afterwards
-            if (IsHolding(player, throwEnt, out _, hands) && !TryDrop(player, throwEnt, handsComp: hands))
+            if (IsHolding((player, hands), throwEnt, out _) && !TryDrop(player, throwEnt.Value))
                 return false;
 
             _throwingSystem.TryThrow(ev.ItemUid, ev.Direction, ev.ThrowSpeed, ev.PlayerUid, compensateFriction: !HasComp<LandAtCursorComponent>(ev.ItemUid));
@@ -207,20 +202,20 @@ namespace Content.Server.Hands.Systems
             var spreadMaxAngle = Angle.FromDegrees(DropHeldItemsSpread);
 
             var fellEvent = new FellDownEvent(entity);
-            RaiseLocalEvent(entity, fellEvent, false);
+            RaiseLocalEvent(entity, fellEvent);
 
-            foreach (var hand in entity.Comp.Hands.Values)
+            foreach (var hand in entity.Comp.Hands.Keys)
             {
-                if (hand.HeldEntity is not EntityUid held)
+                if (!TryGetHeldItem(entity.AsNullable(), hand, out var heldEntity))
                     continue;
 
                 var throwAttempt = new FellDownThrowAttemptEvent(entity);
-                RaiseLocalEvent(hand.HeldEntity.Value, ref throwAttempt);
+                RaiseLocalEvent(heldEntity.Value, ref throwAttempt);
 
                 if (throwAttempt.Cancelled)
                     continue;
 
-                if (!TryDrop(entity, hand, null, checkActionBlocker: false, handsComp: entity.Comp))
+                if (!TryDrop(entity.AsNullable(), hand, checkActionBlocker: false))
                     continue;
 
                 // Rotate the item's throw vector a bit for each item
@@ -231,12 +226,12 @@ namespace Content.Server.Hands.Systems
                 itemVelocity *= _random.NextFloat(1f);
                 // Heavier objects don't get thrown as far
                 // If the item doesn't have a physics component, it isn't going to get thrown anyway, but we'll assume infinite mass
-                itemVelocity *= _physicsQuery.TryComp(held, out var heldPhysics) ? heldPhysics.InvMass : 0;
+                itemVelocity *= _physicsQuery.TryComp(heldEntity, out var heldPhysics) ? heldPhysics.InvMass : 0;
                 // Throw at half the holder's intentional throw speed and
                 // vary the speed a little to make it look more interesting
                 var throwSpeed = entity.Comp.BaseThrowspeed * _random.NextFloat(0.45f, 0.55f);
 
-                _throwingSystem.TryThrow(held,
+                _throwingSystem.TryThrow(heldEntity.Value,
                     itemVelocity,
                     throwSpeed,
                     entity,
index 115a7b6cb7664e0c0040af68b9fece924f3913cd..8ca33fb8cd12d6eeb1d10085b5f271e068deb0af 100644 (file)
@@ -43,7 +43,7 @@ public sealed class HotPotatoSystem : SharedHotPotatoSystem
             if (!TryComp<HandsComponent>(hitEntity, out var hands))
                 continue;
 
-            if (!_hands.IsHolding(hitEntity, uid, out _, hands) && _hands.TryForcePickupAnyHand(hitEntity, uid, handsComp: hands))
+            if (!_hands.IsHolding((hitEntity, hands), uid, out _) && _hands.TryForcePickupAnyHand(hitEntity, uid, handsComp: hands))
             {
                 _popup.PopupEntity(Loc.GetString("hot-potato-passed",
                     ("from", args.User), ("to", hitEntity)), uid, PopupType.Medium);
index e2e65684ff67843bca72ccc325acfeff95aef923..93f7679766fb265ea7a16d9b79aab1f0c1cecac9 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Shared.Hands.Components;
+using Content.Server.Hands.Systems;
 using Robust.Shared.Prototypes;
 
 namespace Content.Server.NPC.HTN.Preconditions;
@@ -18,14 +18,18 @@ public sealed partial class ActiveHandComponentPrecondition : HTNPrecondition
 
     public override bool IsMet(NPCBlackboard blackboard)
     {
-        if (!blackboard.TryGetValue<Hand>(NPCBlackboard.ActiveHand, out var hand, _entManager) || hand.HeldEntity == null)
+        if (!blackboard.TryGetValue<EntityUid>(NPCBlackboard.Owner, out var owner, _entManager) ||
+            !blackboard.TryGetValue<string>(NPCBlackboard.ActiveHand, out var hand, _entManager))
         {
             return Invert;
         }
 
+        if (!_entManager.System<HandsSystem>().TryGetHeldItem(owner, hand, out var entity))
+            return Invert;
+
         foreach (var comp in Components)
         {
-            var hasComp = _entManager.HasComponent(hand.HeldEntity, comp.Value.Component.GetType());
+            var hasComp = _entManager.HasComponent(entity, comp.Value.Component.GetType());
 
             if (!hasComp ||
                 Invert && hasComp)
index 0e6c63a433d89de51dff806fb57dd07322e328fe..049d81130a07e4eb634b05bfcc7bc7707ade0ac6 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Shared.Hands.Components;
+using Content.Server.Hands.Systems;
 
 namespace Content.Server.NPC.HTN.Preconditions;
 
@@ -11,11 +11,12 @@ public sealed partial class ActiveHandEntityPrecondition : HTNPrecondition
 
     public override bool IsMet(NPCBlackboard blackboard)
     {
-        if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out Hand? activeHand, _entManager))
+        if (!blackboard.TryGetValue(NPCBlackboard.Owner, out EntityUid owner, _entManager) ||
+            !blackboard.TryGetValue(NPCBlackboard.ActiveHand, out string? activeHand, _entManager))
         {
             return false;
         }
 
-        return activeHand.HeldEntity != null;
+        return !_entManager.System<HandsSystem>().HandIsEmpty(owner, activeHand);
     }
 }
index 0ea062a9100c578862c765d28eef558bc58bfb7c..89d68283b6d8f7745034acb9b71d3e805fc81e8a 100644 (file)
@@ -12,7 +12,7 @@ public sealed partial class DropOperator : HTNOperator
 
     public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
     {
-        if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out Hand? activeHand, _entManager))
+        if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out string? activeHand, _entManager))
         {
             return HTNOperatorStatus.Finished;
         }
index ffdb55045d358598c2fd78a027663561e9f850ee..8b70569177cf85ac58e64dfe3aac87010476a39d 100644 (file)
@@ -1,11 +1,10 @@
 using System.Collections;
 using System.Diagnostics.CodeAnalysis;
-using Content.Server.Interaction;
+using Content.Server.Hands.Systems;
 using Content.Shared.Access.Systems;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Hands.Components;
 using Content.Shared.Interaction;
-using Content.Shared.Inventory;
 using JetBrains.Annotations;
 using Robust.Shared.Utility;
 
@@ -152,6 +151,8 @@ public sealed partial class NPCBlackboard : IEnumerable<KeyValuePair<string, obj
         value = default;
         EntityUid owner;
 
+        var handSys = entManager.System<HandsSystem>();
+
         switch (key)
         {
             case Access:
@@ -168,25 +169,24 @@ public sealed partial class NPCBlackboard : IEnumerable<KeyValuePair<string, obj
             case ActiveHand:
             {
                 if (!TryGetValue(Owner, out owner, entManager) ||
-                    !entManager.TryGetComponent<HandsComponent>(owner, out var hands) ||
-                    hands.ActiveHand == null)
+                    handSys.GetActiveHand(owner) is not { } activeHand)
                 {
                     return false;
                 }
 
-                value = hands.ActiveHand;
+                value = activeHand;
                 return true;
             }
             case ActiveHandFree:
             {
                 if (!TryGetValue(Owner, out owner, entManager) ||
                     !entManager.TryGetComponent<HandsComponent>(owner, out var hands) ||
-                    hands.ActiveHand == null)
+                    handSys.GetActiveHand(owner) is not { } activeHand)
                 {
                     return false;
                 }
 
-                value = hands.ActiveHand.IsEmpty;
+                value = handSys.HandIsEmpty((owner, hands), activeHand);
                 return true;
             }
             case CanMove:
@@ -204,16 +204,16 @@ public sealed partial class NPCBlackboard : IEnumerable<KeyValuePair<string, obj
             {
                 if (!TryGetValue(Owner, out owner, entManager) ||
                     !entManager.TryGetComponent<HandsComponent>(owner, out var hands) ||
-                    hands.ActiveHand == null)
+                    handSys.GetActiveHand(owner) is null)
                 {
                     return false;
                 }
 
                 var handos = new List<string>();
 
-                foreach (var (id, hand) in hands.Hands)
+                foreach (var id in hands.Hands.Keys)
                 {
-                    if (!hand.IsEmpty)
+                    if (!handSys.HandIsEmpty((owner, hands), id))
                         continue;
 
                     handos.Add(id);
@@ -226,16 +226,16 @@ public sealed partial class NPCBlackboard : IEnumerable<KeyValuePair<string, obj
             {
                 if (!TryGetValue(Owner, out owner, entManager) ||
                     !entManager.TryGetComponent<HandsComponent>(owner, out var hands) ||
-                    hands.ActiveHand == null)
+                    handSys.GetActiveHand(owner) is null)
                 {
                     return false;
                 }
 
                 var handos = new List<string>();
 
-                foreach (var (id, hand) in hands.Hands)
+                foreach (var id in hands.Hands.Keys)
                 {
-                    if (!hand.IsEmpty)
+                    if (!handSys.HandIsEmpty((owner, hands), id))
                         continue;
 
                     handos.Add(id);
index c5b463d0d3a4b7c58f98641869cc763140c06478..489ac6de558f8b160a8d25e513bed5050185060c 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.Atmos.Components;
 using Content.Server.Fluids.EntitySystems;
+using Content.Server.Hands.Systems;
 using Content.Server.NPC.Queries;
 using Content.Server.NPC.Queries.Considerations;
 using Content.Server.NPC.Queries.Curves;
@@ -44,6 +45,7 @@ public sealed class NPCUtilitySystem : EntitySystem
     [Dependency] private readonly DrinkSystem _drink = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
     [Dependency] private readonly FoodSystem _food = default!;
+    [Dependency] private readonly HandsSystem _hands = default!;
     [Dependency] private readonly InventorySystem _inventory = default!;
     [Dependency] private readonly MobStateSystem _mobState = default!;
     [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
@@ -256,8 +258,9 @@ public sealed class NPCUtilitySystem : EntitySystem
             }
             case TargetAmmoMatchesCon:
             {
-                if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out Hand? activeHand, EntityManager) ||
-                    !TryComp<BallisticAmmoProviderComponent>(activeHand.HeldEntity, out var heldGun))
+                if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out string? activeHand, EntityManager) ||
+                    !_hands.TryGetHeldItem(owner, activeHand, out var heldEntity) ||
+                    !TryComp<BallisticAmmoProviderComponent>(heldEntity, out var heldGun))
                 {
                     return 0f;
                 }
index 014dbe6d995ed93fbb159b9da91f5c021569de98..63d45e23644b38ffc3eaaf916557a8af330bb3eb 100644 (file)
@@ -152,7 +152,7 @@ public sealed class FTLDiskCommand : LocalizedCommands
 
                 if (_entManager.TryGetComponent<StorageComponent>(cdCaseUid, out var storage) && storageSystem.Insert(cdCaseUid, cdUid, out _, storageComp: storage, playSound: false))
                 {
-                    if (_entManager.TryGetComponent<HandsComponent>(entity, out var handsComponent) && handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
+                    if (_entManager.TryGetComponent<HandsComponent>(entity, out var handsComponent) && handsSystem.TryGetEmptyHand((entity, handsComponent), out var emptyHand))
                     {
                         handsSystem.TryPickup(entity, cdCaseUid, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
                     }
@@ -161,7 +161,7 @@ public sealed class FTLDiskCommand : LocalizedCommands
                 {
                     _entManager.DeleteEntity(cdCaseUid); // something went wrong so just yeet the chaf
 
-                    if (_entManager.TryGetComponent<HandsComponent>(entity, out var handsComponent) && handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
+                    if (_entManager.TryGetComponent<HandsComponent>(entity, out var handsComponent) && handsSystem.TryGetEmptyHand((entity, handsComponent), out var emptyHand))
                     {
                         handsSystem.TryPickup(entity, cdUid, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
                     }
index 784c2f8fbb9c7ae7df443c8b08645c9dea38418f..1b6040773c48c316a23cc87837af55974599563a 100644 (file)
@@ -217,8 +217,8 @@ public sealed partial class BorgSystem
 
             var handId = $"{uid}-item{component.HandCounter}";
             component.HandCounter++;
-            _hands.AddHand(chassis, handId, HandLocation.Middle, hands);
-            _hands.DoPickup(chassis, hands.Hands[handId], item, hands);
+            _hands.AddHand((chassis, hands), handId, HandLocation.Middle);
+            _hands.DoPickup(chassis, handId, item, hands);
             EnsureComp<UnremoveableComponent>(item);
             component.ProvidedItems.Add(handId, item);
         }
@@ -239,7 +239,7 @@ public sealed partial class BorgSystem
             foreach (var (hand, item) in component.ProvidedItems)
             {
                 QueueDel(item);
-                _hands.RemoveHand(chassis, hand, hands);
+                _hands.RemoveHand(chassis, hand);
             }
             component.ProvidedItems.Clear();
             return;
@@ -252,7 +252,7 @@ public sealed partial class BorgSystem
                 RemComp<UnremoveableComponent>(item);
                 _container.Insert(item, component.ProvidedContainer);
             }
-            _hands.RemoveHand(chassis, handId, hands);
+            _hands.RemoveHand(chassis, handId);
         }
         component.ProvidedItems.Clear();
     }
index 6938372233cb2d6b665bc9b3ee879dd2a20dcdf8..f3f5801b2d671af3aa78d37b1d5433d1b5e4f1af 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.Hands.Systems;
 using Content.Server.Popups;
 using Content.Server.Tabletop.Components;
 using Content.Shared.CCVar;
@@ -22,6 +23,7 @@ namespace Content.Server.Tabletop
     {
         [Dependency] private readonly SharedMapSystem _map = default!;
         [Dependency] private readonly EyeSystem _eye = default!;
+        [Dependency] private readonly HandsSystem _hands = default!;
         [Dependency] private readonly ViewSubscriberSystem _viewSubscriberSystem = default!;
         [Dependency] private readonly PopupSystem _popupSystem = default!;
         [Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -84,18 +86,13 @@ namespace Content.Server.Tabletop
             if (component.Session is not { } session)
                 return;
 
-            if (hands.ActiveHand == null)
+            if (!_hands.TryGetActiveItem(uid, out var handEnt))
                 return;
 
-            if (hands.ActiveHand.HeldEntity == null)
-                return;
-
-            var handEnt = hands.ActiveHand.HeldEntity.Value;
-
             if (!TryComp<ItemComponent>(handEnt, out var item))
                 return;
 
-            var meta = MetaData(handEnt);
+            var meta = MetaData(handEnt.Value);
             var protoId = meta.EntityPrototype?.ID;
 
             var hologram = Spawn(protoId, session.Position.Offset(-1, 0));
index b8d1dd935cceb944012dda4701025262a75a00e4..6afe886303349635be48a7f046f0162b96e07aa7 100644 (file)
@@ -90,9 +90,9 @@ public sealed class InnateToolSystem : EntitySystem
 
             if (TryComp<HandsComponent>(uid, out var hands))
             {
-                foreach (var hand in hands.Hands)
+                foreach (var hand in hands.Hands.Keys)
                 {
-                    _sharedHandsSystem.TryDrop(uid, hand.Value, checkActionBlocker: false, handsComp: hands);
+                    _sharedHandsSystem.TryDrop((uid, hands), hand, checkActionBlocker: false);
                 }
             }
         }
index a0ca0a80dfd4be3857bb84841f6bda1bc9a52b2b..7a1e5facd02aba75021079ab15fcdceea4dd396a 100644 (file)
@@ -1,10 +1,10 @@
 using System.Linq;
 using Content.Server.Administration.Managers;
+using Content.Server.Hands.Systems;
 using Content.Server.Popups;
 using Content.Shared.Administration;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Database;
-using Content.Shared.Hands.Components;
 using Content.Shared.Inventory.VirtualItem;
 using Content.Shared.Verbs;
 using Robust.Shared.Utility;
@@ -14,6 +14,7 @@ namespace Content.Server.Verbs
     public sealed class VerbSystem : SharedVerbSystem
     {
         [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+        [Dependency] private readonly HandsSystem _hands = default!;
         [Dependency] private readonly PopupSystem _popupSystem = default!;
         [Dependency] private readonly IAdminManager _adminMgr = default!;
 
@@ -91,8 +92,7 @@ namespace Content.Server.Verbs
         {
             // first get the held item. again.
             EntityUid? holding = null;
-            if (TryComp(user, out HandsComponent? hands) &&
-                hands.ActiveHandEntity is EntityUid heldEntity)
+            if (_hands.GetActiveItem(user) is { } heldEntity)
             {
                 holding = heldEntity;
             }
index c3fde14517504110331b56bcb090ad07e2e3d708..1755c6df0874f5a5b04ca2f5321ed08e832df515 100644 (file)
@@ -2,7 +2,6 @@ using Content.Server.Hands.Systems;
 using Content.Server.Storage.EntitySystems;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Database;
-using Content.Shared.Hands.Components;
 using Content.Shared.Inventory;
 using Content.Shared.Popups;
 using Content.Shared.Storage;
@@ -40,24 +39,20 @@ public sealed class StorageVoiceControlSystem : EntitySystem
         if (!TryComp<StorageComponent>(ent, out var storage))
             return;
 
-        // Get the hands component
-        if (!TryComp<HandsComponent>(args.Source, out var hands))
-            return;
-
         // If the player has something in their hands, try to insert it into the storage
-        if (hands.ActiveHand != null && hands.ActiveHand.HeldEntity.HasValue)
+        if (_hands.TryGetActiveItem(ent.Owner, out var activeItem))
         {
             // Disallow insertion and provide a reason why if the person decides to insert the item into itself
-            if (ent.Owner.Equals(hands.ActiveHand.HeldEntity.Value))
+            if (ent.Owner.Equals(activeItem.Value))
             {
-                _popup.PopupEntity(Loc.GetString("comp-storagevoicecontrol-self-insert", ("entity", hands.ActiveHand.HeldEntity.Value)), ent, args.Source);
+                _popup.PopupEntity(Loc.GetString("comp-storagevoicecontrol-self-insert", ("entity", activeItem.Value)), ent, args.Source);
                 return;
             }
-            if (_storage.CanInsert(ent, hands.ActiveHand.HeldEntity.Value, out var failedReason))
+            if (_storage.CanInsert(ent, activeItem.Value, out var failedReason))
             {
                 // We adminlog before insertion, otherwise the logger will attempt to pull info on an entity that no longer is present and throw an exception
-                _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Source)} inserted {ToPrettyString(hands.ActiveHand.HeldEntity.Value)} into {ToPrettyString(ent)} via voice control");
-                _storage.Insert(ent, hands.ActiveHand.HeldEntity.Value, out _);
+                _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Source)} inserted {ToPrettyString(activeItem.Value)} into {ToPrettyString(ent)} via voice control");
+                _storage.Insert(ent, activeItem.Value, out _);
                 return;
             }
             {
@@ -67,7 +62,7 @@ public sealed class StorageVoiceControlSystem : EntitySystem
                 _popup.PopupEntity(Loc.GetString(failedReason), ent, args.Source);
                 _adminLogger.Add(LogType.Action,
                     LogImpact.Low,
-                    $"{ToPrettyString(args.Source)} failed to insert {ToPrettyString(hands.ActiveHand.HeldEntity.Value)} into {ToPrettyString(ent)} via voice control");
+                    $"{ToPrettyString(args.Source)} failed to insert {ToPrettyString(activeItem.Value)} into {ToPrettyString(ent)} via voice control");
             }
             return;
         }
@@ -80,7 +75,7 @@ public sealed class StorageVoiceControlSystem : EntitySystem
             // E.g "go go s" would give you the screwdriver because "screwdriver" contains "s"
             if (Name(item).Contains(args.MessageWithoutPhrase))
             {
-                ExtractItemFromStorage(ent, item, args.Source, hands);
+                ExtractItemFromStorage(ent, item, args.Source);
                 break;
             }
         }
@@ -92,16 +87,14 @@ public sealed class StorageVoiceControlSystem : EntitySystem
     /// <param name="ent">The entity with the <see cref="StorageVoiceControlComponent"/></param>
     /// <param name="item">The entity to be extracted from the attached storage</param>
     /// <param name="source">The entity wearing the item</param>
-    /// <param name="hands">The <see cref="HandsComponent"/> of the person wearing the item</param>
     private void ExtractItemFromStorage(Entity<StorageVoiceControlComponent> ent,
         EntityUid item,
-        EntityUid source,
-        HandsComponent hands)
+        EntityUid source)
     {
         _container.RemoveEntity(ent, item);
         _adminLogger.Add(LogType.Action,
             LogImpact.Low,
             $"{ToPrettyString(source)} retrieved {ToPrettyString(item)} from {ToPrettyString(ent)} via voice control");
-        _hands.TryPickup(source, item, handsComp: hands);
+        _hands.TryPickup(source, item);
     }
 }
index 7f382e15bbbd44d7241bd3887c0f2b1e5f71ac55..e6a50425b460838f98c360b4b3d94db60ff7afa9 100644 (file)
@@ -3,6 +3,7 @@ using System.Linq;
 using System.Threading;
 using Content.Server.Construction;
 using Content.Server.Construction.Components;
+using Content.Server.Hands.Systems;
 using Content.Server.Power.Components;
 using Content.Shared.DoAfter;
 using Content.Shared.GameTicking;
@@ -24,6 +25,7 @@ public sealed class WiresSystem : SharedWiresSystem
 {
     [Dependency] private readonly IPrototypeManager _protoMan = default!;
     [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly HandsSystem _hands = default!;
     [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
     [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
     [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
@@ -405,19 +407,13 @@ public sealed class WiresSystem : SharedWiresSystem
             return;
         }
 
-        var activeHand = handsComponent.ActiveHand;
-
-        if (activeHand == null)
-            return;
-
-        if (activeHand.HeldEntity == null)
+        if (!_hands.TryGetActiveItem((player, handsComponent), out var heldEntity))
             return;
 
-        var activeHandEntity = activeHand.HeldEntity.Value;
-        if (!EntityManager.TryGetComponent(activeHandEntity, out ToolComponent? tool))
+        if (!EntityManager.TryGetComponent(heldEntity, out ToolComponent? tool))
             return;
 
-        TryDoWireAction(uid, player, activeHandEntity, args.Id, args.Action, component, tool);
+        TryDoWireAction(uid, player, heldEntity.Value, args.Id, args.Action, component, tool);
     }
 
     private void OnDoAfter(EntityUid uid, WiresComponent component, WireDoAfterEvent args)
index dd603cbb0283c6fe8a797106fc849981d5cb4af8..513abb20eef8db9042a95948eb8bf7711eedb274 100644 (file)
@@ -3,7 +3,7 @@ using Content.Shared.Access.Components;
 using Content.Shared.Administration.Logs;
 using Content.Shared.CCVar;
 using Content.Shared.Database;
-using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Inventory;
 using Content.Shared.PDA;
@@ -21,6 +21,7 @@ public abstract class SharedIdCardSystem : EntitySystem
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly SharedAccessSystem _access = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly InventorySystem _inventorySystem = default!;
     [Dependency] private readonly MetaDataSystem _metaSystem = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -83,8 +84,7 @@ public abstract class SharedIdCardSystem : EntitySystem
     public bool TryFindIdCard(EntityUid uid, out Entity<IdCardComponent> idCard)
     {
         // check held item?
-        if (TryComp(uid, out HandsComponent? hands) &&
-            hands.ActiveHandEntity is EntityUid heldItem &&
+        if (_hands.GetActiveItem(uid) is { } heldItem &&
             TryGetIdCard(heldItem, out idCard))
         {
             return true;
index 619adf7918a3f1f96e6fcb4090a80c733ed485ba..6223f8e840ba9e9faa5cbdcecda9af3bd48b6495 100644 (file)
@@ -97,7 +97,7 @@ public sealed partial class BlockingSystem : EntitySystem
         if (!handQuery.TryGetComponent(args.Performer, out var hands))
             return;
 
-        var shields = _handsSystem.EnumerateHeld(args.Performer, hands).ToArray();
+        var shields = _handsSystem.EnumerateHeld((args.Performer, hands)).ToArray();
 
         foreach (var shield in shields)
         {
@@ -277,7 +277,7 @@ public sealed partial class BlockingSystem : EntitySystem
         if (!handQuery.TryGetComponent(user, out var hands))
             return;
 
-        var shields = _handsSystem.EnumerateHeld(user, hands).ToArray();
+        var shields = _handsSystem.EnumerateHeld((user, hands)).ToArray();
 
         foreach (var shield in shields)
         {
index f536beef2bc46410511b964a44baf7c109698ae9..e314db0ddba8295f8d9208adb4c7e1aad44e2374 100644 (file)
@@ -947,12 +947,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
         if (!entity.Comp.HeldOnly)
             return true;
 
-        if (TryComp(examiner, out HandsComponent? handsComp))
-        {
-            return Hands.IsHolding(examiner, entity, out _, handsComp);
-        }
-
-        return true;
+        return Hands.IsHolding(examiner, entity, out _);
     }
 
     private void OnMapInit(Entity<SolutionContainerManagerComponent> entity, ref MapInitEvent args)
index 3b5a880d4635030b2d88ab6d3e3c976c0953a22b..4f5f567e435d52154a4a54b0472cbb458f1687df 100644 (file)
@@ -255,7 +255,7 @@ namespace Content.Shared.Containers.ItemSlots
             }
 
             // Drop the held item onto the floor. Return if the user cannot drop.
-            if (!_handsSystem.TryDrop(args.User, args.Used, handsComp: hands))
+            if (!_handsSystem.TryDrop(args.User, args.Used))
                 return;
 
             slots.Sort(SortEmpty);
@@ -395,17 +395,17 @@ namespace Content.Shared.Containers.ItemSlots
             if (!Resolve(user, ref hands, false))
                 return false;
 
-            if (hands.ActiveHand?.HeldEntity is not { } held)
+            if (!_handsSystem.TryGetActiveItem((uid, hands), out var held))
                 return false;
 
-            if (!CanInsert(uid, held, user, slot))
+            if (!CanInsert(uid, held.Value, user, slot))
                 return false;
 
             // hands.Drop(item) checks CanDrop action blocker
-            if (!_handsSystem.TryDrop(user, hands.ActiveHand))
+            if (!_handsSystem.TryDrop(user, hands.ActiveHandId!))
                 return false;
 
-            Insert(uid, slot, held, user, excludeUserAudio: excludeUserAudio);
+            Insert(uid, slot, held.Value, user, excludeUserAudio: excludeUserAudio);
             return true;
         }
 
@@ -428,16 +428,14 @@ namespace Content.Shared.Containers.ItemSlots
             if (!Resolve(ent, ref ent.Comp, false))
                 return false;
 
-            TryComp(user, out HandsComponent? handsComp);
-
             if (!TryGetAvailableSlot(ent,
                     item,
-                    user == null ? null : (user.Value, handsComp),
+                    user,
                     out var itemSlot,
                     emptyOnly: true))
                 return false;
 
-            if (user != null && !_handsSystem.TryDrop(user.Value, item, handsComp: handsComp))
+            if (user != null && !_handsSystem.TryDrop(user.Value, item))
                 return false;
 
             Insert(ent, itemSlot, item, user, excludeUserAudio: excludeUserAudio);
@@ -466,7 +464,7 @@ namespace Content.Shared.Containers.ItemSlots
                 && Resolve(user, ref user.Comp)
                 && _handsSystem.IsHolding(user, item))
             {
-                if (!_handsSystem.CanDrop(user, item, user.Comp))
+                if (!_handsSystem.CanDrop(user, item))
                     return false;
             }
 
index c55bbdb152d382a5eb2554702d83ea456309f380..c16bbd1e85a7ee84e4f05ae95346690511894d91 100644 (file)
@@ -397,6 +397,10 @@ namespace Content.Shared.Cuffs
         /// </summary>
         private void OnHandCountChanged(Entity<CuffableComponent> ent, ref HandCountChangedEvent message)
         {
+            // TODO: either don't store a container ref, or make it actually nullable.
+            if (ent.Comp.Container == default!)
+                return;
+
             var dirty = false;
             var handCount = CompOrNull<HandsComponent>(ent.Owner)?.Count ?? 0;
 
@@ -431,19 +435,19 @@ namespace Content.Shared.Cuffs
                 return;
 
             var freeHands = 0;
-            foreach (var hand in _hands.EnumerateHands(uid, handsComponent))
+            foreach (var hand in _hands.EnumerateHands((uid, handsComponent)))
             {
-                if (hand.HeldEntity == null)
+                if (!_hands.TryGetHeldItem((uid, handsComponent), hand, out var held))
                 {
                     freeHands++;
                     continue;
                 }
 
                 // Is this entity removable? (it might be an existing handcuff blocker)
-                if (HasComp<UnremoveableComponent>(hand.HeldEntity))
+                if (HasComp<UnremoveableComponent>(held))
                     continue;
 
-                _hands.DoDrop(uid, hand, true, handsComponent);
+                _hands.DoDrop(uid, hand, true);
                 freeHands++;
                 if (freeHands == 2)
                     break;
index 1db24c700def844343d6bb7a0b7c237b4cfdafaa..e92552ef6d48c6a8f634a288ac0cd24db62f4e8b 100644 (file)
@@ -141,7 +141,7 @@ public abstract class SharedDisposalUnitSystem : EntitySystem
             Category = VerbCategory.Insert,
             Act = () =>
             {
-                _handsSystem.TryDropIntoContainer(args.User, args.Using.Value, component.Container, checkActionBlocker: false, args.Hands);
+                _handsSystem.TryDropIntoContainer((args.User, args.Hands), args.Using.Value, component.Container, checkActionBlocker: false);
                 _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):player} inserted {ToPrettyString(args.Using.Value)} into {ToPrettyString(uid)}");
                 AfterInsert(uid, component, args.Using.Value, args.User);
             }
index f9718be52de3ca4293dbdcda422f2367ff001a96..f6eb9ef99692908db526516f236b6e9f27c0266a 100644 (file)
@@ -233,7 +233,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
                     return true;
 
             // If the user changes which hand is active at all, interrupt the do-after
-            if (args.BreakOnHandChange && hands.ActiveHand?.Name != doAfter.InitialHand)
+            if (args.BreakOnHandChange && hands.ActiveHandId != doAfter.InitialHand)
                 return true;
         }
 
index 9765bac912ee814b471dec42c6355acb4e899bf9..1dc1e58be6472b6a3ab4b290caa8ad4114c39b26 100644 (file)
@@ -221,8 +221,8 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
             if (!TryComp(args.User, out HandsComponent? handsComponent))
                 return false;
 
-            doAfter.InitialHand = handsComponent.ActiveHand?.Name;
-            doAfter.InitialItem = handsComponent.ActiveHandEntity;
+            doAfter.InitialHand = handsComponent.ActiveHandId;
+            doAfter.InitialItem = _hands.GetActiveItem((args.User, handsComponent));
         }
 
         doAfter.NetInitialItem = GetNetEntity(doAfter.InitialItem);
index a83518dfa338c218b37275e85cbc343b598c7776..cac73f64281e6f7ef60af4e82bbdaa22fe518239 100644 (file)
@@ -70,7 +70,7 @@ public sealed class DeployFoldableSystem : EntitySystem
         }
 
         if (!TryComp(args.User, out HandsComponent? hands)
-            || !_hands.TryDrop(args.User, args.Used, targetDropLocation: args.ClickLocation, handsComp: hands))
+            || !_hands.TryDrop((args.User, hands), args.Used, targetDropLocation: args.ClickLocation))
             return;
 
         if (!_foldable.TrySetFolded(ent, foldable, false))
diff --git a/Content.Shared/Hands/Components/HandHelpers.cs b/Content.Shared/Hands/Components/HandHelpers.cs
deleted file mode 100644 (file)
index aecf3a6..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Linq;
-using Content.Shared.Hands.EntitySystems;
-
-namespace Content.Shared.Hands.Components;
-
-/// <summary>
-///     These helpers exist to make getting basic information out of the hands component more convenient, without
-///     needing to resolve hands system or something like that.
-/// </summary>
-public static class HandHelpers
-{
-    /// <summary>
-    ///     Returns true if any hand is free. This is a LinQ method, not a property, so
-    ///     cache it instead of accessing this multiple times.
-    /// </summary>
-    public static bool IsAnyHandFree(this HandsComponent component) => component.Hands.Values.Any(hand => hand.IsEmpty);
-
-    /// <summary>
-    ///     Get the number of hands that are not currently holding anything. This is a LinQ method, not a property, so
-    ///     cache it instead of accessing this multiple times.
-    /// </summary>
-    public static int CountFreeHands(this HandsComponent component) => component.Hands.Values.Count(hand => hand.IsEmpty);
-
-    /// <summary>
-    ///     Get the number of hands that are not currently holding anything. This is a LinQ method, not a property, so
-    ///     cache it instead of accessing this multiple times.
-    /// </summary>
-    public static int CountFreeableHands(this Entity<HandsComponent> component, SharedHandsSystem system)
-    {
-        return system.CountFreeableHands(component);
-    }
-
-    /// <summary>
-    ///     Get a list of hands that are currently holding nothing. This is a LinQ method, not a property, so cache
-    ///     it instead of accessing this multiple times.
-    /// </summary>
-    public static IEnumerable<Hand> GetFreeHands(this HandsComponent component) => component.Hands.Values.Where(hand => !hand.IsEmpty);
-
-    /// <summary>
-    ///     Get a list of hands that are currently holding nothing. This is a LinQ method, not a property, so cache
-    ///     it instead of accessing this multiple times.
-    /// </summary>
-    public static IEnumerable<string> GetFreeHandNames(this HandsComponent component) => GetFreeHands(component).Select(hand => hand.Name);
-}
index 0af318ba0678f699efe720418f66c47a19677af5..2aa97e78417ae1f586da83da639add1e97bc7e11 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Shared.DisplacementMap;
 using Content.Shared.Hands.EntitySystems;
-using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 using Robust.Shared.Serialization;
 
@@ -13,50 +12,50 @@ public sealed partial class HandsComponent : Component
     /// <summary>
     ///     The currently active hand.
     /// </summary>
-    [ViewVariables]
-    public Hand? ActiveHand;
+    [DataField]
+    public string? ActiveHandId;
 
     /// <summary>
-    ///     The item currently held in the active hand.
+    /// Dictionary relating a unique hand ID corresponding to a container slot on the attached entity to a class containing information about the Hand itself.
     /// </summary>
-    [ViewVariables]
-    public EntityUid? ActiveHandEntity => ActiveHand?.HeldEntity;
-
-    [ViewVariables]
+    [DataField]
     public Dictionary<string, Hand> Hands = new();
 
+    /// <summary>
+    /// The number of hands
+    /// </summary>
+    [ViewVariables]
     public int Count => Hands.Count;
 
     /// <summary>
     ///     List of hand-names. These are keys for <see cref="Hands"/>. The order of this list determines the order in which hands are iterated over.
     /// </summary>
+    [DataField]
     public List<string> SortedHands = new();
 
     /// <summary>
     ///     If true, the items in the hands won't be affected by explosions.
     /// </summary>
     [DataField]
-    public bool DisableExplosionRecursion = false;
+    public bool DisableExplosionRecursion;
 
     /// <summary>
     ///     Modifies the speed at which items are thrown.
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public float BaseThrowspeed { get; set; } = 11f;
+    public float BaseThrowspeed = 11f;
 
     /// <summary>
     ///     Distance after which longer throw targets stop increasing throw impulse.
     /// </summary>
-    [DataField("throwRange")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public float ThrowRange { get; set; } = 8f;
+    [DataField]
+    public float ThrowRange = 8f;
 
     /// <summary>
     ///     Whether or not to add in-hand sprites for held items. Some entities (e.g., drones) don't want these.
     ///     Used by the client.
     /// </summary>
-    [DataField("showInHands")]
+    [DataField]
     public bool ShowInHands = true;
 
     /// <summary>
@@ -68,14 +67,13 @@ public sealed partial class HandsComponent : Component
     /// <summary>
     ///     The time at which throws will be allowed again.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
-    [AutoPausedField]
+    [DataField, AutoPausedField]
     public TimeSpan NextThrowTime;
 
     /// <summary>
     ///     The minimum time inbetween throws.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public TimeSpan ThrowCooldown = TimeSpan.FromSeconds(0.5f);
 
     /// <summary>
@@ -103,48 +101,37 @@ public sealed partial class HandsComponent : Component
     public bool CanBeStripped = true;
 }
 
+[DataDefinition]
 [Serializable, NetSerializable]
-public sealed class Hand //TODO: This should definitely be a struct - Jezi
+public partial record struct Hand
 {
-    [ViewVariables]
-    public string Name { get; }
-
-    [ViewVariables]
-    public HandLocation Location { get; }
-
-    /// <summary>
-    ///     The container used to hold the contents of this hand. Nullable because the client must get the containers via <see cref="ContainerManagerComponent"/>,
-    ///     which may not be synced with the server when the client hands are created.
-    /// </summary>
-    [ViewVariables, NonSerialized]
-    public ContainerSlot? Container;
+    [DataField]
+    public HandLocation Location = HandLocation.Right;
 
-    [ViewVariables]
-    public EntityUid? HeldEntity => Container?.ContainedEntity;
+    public Hand()
+    {
 
-    public bool IsEmpty => HeldEntity == null;
+    }
 
-    public Hand(string name, HandLocation location, ContainerSlot? container = null)
+    public Hand(HandLocation location)
     {
-        Name = name;
         Location = location;
-        Container = container;
     }
 }
 
 [Serializable, NetSerializable]
 public sealed class HandsComponentState : ComponentState
 {
-    public readonly List<Hand> Hands;
-    public readonly List<string> HandNames;
-    public readonly string? ActiveHand;
+    public readonly Dictionary<string, Hand> Hands;
+    public readonly List<string> SortedHands;
+    public readonly string? ActiveHandId;
 
     public HandsComponentState(HandsComponent handComp)
     {
         // cloning lists because of test networking.
-        Hands = new(handComp.Hands.Values);
-        HandNames = new(handComp.SortedHands);
-        ActiveHand = handComp.ActiveHand?.Name;
+        Hands = new(handComp.Hands);
+        SortedHands = new(handComp.SortedHands);
+        ActiveHandId = handComp.ActiveHandId;
     }
 }
 
index 1eac3a226393bdff74141f82072406b36890c899..e84b23400bac9567088bb6d383db7c8a8eb9c6c6 100644 (file)
@@ -6,17 +6,17 @@ namespace Content.Shared.Hands.EntitySystems;
 // These functions are mostly unused except for some AI operator stuff
 // Nothing stops them from being used in general. If they ever get used elsewhere, then this file probably needs to be renamed.
 
-public abstract partial class SharedHandsSystem : EntitySystem
+public abstract partial class SharedHandsSystem
 {
     public bool TrySelect(EntityUid uid, EntityUid? entity, HandsComponent? handsComp = null)
     {
         if (!Resolve(uid, ref handsComp, false))
             return false;
 
-        if (!IsHolding(uid, entity, out var hand, handsComp))
+        if (!IsHolding((uid, handsComp), entity, out var hand))
             return false;
 
-        SetActiveHand(uid, hand, handsComp);
+        SetActiveHand((uid, handsComp), hand);
         return true;
     }
 
@@ -26,9 +26,12 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!Resolve(uid, ref handsComp, false))
             return false;
 
-        foreach (var hand in handsComp.Hands.Values)
+        foreach (var hand in handsComp.Hands.Keys)
         {
-            if (TryComp(hand.HeldEntity, out component))
+            if (!TryGetHeldItem((uid, handsComp), hand, out var held))
+                continue;
+
+            if (TryComp(held, out component))
                 return true;
         }
 
index 95773697db20666f6e01ec76425389278e70c9c5..c1b44efba4740b21ffa61f1e27faa44835285f0e 100644 (file)
@@ -7,6 +7,7 @@ using Content.Shared.Tag;
 using Robust.Shared.Containers;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Hands.EntitySystems;
 
@@ -28,10 +29,10 @@ public abstract partial class SharedHandsSystem
             return;
         }
 
-        var gotUnequipped = new GotUnequippedHandEvent(uid, args.Entity, hand);
+        var gotUnequipped = new GotUnequippedHandEvent(uid, args.Entity, hand.Value);
         RaiseLocalEvent(args.Entity, gotUnequipped);
 
-        var didUnequip = new DidUnequipHandEvent(uid, args.Entity, hand);
+        var didUnequip = new DidUnequipHandEvent(uid, args.Entity, hand.Value);
         RaiseLocalEvent(uid, didUnequip);
 
         if (TryComp(args.Entity, out VirtualItemComponent? @virtual))
@@ -47,26 +48,29 @@ public abstract partial class SharedHandsSystem
     /// <summary>
     ///     Checks whether an entity can drop a given entity. Will return false if they are not holding the entity.
     /// </summary>
-    public bool CanDrop(EntityUid uid, EntityUid entity, HandsComponent? handsComp = null, bool checkActionBlocker = true)
+    public bool CanDrop(Entity<HandsComponent?> ent, EntityUid entity, bool checkActionBlocker = true)
     {
-        if (!Resolve(uid, ref handsComp))
+        if (!Resolve(ent, ref ent.Comp, false))
             return false;
 
-        if (!IsHolding(uid, entity, out var hand, handsComp))
+        if (!IsHolding(ent, entity, out var hand))
             return false;
 
-        return CanDropHeld(uid, hand, checkActionBlocker);
+        return CanDropHeld(ent, hand, checkActionBlocker);
     }
 
     /// <summary>
     ///     Checks if the contents of a hand is able to be removed from its container.
     /// </summary>
-    public bool CanDropHeld(EntityUid uid, Hand hand, bool checkActionBlocker = true)
+    public bool CanDropHeld(EntityUid uid, string handId, bool checkActionBlocker = true)
     {
-        if (hand.Container?.ContainedEntity is not {} held)
+        if (!ContainerSystem.TryGetContainer(uid, handId, out var container))
             return false;
 
-        if (!ContainerSystem.CanRemove(held, hand.Container))
+        if (container.ContainedEntities.FirstOrNull() is not {} held)
+            return false;
+
+        if (!ContainerSystem.CanRemove(held, container))
             return false;
 
         if (checkActionBlocker && !_actionBlocker.CanDrop(uid))
@@ -78,98 +82,100 @@ public abstract partial class SharedHandsSystem
     /// <summary>
     ///     Attempts to drop the item in the currently active hand.
     /// </summary>
-    public bool TryDrop(EntityUid uid, EntityCoordinates? targetDropLocation = null, bool checkActionBlocker = true, bool doDropInteraction = true, HandsComponent? handsComp = null)
+    public bool TryDrop(Entity<HandsComponent?> ent, EntityCoordinates? targetDropLocation = null, bool checkActionBlocker = true, bool doDropInteraction = true)
     {
-        if (!Resolve(uid, ref handsComp))
+        if (!Resolve(ent, ref ent.Comp, false))
             return false;
 
-        if (handsComp.ActiveHand == null)
+        if (ent.Comp.ActiveHandId == null)
             return false;
 
-        return TryDrop(uid, handsComp.ActiveHand, targetDropLocation, checkActionBlocker, doDropInteraction, handsComp);
+        return TryDrop(ent, ent.Comp.ActiveHandId, targetDropLocation, checkActionBlocker, doDropInteraction);
     }
 
     /// <summary>
     ///     Drops an item at the target location.
     /// </summary>
-    public bool TryDrop(EntityUid uid, EntityUid entity, EntityCoordinates? targetDropLocation = null, bool checkActionBlocker = true, bool doDropInteraction = true, HandsComponent? handsComp = null)
+    public bool TryDrop(Entity<HandsComponent?> ent, EntityUid entity, EntityCoordinates? targetDropLocation = null, bool checkActionBlocker = true, bool doDropInteraction = true)
     {
-        if (!Resolve(uid, ref handsComp))
+        if (!Resolve(ent, ref ent.Comp, false))
             return false;
 
-        if (!IsHolding(uid, entity, out var hand, handsComp))
+        if (!IsHolding(ent, entity, out var hand))
             return false;
 
-        return TryDrop(uid, hand, targetDropLocation, checkActionBlocker, doDropInteraction, handsComp);
+        return TryDrop(ent, hand, targetDropLocation, checkActionBlocker, doDropInteraction);
     }
 
     /// <summary>
     ///     Drops a hands contents at the target location.
     /// </summary>
-    public bool TryDrop(EntityUid uid, Hand hand, EntityCoordinates? targetDropLocation = null, bool checkActionBlocker = true, bool doDropInteraction = true, HandsComponent? handsComp = null)
+    public bool TryDrop(Entity<HandsComponent?> ent, string handId, EntityCoordinates? targetDropLocation = null, bool checkActionBlocker = true, bool doDropInteraction = true)
     {
-        if (!Resolve(uid, ref handsComp))
+        if (!Resolve(ent, ref ent.Comp, false))
             return false;
 
-        if (!CanDropHeld(uid, hand, checkActionBlocker))
+        if (!CanDropHeld(ent, handId, checkActionBlocker))
             return false;
 
-        var entity = hand.HeldEntity!.Value;
+        if (!TryGetHeldItem(ent, handId, out var entity))
+            return false;
 
         // if item is a fake item (like with pulling), just delete it rather than bothering with trying to drop it into the world
         if (TryComp(entity, out VirtualItemComponent? @virtual))
-            _virtualSystem.DeleteVirtualItem((entity, @virtual), uid);
+            _virtualSystem.DeleteVirtualItem((entity.Value, @virtual), ent);
 
         if (TerminatingOrDeleted(entity))
             return true;
 
-        var itemXform = Transform(entity);
+        var itemXform = Transform(entity.Value);
         if (itemXform.MapUid == null)
             return true;
 
-        var userXform = Transform(uid);
-        var isInContainer = ContainerSystem.IsEntityOrParentInContainer(uid, xform: userXform);
+        var userXform = Transform(ent);
+        var isInContainer = ContainerSystem.IsEntityOrParentInContainer(ent, xform: userXform);
 
         // if the user is in a container, drop the item inside the container
-        if (isInContainer) {
-            TransformSystem.DropNextTo((entity, itemXform), (uid, userXform));
+        if (isInContainer)
+        {
+            TransformSystem.DropNextTo((entity.Value, itemXform), (ent, userXform));
             return true;
         }
 
         // drop the item with heavy calculations from their hands and place it at the calculated interaction range position
         // The DoDrop is handle if there's no drop target
-        DoDrop(uid, hand, doDropInteraction: doDropInteraction, handsComp);
+        DoDrop(ent, handId, doDropInteraction: doDropInteraction);
 
         // if there's no drop location stop here
         if (targetDropLocation == null)
             return true;
 
         // otherwise, also move dropped item and rotate it properly according to grid/map
-        var (itemPos, itemRot) = TransformSystem.GetWorldPositionRotation(entity);
+        var (itemPos, itemRot) = TransformSystem.GetWorldPositionRotation(entity.Value);
         var origin = new MapCoordinates(itemPos, itemXform.MapID);
         var target = TransformSystem.ToMapCoordinates(targetDropLocation.Value);
-        TransformSystem.SetWorldPositionRotation(entity, GetFinalDropCoordinates(uid, origin, target, entity), itemRot);
+        TransformSystem.SetWorldPositionRotation(entity.Value, GetFinalDropCoordinates(ent, origin, target, entity.Value), itemRot);
         return true;
     }
 
     /// <summary>
     ///     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.
     /// </summary>
-    public bool TryDropIntoContainer(EntityUid uid, EntityUid entity, BaseContainer targetContainer, bool checkActionBlocker = true, HandsComponent? handsComp = null)
+    public bool TryDropIntoContainer(Entity<HandsComponent?> ent, EntityUid entity, BaseContainer targetContainer, bool checkActionBlocker = true)
     {
-        if (!Resolve(uid, ref handsComp))
+        if (!Resolve(ent, ref ent.Comp, false))
             return false;
 
-        if (!IsHolding(uid, entity, out var hand, handsComp))
+        if (!IsHolding(ent, entity, out var hand))
             return false;
 
-        if (!CanDropHeld(uid, hand, checkActionBlocker))
+        if (!CanDropHeld(ent, hand, checkActionBlocker))
             return false;
 
         if (!ContainerSystem.CanInsert(entity, targetContainer))
             return false;
 
-        DoDrop(uid, hand, false, handsComp);
+        DoDrop(ent, hand, false);
         ContainerSystem.Insert(entity, targetContainer);
         return true;
     }
@@ -202,34 +208,38 @@ public abstract partial class SharedHandsSystem
     /// <summary>
     ///     Removes the contents of a hand from its container. Assumes that the removal is allowed. In general, you should not be calling this directly.
     /// </summary>
-    public virtual void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? handsComp = null, bool log = true)
+    public virtual void DoDrop(Entity<HandsComponent?> ent,
+        string handId,
+        bool doDropInteraction = true,
+        bool log = true)
     {
-        if (!Resolve(uid, ref handsComp))
+        if (!Resolve(ent, ref ent.Comp, false))
             return;
 
-        if (hand.Container?.ContainedEntity == null)
+        if (!ContainerSystem.TryGetContainer(ent, handId, out var container))
             return;
 
-        var entity = hand.Container.ContainedEntity.Value;
+        if (!TryGetHeldItem(ent, handId, out var entity))
+            return;
 
-        if (TerminatingOrDeleted(uid) || TerminatingOrDeleted(entity))
+        if (TerminatingOrDeleted(ent) || TerminatingOrDeleted(entity))
             return;
 
-        if (!ContainerSystem.Remove(entity, hand.Container))
+        if (!ContainerSystem.Remove(entity.Value, container))
         {
-            Log.Error($"Failed to remove {ToPrettyString(entity)} from users hand container when dropping. User: {ToPrettyString(uid)}. Hand: {hand.Name}.");
+            Log.Error($"Failed to remove {ToPrettyString(entity)} from users hand container when dropping. User: {ToPrettyString(ent)}. Hand: {handId}.");
             return;
         }
 
-        Dirty(uid, handsComp);
+        Dirty(ent);
 
         if (doDropInteraction)
-            _interactionSystem.DroppedInteraction(uid, entity);
+            _interactionSystem.DroppedInteraction(ent, entity.Value);
 
         if (log)
-            _adminLogger.Add(LogType.Drop, LogImpact.Low, $"{ToPrettyString(uid):user} dropped {ToPrettyString(entity):entity}");
+            _adminLogger.Add(LogType.Drop, LogImpact.Low, $"{ToPrettyString(ent):user} dropped {ToPrettyString(entity):entity}");
 
-        if (hand == handsComp.ActiveHand)
-            RaiseLocalEvent(entity, new HandDeselectedEvent(uid));
+        if (handId == ent.Comp.ActiveHandId)
+            RaiseLocalEvent(entity.Value, new HandDeselectedEvent(ent));
     }
 }
index 6e383cd69c61c6bff287fb8e0bc64127a101a717..0e8d7a75564056b522c4050bafc7d418cc183e67 100644 (file)
@@ -97,20 +97,20 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!_actionBlocker.CanInteract(session.AttachedEntity.Value, null))
             return;
 
-        if (component.ActiveHand == null || component.Hands.Count < 2)
+        if (component.ActiveHandId == null || component.Hands.Count < 2)
             return;
 
-        var currentIndex = component.SortedHands.IndexOf(component.ActiveHand.Name);
+        var currentIndex = component.SortedHands.IndexOf(component.ActiveHandId);
         var newActiveIndex = (currentIndex + (reverse ? -1 : 1) + component.Hands.Count) % component.Hands.Count;
         var nextHand = component.SortedHands[newActiveIndex];
 
-        TrySetActiveHand(session.AttachedEntity.Value, nextHand, component);
+        TrySetActiveHand((session.AttachedEntity.Value, component), nextHand);
     }
 
     private bool DropPressed(ICommonSession? session, EntityCoordinates coords, EntityUid netEntity)
     {
-        if (TryComp(session?.AttachedEntity, out HandsComponent? hands) && hands.ActiveHand != null)
-            TryDrop(session.AttachedEntity.Value, hands.ActiveHand, coords, handsComp: hands);
+        if (TryComp(session?.AttachedEntity, out HandsComponent? hands) && hands.ActiveHandId != null)
+            TryDrop((session.AttachedEntity.Value, hands), hands.ActiveHandId, coords);
 
         // always send to server.
         return false;
@@ -122,14 +122,14 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!Resolve(uid, ref handsComp, false))
             return false;
 
-        Hand? hand;
-        if (handName == null || !handsComp.Hands.TryGetValue(handName, out hand))
-            hand = handsComp.ActiveHand;
+        var hand = handName;
+        if (!TryGetHand(uid, hand, out _))
+            hand = handsComp.ActiveHandId;
 
-        if (hand?.HeldEntity is not { } held)
+        if (!TryGetHeldItem((uid, handsComp), hand, out var held))
             return false;
 
-        return _interactionSystem.InteractionActivate(uid, held);
+        return _interactionSystem.InteractionActivate(uid, held.Value);
     }
 
     public bool TryInteractHandWithActiveHand(EntityUid uid, string handName, HandsComponent? handsComp = null)
@@ -137,16 +137,13 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!Resolve(uid, ref handsComp, false))
             return false;
 
-        if (handsComp.ActiveHandEntity == null)
+        if (!TryGetActiveItem((uid, handsComp), out var activeHeldItem))
             return false;
 
-        if (!handsComp.Hands.TryGetValue(handName, out var hand))
+        if (!TryGetHeldItem((uid, handsComp), handName, out var held))
             return false;
 
-        if (hand.HeldEntity == null)
-            return false;
-
-        _interactionSystem.InteractUsing(uid, handsComp.ActiveHandEntity.Value, hand.HeldEntity.Value, Transform(hand.HeldEntity.Value).Coordinates);
+        _interactionSystem.InteractUsing(uid, activeHeldItem.Value, held.Value, Transform(held.Value).Coordinates);
         return true;
     }
 
@@ -155,17 +152,16 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!Resolve(uid, ref handsComp, false))
             return false;
 
-        Hand? hand;
-        if (handName == null || !handsComp.Hands.TryGetValue(handName, out hand))
-            hand = handsComp.ActiveHand;
+        var hand = handName;
+        if (!TryGetHand(uid, hand, out _))
+            hand = handsComp.ActiveHandId;
 
-        if (hand?.HeldEntity is not { } held)
+        if (!TryGetHeldItem((uid, handsComp), hand, out var held))
             return false;
 
         if (altInteract)
-            return _interactionSystem.AltInteract(uid, held);
-        else
-            return _interactionSystem.UseInHandInteraction(uid, held);
+            return _interactionSystem.AltInteract(uid, held.Value);
+        return _interactionSystem.UseInHandInteraction(uid, held.Value);
     }
 
     /// <summary>
@@ -176,22 +172,20 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!Resolve(uid, ref handsComp))
             return false;
 
-        if (handsComp.ActiveHand == null || !handsComp.ActiveHand.IsEmpty)
+        if (handsComp.ActiveHandId == null || !HandIsEmpty((uid, handsComp), handsComp.ActiveHandId))
             return false;
 
-        if (!handsComp.Hands.TryGetValue(handName, out var hand))
+        if (!TryGetHeldItem((uid, handsComp), handName, out var entity))
             return false;
 
-        if (!CanDropHeld(uid, hand, checkActionBlocker))
+        if (!CanDropHeld(uid, handName, checkActionBlocker))
             return false;
 
-        var entity = hand.HeldEntity!.Value;
-
-        if (!CanPickupToHand(uid, entity, handsComp.ActiveHand, checkActionBlocker, handsComp))
+        if (!CanPickupToHand(uid, entity.Value, handsComp.ActiveHandId, checkActionBlocker, handsComp))
             return false;
 
-        DoDrop(uid, hand, false, handsComp, log:false);
-        DoPickup(uid, handsComp.ActiveHand, entity, handsComp, log: false);
+        DoDrop(uid, handName, false, log: false);
+        DoPickup(uid, handsComp.ActiveHandId, entity.Value, handsComp, log: false);
         return true;
     }
 
@@ -200,19 +194,19 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (args.Handled)
             return;
 
-        if (component.ActiveHandEntity.HasValue)
+        if (TryGetActiveItem((uid, component), out var activeHeldItem))
         {
             // allow for the item to return a different entity, e.g. virtual items
-            RaiseLocalEvent(component.ActiveHandEntity.Value, ref args);
+            RaiseLocalEvent(activeHeldItem.Value, ref args);
         }
 
-        args.Used ??= component.ActiveHandEntity;
+        args.Used ??= activeHeldItem;
     }
 
     //TODO: Actually shows all items/clothing/etc.
     private void HandleExamined(EntityUid examinedUid, HandsComponent handsComp, ExaminedEvent args)
     {
-        var heldItemNames = EnumerateHeld(examinedUid, handsComp)
+        var heldItemNames = EnumerateHeld((examinedUid, handsComp))
             .Where(entity => !HasComp<VirtualItemComponent>(entity))
             .Select(item => FormattedMessage.EscapeText(Identity.Name(item, EntityManager)))
             .Select(itemName => Loc.GetString("comp-hands-examine-wrapper", ("item", itemName)))
index 5addd7c029aa308d71ae2ea5b45260a91aabbfef..4558f6c5287970860e7d8b5fb7d4c0f06785d31d 100644 (file)
@@ -1,15 +1,15 @@
-using Content.Shared.Clothing.Components;
+using System.Diagnostics;
 using Content.Shared.Database;
 using Content.Shared.Hands.Components;
 using Content.Shared.Item;
 using Robust.Shared.Containers;
-using Robust.Shared.Map;
 using Robust.Shared.Physics;
 using Robust.Shared.Physics.Components;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Hands.EntitySystems;
 
-public abstract partial class SharedHandsSystem : EntitySystem
+public abstract partial class SharedHandsSystem
 {
     private void InitializePickup()
     {
@@ -23,11 +23,11 @@ public abstract partial class SharedHandsSystem : EntitySystem
             return;
         }
 
-        var didEquip = new DidEquipHandEvent(uid, args.Entity, hand);
-        RaiseLocalEvent(uid, didEquip, false);
+        var didEquip = new DidEquipHandEvent(uid, args.Entity, hand.Value);
+        RaiseLocalEvent(uid, didEquip);
 
-        var gotEquipped = new GotEquippedHandEvent(uid, args.Entity, hand);
-        RaiseLocalEvent(args.Entity, gotEquipped, false);
+        var gotEquipped = new GotEquippedHandEvent(uid, args.Entity, hand.Value);
+        RaiseLocalEvent(args.Entity, gotEquipped);
     }
 
     /// <summary>
@@ -35,32 +35,6 @@ public abstract partial class SharedHandsSystem : EntitySystem
     /// </summary>
     public const float MaxAnimationRange = 10;
 
-    /// <summary>
-    ///     Tries to pick up an entity to a specific hand. If no explicit hand is specified, defaults to using the currently active hand.
-    /// </summary>
-    public bool TryPickup(
-        EntityUid uid,
-        EntityUid entity,
-        string? handName = null,
-        bool checkActionBlocker = true,
-        bool animateUser = false,
-        bool animate = true,
-        HandsComponent? handsComp = null,
-        ItemComponent? item = null)
-    {
-        if (!Resolve(uid, ref handsComp, false))
-            return false;
-
-        var hand = handsComp.ActiveHand;
-        if (handName != null && !handsComp.Hands.TryGetValue(handName, out hand))
-            return false;
-
-        if (hand == null)
-            return false;
-
-        return TryPickup(uid, entity, hand, checkActionBlocker, animate, handsComp, item);
-    }
-
     /// <summary>
     ///     Attempts to pick up an item into any empty hand. Prioritizes the currently active hand.
     /// </summary>
@@ -80,17 +54,21 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!Resolve(uid, ref handsComp, false))
             return false;
 
-        if (!TryGetEmptyHand(uid, out var hand, handsComp))
+        if (!TryGetEmptyHand((uid, handsComp), out var hand))
             return false;
 
-        return TryPickup(uid, entity, hand, checkActionBlocker, animate, handsComp, item);
+        return TryPickup(uid, entity, hand, checkActionBlocker, animateUser, animate, handsComp, item);
     }
 
+    /// <summary>
+    ///     Tries to pick up an entity to a specific hand. If no explicit hand is specified, defaults to using the currently active hand.
+    /// </summary>
     public bool TryPickup(
         EntityUid uid,
         EntityUid entity,
-        Hand hand,
+        string? handId = null,
         bool checkActionBlocker = true,
+        bool animateUser = false,
         bool animate = true,
         HandsComponent? handsComp = null,
         ItemComponent? item = null)
@@ -98,10 +76,15 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!Resolve(uid, ref handsComp, false))
             return false;
 
+        handId ??= handsComp.ActiveHandId;
+
+        if (handId == null)
+            return false;
+
         if (!Resolve(entity, ref item, false))
             return false;
 
-        if (!CanPickupToHand(uid, entity, hand, checkActionBlocker, handsComp, item))
+        if (!CanPickupToHand(uid, entity, handId, checkActionBlocker, handsComp, item))
             return false;
 
         if (animate)
@@ -119,7 +102,7 @@ public abstract partial class SharedHandsSystem : EntitySystem
                 _storage.PlayPickupAnimation(entity, initialPosition, xform.Coordinates, itemXform.LocalRotation, uid);
             }
         }
-        DoPickup(uid, hand, entity, handsComp);
+        DoPickup(uid, handId, entity, handsComp);
 
         return true;
     }
@@ -129,20 +112,20 @@ public abstract partial class SharedHandsSystem : EntitySystem
     /// By default it does check if it's possible to drop items.
     /// </summary>
     public bool TryForcePickup(
-        EntityUid uid,
+        Entity<HandsComponent?> ent,
         EntityUid entity,
-        Hand hand,
+        string hand,
         bool checkActionBlocker = true,
         bool animate = true,
         HandsComponent? handsComp = null,
         ItemComponent? item = null)
     {
-        if (!Resolve(uid, ref handsComp, false))
+        if (!Resolve(ent, ref ent.Comp, false))
             return false;
 
-        TryDrop(uid, hand, checkActionBlocker: checkActionBlocker, handsComp: handsComp);
+        TryDrop(ent, hand, checkActionBlocker: checkActionBlocker);
 
-        return TryPickup(uid, entity, hand, checkActionBlocker, animate, handsComp, item);
+        return TryPickup(ent, entity, hand, checkActionBlocker, animate: animate, handsComp: handsComp, item: item);
     }
 
     /// <summary>
@@ -157,9 +140,9 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (TryPickupAnyHand(uid, entity, checkActionBlocker: checkActionBlocker, handsComp: handsComp))
             return true;
 
-        foreach (var hand in handsComp.Hands.Values)
+        foreach (var hand in handsComp.Hands.Keys)
         {
-            if (TryDrop(uid, hand, checkActionBlocker: checkActionBlocker, handsComp: handsComp) &&
+            if (TryDrop((uid, handsComp), hand, checkActionBlocker: checkActionBlocker) &&
                 TryPickup(uid, entity, hand, checkActionBlocker: checkActionBlocker, handsComp: handsComp))
             {
                 return true;
@@ -173,7 +156,7 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!Resolve(uid, ref handsComp, false))
             return false;
 
-        if (!TryGetEmptyHand(uid, out var hand, handsComp))
+        if (!TryGetEmptyHand((uid, handsComp), out var hand))
             return false;
 
         return CanPickupToHand(uid, entity, hand, checkActionBlocker, handsComp, item);
@@ -182,13 +165,15 @@ public abstract partial class SharedHandsSystem : EntitySystem
     /// <summary>
     ///     Checks whether a given item will fit into a specific user's hand. Unless otherwise specified, this will also check the general CanPickup action blocker.
     /// </summary>
-    public bool CanPickupToHand(EntityUid uid, EntityUid entity, Hand hand, bool checkActionBlocker = true, HandsComponent? handsComp = null, ItemComponent? item = null)
+    public bool CanPickupToHand(EntityUid uid, EntityUid entity, string handId, bool checkActionBlocker = true, HandsComponent? handsComp = null, ItemComponent? item = null)
     {
         if (!Resolve(uid, ref handsComp, false))
             return false;
 
-        var handContainer = hand.Container;
-        if (handContainer == null || handContainer.ContainedEntity != null)
+        if (!ContainerSystem.TryGetContainer(uid, handId, out var handContainer))
+            return false;
+
+        if (handContainer.ContainedEntities.FirstOrNull() != null)
             return false;
 
         if (!Resolve(entity, ref item, false))
@@ -231,8 +216,7 @@ public abstract partial class SharedHandsSystem : EntitySystem
     {
         if (uid == null
             || !Resolve(uid.Value, ref handsComp, false)
-            || !TryGetEmptyHand(uid.Value, out var hand, handsComp)
-            || !TryPickup(uid.Value, entity, hand, checkActionBlocker, animate, handsComp, item))
+            || !TryPickupAnyHand(uid.Value, entity, checkActionBlocker, animateUser, animate, handsComp, item))
         {
             // TODO make this check upwards for any container, and parent to that.
             // Currently this just checks the direct parent, so items can still teleport through containers.
@@ -248,18 +232,20 @@ public abstract partial class SharedHandsSystem : EntitySystem
     /// <summary>
     ///     Puts an entity into the player's hand, assumes that the insertion is allowed. In general, you should not be calling this function directly.
     /// </summary>
-    public virtual void DoPickup(EntityUid uid, Hand hand, EntityUid entity, HandsComponent? hands = null, bool log = true)
+    public virtual void DoPickup(EntityUid uid, string hand, EntityUid entity, HandsComponent? hands = null, bool log = true)
     {
         if (!Resolve(uid, ref hands))
             return;
 
-        var handContainer = hand.Container;
-        if (handContainer == null || handContainer.ContainedEntity != null)
+        if (!ContainerSystem.TryGetContainer(uid, hand, out var handContainer))
+            return;
+
+        if (handContainer.ContainedEntities.FirstOrNull() != null)
             return;
 
         if (!ContainerSystem.Insert(entity, handContainer))
         {
-            Log.Error($"Failed to insert {ToPrettyString(entity)} into users hand container when picking up. User: {ToPrettyString(uid)}. Hand: {hand.Name}.");
+            Log.Error($"Failed to insert {ToPrettyString(entity)} into users hand container when picking up. User: {ToPrettyString(uid)}. Hand: {hand}.");
             return;
         }
 
@@ -270,7 +256,7 @@ public abstract partial class SharedHandsSystem : EntitySystem
 
         Dirty(uid, hands);
 
-        if (hand == hands.ActiveHand)
-            RaiseLocalEvent(entity, new HandSelectedEvent(uid), false);
+        if (hand == hands.ActiveHandId)
+            RaiseLocalEvent(entity, new HandSelectedEvent(uid));
     }
 }
index e6f21abf1b417633e9dfb173e7ebb4360d456863..bfb9a41b0b0292a796e921ae8621acb01c02d503 100644 (file)
@@ -41,7 +41,7 @@ public abstract partial class SharedHandsSystem
     {
         var ev = new HeldRelayedEvent<T>(args);
 
-        foreach (var held in EnumerateHeld(entity, entity.Comp))
+        foreach (var held in EnumerateHeld(entity.AsNullable()))
         {
             RaiseLocalEvent(held, ref ev);
         }
index 84beabf9ac859ff9dffae7ab8655ae2205e61bd4..4a24b9de02333b3dcb5e4631c7ff5f53e9e0f490 100644 (file)
@@ -9,6 +9,7 @@ using Content.Shared.Inventory.VirtualItem;
 using Content.Shared.Storage.EntitySystems;
 using Robust.Shared.Containers;
 using Robust.Shared.Input.Binding;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Hands.EntitySystems;
 
@@ -23,6 +24,8 @@ public abstract partial class SharedHandsSystem
     [Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
     [Dependency] private readonly SharedVirtualItemSystem _virtualSystem = default!;
 
+    public event Action<Entity<HandsComponent>, string, HandLocation>? OnPlayerAddHand;
+    public event Action<Entity<HandsComponent>, string>? OnPlayerRemoveHand;
     protected event Action<Entity<HandsComponent>?>? OnHandSetActive;
 
     public override void Initialize()
@@ -33,6 +36,9 @@ public abstract partial class SharedHandsSystem
         InitializeDrop();
         InitializePickup();
         InitializeRelay();
+
+        SubscribeLocalEvent<HandsComponent, ComponentInit>(OnInit);
+        SubscribeLocalEvent<HandsComponent, MapInitEvent>(OnMapInit);
     }
 
     public override void Shutdown()
@@ -41,71 +47,94 @@ public abstract partial class SharedHandsSystem
         CommandBinds.Unregister<SharedHandsSystem>();
     }
 
-    public virtual void AddHand(EntityUid uid, string handName, HandLocation handLocation, HandsComponent? handsComp = null)
+    private void OnInit(Entity<HandsComponent> ent, ref ComponentInit args)
+    {
+        var container = EnsureComp<ContainerManagerComponent>(ent);
+        foreach (var id in ent.Comp.Hands.Keys)
+        {
+            ContainerSystem.EnsureContainer<ContainerSlot>(ent, id, container);
+        }
+    }
+
+    private void OnMapInit(Entity<HandsComponent> ent, ref MapInitEvent args)
     {
-        if (!Resolve(uid, ref handsComp, false))
+        if (ent.Comp.ActiveHandId == null)
+            SetActiveHand(ent.AsNullable(), ent.Comp.SortedHands.FirstOrDefault());
+    }
+
+    /// <summary>
+    /// Adds a hand with the given container id and supplied location to the specified entity.
+    /// </summary>
+    public void AddHand(Entity<HandsComponent?> ent, string handName, HandLocation handLocation)
+    {
+        AddHand(ent, handName, new Hand(handLocation));
+    }
+
+    /// <summary>
+    /// Adds a hand with the given container id and supplied hand definition to the given entity.
+    /// </summary>
+    public void AddHand(Entity<HandsComponent?> ent, string handName, Hand hand)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
             return;
 
-        if (handsComp.Hands.ContainsKey(handName))
+        if (ent.Comp.Hands.ContainsKey(handName))
             return;
 
-        var container = ContainerSystem.EnsureContainer<ContainerSlot>(uid, handName);
+        var container = ContainerSystem.EnsureContainer<ContainerSlot>(ent, handName);
         container.OccludesLight = false;
 
-        var newHand = new Hand(handName, handLocation, container);
-        handsComp.Hands.Add(handName, newHand);
-        handsComp.SortedHands.Add(handName);
+        ent.Comp.Hands.Add(handName, hand);
+        ent.Comp.SortedHands.Add(handName);
+        Dirty(ent);
+
+        OnPlayerAddHand?.Invoke((ent, ent.Comp), handName, hand.Location);
 
-        if (handsComp.ActiveHand == null)
-            SetActiveHand(uid, newHand, handsComp);
+        if (ent.Comp.ActiveHandId == null)
+            SetActiveHand(ent, handName);
 
-        RaiseLocalEvent(uid, new HandCountChangedEvent(uid));
-        Dirty(uid, handsComp);
+        RaiseLocalEvent(ent, new HandCountChangedEvent(ent));
     }
 
-    public virtual void RemoveHand(EntityUid uid, string handName, HandsComponent? handsComp = null)
+    /// <summary>
+    /// Removes the specified hand from the specified entity
+    /// </summary>
+    public virtual void RemoveHand(Entity<HandsComponent?> ent, string handName)
     {
-        if (!Resolve(uid, ref handsComp, false))
+        if (!Resolve(ent, ref ent.Comp, false))
             return;
 
-        if (!handsComp.Hands.Remove(handName, out var hand))
+        OnPlayerRemoveHand?.Invoke((ent, ent.Comp), handName);
+
+        TryDrop(ent, handName, null, false);
+
+        if (!ent.Comp.Hands.Remove(handName))
             return;
 
-        handsComp.SortedHands.Remove(hand.Name);
-        TryDrop(uid, hand, null, false, true, handsComp);
-        if (hand.Container != null)
-            ContainerSystem.ShutdownContainer(hand.Container);
+        if (ContainerSystem.TryGetContainer(ent, handName, out var container))
+            ContainerSystem.ShutdownContainer(container);
 
-        if (handsComp.ActiveHand == hand)
-            TrySetActiveHand(uid, handsComp.SortedHands.FirstOrDefault(), handsComp);
+        ent.Comp.SortedHands.Remove(handName);
+        if (ent.Comp.ActiveHandId == handName)
+            TrySetActiveHand(ent, ent.Comp.SortedHands.FirstOrDefault());
 
-        RaiseLocalEvent(uid, new HandCountChangedEvent(uid));
-        Dirty(uid, handsComp);
+        RaiseLocalEvent(ent, new HandCountChangedEvent(ent));
+        Dirty(ent);
     }
 
     /// <summary>
     /// Gets rid of all the entity's hands.
     /// </summary>
-    /// <param name="uid"></param>
-    /// <param name="handsComp"></param>
-    public void RemoveHands(EntityUid uid, HandsComponent? handsComp = null)
+    public void RemoveHands(Entity<HandsComponent?> ent)
     {
-        if (!Resolve(uid, ref handsComp))
+        if (!Resolve(ent, ref ent.Comp, false))
             return;
 
-        RemoveHands(uid, EnumerateHands(uid), handsComp);
-    }
-
-    private void RemoveHands(EntityUid uid, IEnumerable<Hand> hands, HandsComponent handsComp)
-    {
-        if (!hands.Any())
-            return;
-
-        var hand = hands.First();
-        RemoveHand(uid, hand.Name, handsComp);
-
-        // Repeats it for any additional hands.
-        RemoveHands(uid, hands, handsComp);
+        var handIds = new List<string>(ent.Comp.Hands.Keys);
+        foreach (var handId in handIds)
+        {
+            RemoveHand(ent, handId);
+        }
     }
 
     private void HandleSetHand(RequestSetHandEvent msg, EntitySessionEventArgs eventArgs)
@@ -119,15 +148,15 @@ public abstract partial class SharedHandsSystem
     /// <summary>
     ///     Get any empty hand. Prioritizes the currently active hand.
     /// </summary>
-    public bool TryGetEmptyHand(EntityUid uid, [NotNullWhen(true)] out Hand? emptyHand, HandsComponent? handComp = null)
+    public bool TryGetEmptyHand(Entity<HandsComponent?> ent, [NotNullWhen(true)] out string? emptyHand)
     {
         emptyHand = null;
-        if (!Resolve(uid, ref handComp, false))
+        if (!Resolve(ent, ref ent.Comp, false))
             return false;
 
-        foreach (var hand in EnumerateHands(uid, handComp))
+        foreach (var hand in EnumerateHands(ent))
         {
-            if (hand.IsEmpty)
+            if (HandIsEmpty(ent, hand))
             {
                 emptyHand = hand;
                 return true;
@@ -137,28 +166,20 @@ public abstract partial class SharedHandsSystem
         return false;
     }
 
-    public bool TryGetActiveHand(Entity<HandsComponent?> entity, [NotNullWhen(true)] out Hand? hand)
+    /// <summary>
+    /// Attempts to retrieve the item held in the entity's active hand.
+    /// </summary>
+    public bool TryGetActiveItem(Entity<HandsComponent?> entity, [NotNullWhen(true)] out EntityUid? item)
     {
+        item = null;
         if (!Resolve(entity, ref entity.Comp, false))
-        {
-            hand = null;
             return false;
-        }
 
-        hand = entity.Comp.ActiveHand;
-        return hand != null;
-    }
-
-    public bool TryGetActiveItem(Entity<HandsComponent?> entity, [NotNullWhen(true)] out EntityUid? item)
-    {
-        if (!TryGetActiveHand(entity, out var hand))
-        {
-            item = null;
+        if (!TryGetHeldItem(entity, entity.Comp.ActiveHandId, out var held))
             return false;
-        }
 
-        item = hand.HeldEntity;
-        return item != null;
+        item = held;
+        return true;
     }
 
     /// <summary>
@@ -174,55 +195,73 @@ public abstract partial class SharedHandsSystem
         return item.Value;
     }
 
-    public Hand? GetActiveHand(Entity<HandsComponent?> entity)
+    /// <summary>
+    /// Gets the current active hand's Id for the specified entity
+    /// </summary>
+    /// <param name="entity"></param>
+    /// <returns></returns>
+    public string? GetActiveHand(Entity<HandsComponent?> entity)
     {
-        if (!Resolve(entity, ref entity.Comp))
+        if (!Resolve(entity, ref entity.Comp, false))
             return null;
 
-        return entity.Comp.ActiveHand;
+        return entity.Comp.ActiveHandId;
     }
 
+    /// <summary>
+    /// Gets the current active hand's held entity for the specified entity
+    /// </summary>
+    /// <param name="entity"></param>
+    /// <returns></returns>
     public EntityUid? GetActiveItem(Entity<HandsComponent?> entity)
     {
-        return GetActiveHand(entity)?.HeldEntity;
+        if (!Resolve(entity, ref entity.Comp, false))
+            return null;
+
+        return GetHeldItem(entity, entity.Comp.ActiveHandId);
+    }
+
+    public bool ActiveHandIsEmpty(Entity<HandsComponent?> entity)
+    {
+        return GetActiveItem(entity) == null;
     }
 
     /// <summary>
     ///     Enumerate over hands, starting with the currently active hand.
     /// </summary>
-    public IEnumerable<Hand> EnumerateHands(EntityUid uid, HandsComponent? handsComp = null)
+    public IEnumerable<string> EnumerateHands(Entity<HandsComponent?> ent)
     {
-        if (!Resolve(uid, ref handsComp, false))
+        if (!Resolve(ent, ref ent.Comp, false))
             yield break;
 
-        if (handsComp.ActiveHand != null)
-            yield return handsComp.ActiveHand;
+        if (ent.Comp.ActiveHandId != null)
+            yield return ent.Comp.ActiveHandId;
 
-        foreach (var name in handsComp.SortedHands)
+        foreach (var name in ent.Comp.SortedHands)
         {
-            if (name != handsComp.ActiveHand?.Name)
-                yield return handsComp.Hands[name];
+            if (name != ent.Comp.ActiveHandId)
+                yield return name;
         }
     }
 
     /// <summary>
     ///     Enumerate over held items, starting with the item in the currently active hand (if there is one).
     /// </summary>
-    public IEnumerable<EntityUid> EnumerateHeld(EntityUid uid, HandsComponent? handsComp = null)
+    public IEnumerable<EntityUid> EnumerateHeld(Entity<HandsComponent?> ent)
     {
-        if (!Resolve(uid, ref handsComp, false))
+        if (!Resolve(ent, ref ent.Comp, false))
             yield break;
 
-        if (handsComp.ActiveHandEntity != null)
-            yield return handsComp.ActiveHandEntity.Value;
+        if (TryGetActiveItem(ent, out var activeHeld))
+            yield return activeHeld.Value;
 
-        foreach (var name in handsComp.SortedHands)
+        foreach (var name in ent.Comp.SortedHands)
         {
-            if (name == handsComp.ActiveHand?.Name)
+            if (name == ent.Comp.ActiveHandId)
                 continue;
 
-            if (handsComp.Hands[name].HeldEntity is { } held)
-                yield return held;
+            if (TryGetHeldItem(ent, name, out var held))
+                yield return held.Value;
         }
     }
 
@@ -231,18 +270,17 @@ public abstract partial class SharedHandsSystem
     /// </summary>
     /// <returns>True if the active hand was set to a NEW value. Setting it to the same value returns false and does
     /// not trigger interactions.</returns>
-    public virtual bool TrySetActiveHand(EntityUid uid, string? name, HandsComponent? handComp = null)
+    public bool TrySetActiveHand(Entity<HandsComponent?> ent, string? name)
     {
-        if (!Resolve(uid, ref handComp))
+        if (!Resolve(ent, ref ent.Comp, false))
             return false;
 
-        if (name == handComp.ActiveHand?.Name)
+        if (name == ent.Comp.ActiveHandId)
             return false;
 
-        Hand? hand = null;
-        if (name != null && !handComp.Hands.TryGetValue(name, out hand))
+        if (name != null && !ent.Comp.Hands.ContainsKey(name))
             return false;
-        return SetActiveHand(uid, hand, handComp);
+        return SetActiveHand(ent, name);
     }
 
     /// <summary>
@@ -250,50 +288,50 @@ public abstract partial class SharedHandsSystem
     /// </summary>
     /// <returns>True if the active hand was set to a NEW value. Setting it to the same value returns false and does
     /// not trigger interactions.</returns>
-    public bool SetActiveHand(EntityUid uid, Hand? hand, HandsComponent? handComp = null)
+    public bool SetActiveHand(Entity<HandsComponent?> ent, string? handId)
     {
-        if (!Resolve(uid, ref handComp))
+        if (!Resolve(ent, ref ent.Comp))
             return false;
 
-        if (hand == handComp.ActiveHand)
+        if (handId == ent.Comp.ActiveHandId)
             return false;
 
-        if (handComp.ActiveHand?.HeldEntity is { } held)
-            RaiseLocalEvent(held, new HandDeselectedEvent(uid));
+        if (TryGetHeldItem(ent, handId, out var oldHeld))
+            RaiseLocalEvent(oldHeld.Value, new HandDeselectedEvent(ent));
 
-        if (hand == null)
+        if (handId == null)
         {
-            handComp.ActiveHand = null;
+            ent.Comp.ActiveHandId = null;
             return true;
         }
 
-        handComp.ActiveHand = hand;
-        OnHandSetActive?.Invoke((uid, handComp));
+        ent.Comp.ActiveHandId = handId;
+        OnHandSetActive?.Invoke((ent, ent.Comp));
 
-        if (hand.HeldEntity != null)
-            RaiseLocalEvent(hand.HeldEntity.Value, new HandSelectedEvent(uid));
+        if (TryGetHeldItem(ent, handId, out var newHeld))
+            RaiseLocalEvent(newHeld.Value, new HandSelectedEvent(ent));
 
-        Dirty(uid, handComp);
+        Dirty(ent);
         return true;
     }
 
     public bool IsHolding(Entity<HandsComponent?> entity, [NotNullWhen(true)] EntityUid? item)
     {
-        return IsHolding(entity, item, out _, entity);
+        return IsHolding(entity, item, out _);
     }
 
-    public bool IsHolding(EntityUid uid, [NotNullWhen(true)] EntityUid? entity, [NotNullWhen(true)] out Hand? inHand, HandsComponent? handsComp = null)
+    public bool IsHolding(Entity<HandsComponent?> ent, [NotNullWhen(true)] EntityUid? entity, [NotNullWhen(true)] out string? inHand)
     {
         inHand = null;
         if (entity == null)
             return false;
 
-        if (!Resolve(uid, ref handsComp, false))
+        if (!Resolve(ent, ref ent.Comp, false))
             return false;
 
-        foreach (var hand in handsComp.Hands.Values)
+        foreach (var hand in ent.Comp.Hands.Keys)
         {
-            if (hand.HeldEntity == entity)
+            if (GetHeldItem(ent, hand) == entity)
             {
                 inHand = hand;
                 return true;
@@ -303,23 +341,89 @@ public abstract partial class SharedHandsSystem
         return false;
     }
 
-    public bool TryGetHand(EntityUid handsUid, string handId, [NotNullWhen(true)] out Hand? hand,
-        HandsComponent? hands = null)
+    /// <summary>
+    /// Attempts to retrieve the associated hand struct corresponding to a hand ID on a given entity.
+    /// </summary>
+    public bool TryGetHand(Entity<HandsComponent?> ent, [NotNullWhen(true)] string? handId, [NotNullWhen(true)] out Hand? hand)
     {
         hand = null;
 
-        if (!Resolve(handsUid, ref hands))
+        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;
+    }
+
+    /// <summary>
+    /// Gets the item currently held in the entity's specified hand. Returns null if no hands are present or there is no item.
+    /// </summary>
+    public EntityUid? GetHeldItem(Entity<HandsComponent?> ent, string? handId)
+    {
+        TryGetHeldItem(ent, handId, out var held);
+        return held;
+    }
+
+    /// <summary>
+    /// Gets the item currently held in the entity's specified hand. Returns false if no hands are present or there is no item.
+    /// </summary>
+    public bool TryGetHeldItem(Entity<HandsComponent?> 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;
 
-        return hands.Hands.TryGetValue(handId, out hand);
+        if (!ContainerSystem.TryGetContainer(ent, handId, out var container))
+            return false;
+
+        held = container.ContainedEntities.FirstOrNull();
+        return held != null;
+    }
+
+    public bool HandIsEmpty(Entity<HandsComponent?> ent, string handId)
+    {
+        return GetHeldItem(ent, handId) == null;
+    }
+
+    public int GetHandCount(Entity<HandsComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
+            return 0;
+
+        return ent.Comp.Hands.Count;
+    }
+
+    public int CountFreeHands(Entity<HandsComponent?> 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<HandsComponent> hands)
     {
         var freeable = 0;
-        foreach (var hand in hands.Comp.Hands.Values)
+        foreach (var name in hands.Comp.Hands.Keys)
         {
-            if (hand.IsEmpty || CanDropHeld(hands, hand))
+            if (HandIsEmpty(hands.AsNullable(), name) || CanDropHeld(hands, name))
                 freeable++;
         }
 
index 2c75cef1325c77ffd3c0072a624b6847d6fe258a..b91f56a8360ed28552454c684050419d84099092 100644 (file)
@@ -9,6 +9,7 @@ using Content.Shared.Database;
 using Content.Shared.Ghost;
 using Content.Shared.Hands;
 using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Input;
 using Content.Shared.Interaction.Components;
 using Content.Shared.Interaction.Events;
@@ -58,6 +59,7 @@ namespace Content.Shared.Interaction
         [Dependency] private readonly ISharedChatManager _chat = default!;
         [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
         [Dependency] private readonly EntityLookupSystem _lookup = default!;
+        [Dependency] private readonly SharedHandsSystem _hands = default!;
         [Dependency] private readonly InventorySystem _inventory = default!;
         [Dependency] private readonly PullingSystem _pullSystem = default!;
         [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
@@ -343,7 +345,7 @@ namespace Content.Shared.Interaction
         public bool CombatModeCanHandInteract(EntityUid user, EntityUid? target)
         {
             // Always allow attack in these cases
-            if (target == null || !_handsQuery.TryComp(user, out var hands) || hands.ActiveHand?.HeldEntity is not null)
+            if (target == null || !_handsQuery.TryComp(user, out var hands) || _hands.GetActiveItem((user, hands)) is not null)
                 return false;
 
             // Only eat input if:
index 746bc994ee16c05d440551900f997031c6d050c0..797dc367b466a7e2165a9699367b1e85d1cc1639 100644 (file)
@@ -64,10 +64,10 @@ public sealed class SmartEquipSystem : EntitySystem
             return;
 
         // early out if we don't have any hands or a valid inventory slot
-        if (!TryComp<HandsComponent>(uid, out var hands) || hands.ActiveHand == null)
+        if (!TryComp<HandsComponent>(uid, out var hands) || hands.ActiveHandId == null)
             return;
 
-        var handItem = hands.ActiveHand.HeldEntity;
+        var handItem = _hands.GetActiveItem((uid, hands));
 
         // can the user interact, and is the item interactable? e.g. virtual items
         if (!_actionBlocker.CanInteract(uid, handItem))
@@ -80,7 +80,7 @@ public sealed class SmartEquipSystem : EntitySystem
         }
 
         // early out if we have an item and cant drop it at all
-        if (handItem != null && !_hands.CanDropHeld(uid, hands.ActiveHand))
+        if (hands.ActiveHandId != null && !_hands.CanDropHeld(uid, hands.ActiveHandId))
         {
             _popup.PopupClient(Loc.GetString("smart-equip-cant-drop"), uid, uid);
             return;
@@ -121,7 +121,7 @@ public sealed class SmartEquipSystem : EntitySystem
                 return;
             }
 
-            _hands.TryDrop(uid, hands.ActiveHand, handsComp: hands);
+            _hands.TryDrop((uid, hands), hands.ActiveHandId!);
             _inventory.TryEquip(uid, handItem.Value, equipmentSlot, predicted: true, checkDoafter:true);
             return;
         }
@@ -149,7 +149,7 @@ public sealed class SmartEquipSystem : EntitySystem
                 return;
             }
 
-            _hands.TryDrop(uid, hands.ActiveHand, handsComp: hands);
+            _hands.TryDrop((uid, hands), hands.ActiveHandId!);
             _storage.Insert(slotItem, handItem.Value, out var stacked, out _);
 
             // if the hand item stacked with the things in inventory, but there's no more space left for the rest
index f2fb3987b793d491dfc2f11449dd937bf63c6550..de24a64a1c14218592e9dcf74f9b595cc3aa9dad 100644 (file)
@@ -83,7 +83,7 @@ public abstract partial class InventorySystem
         if (!TryComp(actor, out InventoryComponent? inventory) || !TryComp<HandsComponent>(actor, out var hands))
             return;
 
-        var held = hands.ActiveHandEntity;
+        var held = _handsSystem.GetActiveItem((actor, hands));
         TryGetSlotEntity(actor, ev.Slot, out var itemUid, inventory);
 
         // attempt to perform some interaction
@@ -115,7 +115,7 @@ public abstract partial class InventorySystem
             return;
         }
 
-        if (!_handsSystem.CanDropHeld(actor, hands.ActiveHand!, checkActionBlocker: false))
+        if (!_handsSystem.CanDropHeld(actor, hands.ActiveHandId!, checkActionBlocker: false))
             return;
 
         RaiseLocalEvent(held.Value, new HandDeselectedEvent(actor));
index 746342e2f15455c9ffcfc4855e28477b56247191..44dede02c76e77bcf8fb7a0faaf711658e91b143 100644 (file)
@@ -16,12 +16,9 @@ public partial class InventorySystem
     {
         if (Resolve(user.Owner, ref user.Comp1, false))
         {
-            foreach (var hand in user.Comp1.Hands.Values)
+            foreach (var held in _handsSystem.EnumerateHeld(user))
             {
-                if (hand.HeldEntity == null)
-                    continue;
-
-                yield return hand.HeldEntity.Value;
+                yield return held;
             }
         }
 
index 779ad1878983d924b62829b44a0df15931176561..d76a4a3415e6ce538113a66fd34b833ed90c9a08 100644 (file)
@@ -1,6 +1,5 @@
 using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Hands;
-using Content.Shared.Hands.Components;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
 using Content.Shared.Interaction.Events;
@@ -8,7 +7,6 @@ using Content.Shared.Inventory.Events;
 using Content.Shared.Item;
 using Content.Shared.Popups;
 using Robust.Shared.Containers;
-using Robust.Shared.Network;
 using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Inventory.VirtualItem;
@@ -88,9 +86,9 @@ public abstract class SharedVirtualItemSystem : EntitySystem
 
         // if the user is holding the real item the virtual item points to,
         // we allow them to use it in the interaction
-        foreach (var hand in _handsSystem.EnumerateHands(args.User))
+        foreach (var held in _handsSystem.EnumerateHeld(args.User))
         {
-            if (hand.HeldEntity == ent.Comp.BlockingEntity)
+            if (held == ent.Comp.BlockingEntity)
             {
                 args.Used = ent.Comp.BlockingEntity;
                 return;
@@ -112,7 +110,7 @@ public abstract class SharedVirtualItemSystem : EntitySystem
     }
 
     /// <inheritdoc cref="TrySpawnVirtualItemInHand(Robust.Shared.GameObjects.EntityUid,Robust.Shared.GameObjects.EntityUid,bool)"/>
-    public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user, [NotNullWhen(true)] out EntityUid? virtualItem, bool dropOthers = false, Hand? empty = null)
+    public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user, [NotNullWhen(true)] out EntityUid? virtualItem, bool dropOthers = false, string? empty = null)
     {
         virtualItem = null;
         if (empty == null && !_handsSystem.TryGetEmptyHand(user, out empty))
@@ -122,7 +120,7 @@ public abstract class SharedVirtualItemSystem : EntitySystem
 
             foreach (var hand in _handsSystem.EnumerateHands(user))
             {
-                if (hand.HeldEntity is not { } held)
+                if (!_handsSystem.TryGetHeldItem(user, hand, out var held))
                     continue;
 
                 if (held == blockingEnt)
@@ -155,11 +153,11 @@ public abstract class SharedVirtualItemSystem : EntitySystem
     /// </summary>
     public void DeleteInHandsMatching(EntityUid user, EntityUid matching)
     {
-        foreach (var hand in _handsSystem.EnumerateHands(user))
+        foreach (var held in _handsSystem.EnumerateHeld(user))
         {
-            if (TryComp(hand.HeldEntity, out VirtualItemComponent? virt) && virt.BlockingEntity == matching)
+            if (TryComp(held, out VirtualItemComponent? virt) && virt.BlockingEntity == matching)
             {
-                DeleteVirtualItem((hand.HeldEntity.Value, virt), user);
+                DeleteVirtualItem((held, virt), user);
             }
         }
     }
index da9d895dd2a9ae1cba6379536ab4fd7727fdaf3f..f17ccdc922a48cb0f02d57472491195de8fe70f0 100644 (file)
@@ -1,5 +1,4 @@
 using Content.Shared.Hands;
-using Content.Shared.Hands.Components;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Inventory.VirtualItem;
 using Content.Shared.Popups;
@@ -38,7 +37,7 @@ public sealed class MultiHandedItemSystem : EntitySystem
 
     private void OnAttemptPickup(Entity<MultiHandedItemComponent> ent, ref GettingPickedUpAttemptEvent args)
     {
-        if (TryComp<HandsComponent>(args.User, out var hands) && hands.CountFreeHands() >= ent.Comp.HandsNeeded)
+        if (_hands.CountFreeHands(ent.Owner) >= ent.Comp.HandsNeeded)
             return;
 
         args.Cancel();
index c277bb7e877ba0e42077388ffb3728fb17cf513e..9319755501f783a7e51621cf1138848aa033a335 100644 (file)
@@ -110,7 +110,7 @@ public abstract class SharedItemSystem : EntitySystem
         if (args.Handled)
             return;
 
-        args.Handled = _handsSystem.TryPickup(args.User, uid, animateUser: false);
+        args.Handled = _handsSystem.TryPickup(args.User, uid, null, animateUser: false);
     }
 
     private void AddPickupVerb(EntityUid uid, ItemComponent component, GetVerbsEvent<InteractionVerb> args)
index 09b289c2be956c42313008756a9644cfe4d3669b..a860f1dcfb7287bd845bd51a2b283076ba68d88f 100644 (file)
@@ -434,7 +434,7 @@ public abstract class SharedMagicSystem : EntitySystem
             return;
 
         EntityUid? wand = null;
-        foreach (var item in _hands.EnumerateHeld(ev.Performer, handsComp))
+        foreach (var item in _hands.EnumerateHeld((ev.Performer, handsComp)))
         {
             if (!_tag.HasTag(item, ev.WandTag))
                 continue;
index bc505ee98947b03ea1a7cc77930bc1edd575b646..e1c07df3c34c37bb5fde03ef01f8302b3536dda3 100644 (file)
@@ -109,16 +109,12 @@ public sealed class PullingSystem : EntitySystem
 
         // Try find hand that is doing this pull.
         // and clear it.
-        foreach (var hand in component.Hands.Values)
+        foreach (var held in _handsSystem.EnumerateHeld((uid, component)))
         {
-            if (hand.HeldEntity == null
-                || !TryComp(hand.HeldEntity, out VirtualItemComponent? virtualItem)
-                || virtualItem.BlockingEntity != args.PulledUid)
-            {
+            if (!TryComp(held, out VirtualItemComponent? virtualItem) || virtualItem.BlockingEntity != args.PulledUid)
                 continue;
-            }
 
-            _handsSystem.TryDrop(args.PullerUid, hand, handsComp: component);
+            _handsSystem.TryDrop((args.PullerUid, component), held);
             break;
         }
     }
index 8b892190b7b6d1f489a2fb5e6530e8d1ab0418e3..1f3cc3881d97dc51b8bc04be019eaeda74f59963 100644 (file)
@@ -2,6 +2,7 @@ using Content.Shared.Clothing.Components;
 using Content.Shared.CombatMode;
 using Content.Shared.Examine;
 using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
 using Content.Shared.Inventory.Events;
 using Content.Shared.Item.ItemToggle;
@@ -19,6 +20,7 @@ public abstract class SharedNinjaGlovesSystem : EntitySystem
 {
     [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly SharedInteractionSystem _interaction = default!;
     [Dependency] private readonly ItemToggleSystem _toggle = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -128,8 +130,7 @@ public abstract class SharedNinjaGlovesSystem : EntitySystem
         target = args.Target;
         return _timing.IsFirstTimePredicted
             && !_combatMode.IsInCombatMode(uid)
-            && TryComp<HandsComponent>(uid, out var hands)
-            && hands.ActiveHandEntity == null
+            && _hands.GetActiveItem(uid) == null
             && _interaction.InRangeUnobstructed(uid, target);
     }
 }
index a4122168e4c0bf1c2b53160db926fab512c7b328..6099939725daa1fc60aaf46307b26299fc7deada 100644 (file)
@@ -459,7 +459,7 @@ public sealed class FoodSystem : EntitySystem
 
         var usedTypes = UtensilType.None;
 
-        foreach (var item in _hands.EnumerateHeld(user, hands))
+        foreach (var item in _hands.EnumerateHeld((user, hands)))
         {
             // Is utensil?
             if (!TryComp<UtensilComponent>(item, out var utensil))
index 6d85b171e09d6b780f1c5d72561feae4eb90178b..68056ef267beb63022c2d53db2cc50cec29a058f 100644 (file)
@@ -5,7 +5,7 @@ using Content.Shared.Construction;
 using Content.Shared.Database;
 using Content.Shared.DoAfter;
 using Content.Shared.Examine;
-using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
 using Content.Shared.Maps;
 using Content.Shared.Physics;
@@ -35,6 +35,7 @@ public sealed class RCDSystem : EntitySystem
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
     [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly SharedInteractionSystem _interaction = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
     [Dependency] private readonly TurfSystem _turf = default!;
@@ -296,11 +297,10 @@ public sealed class RCDSystem : EntitySystem
         var uid = GetEntity(ev.NetEntity);
 
         // Determine if player that send the message is carrying the specified RCD in their active hand
-        if (session.SenderSession.AttachedEntity == null)
+        if (session.SenderSession.AttachedEntity is not { } player)
             return;
 
-        if (!TryComp<HandsComponent>(session.SenderSession.AttachedEntity, out var hands) ||
-            uid != hands.ActiveHand?.HeldEntity)
+        if (_hands.GetActiveItem(player) != uid)
             return;
 
         if (!TryComp<RCDComponent>(uid, out var rcd))
index c24da14c682aa223f8d657dd8e9a36ae015c08b0..1820541746688a240fc769e49463ad6dd9409f26 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Shared.Actions;
 using Content.Shared.Cuffs;
 using Content.Shared.Hands;
-using Content.Shared.Hands.Components;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction.Components;
 using Content.Shared.Inventory;
@@ -42,7 +41,7 @@ public sealed class RetractableItemActionSystem : EntitySystem
 
     private void OnRetractableItemAction(Entity<RetractableItemActionComponent> ent, ref OnRetractableItemActionEvent args)
     {
-        if (_hands.GetActiveHand(args.Performer) is not { } userHand)
+        if (_hands.GetActiveHand(args.Performer) is not { } activeHand)
             return;
 
         if (_actions.GetAction(ent.Owner) is not { } action)
@@ -55,7 +54,9 @@ public sealed class RetractableItemActionSystem : EntitySystem
             return;
 
         // Don't allow to summon an item if holding an unremoveable item unless that item is summoned by the action.
-        if (userHand.HeldEntity != null && !_hands.IsHolding(args.Performer, ent.Comp.ActionItemUid) && !_hands.CanDropHeld(args.Performer, userHand, false))
+        if (_hands.GetActiveItem(ent.Owner) != null
+            && !_hands.IsHolding(args.Performer, ent.Comp.ActionItemUid)
+            && !_hands.CanDropHeld(args.Performer, activeHand, false))
         {
             _popups.PopupClient(Loc.GetString("retractable-item-hand-cannot-drop"), args.Performer, args.Performer);
             return;
@@ -67,7 +68,7 @@ public sealed class RetractableItemActionSystem : EntitySystem
         }
         else
         {
-            SummonRetractableItem(args.Performer, ent.Comp.ActionItemUid.Value, userHand, ent.Owner);
+            SummonRetractableItem(args.Performer, ent.Comp.ActionItemUid.Value, activeHand, ent.Owner);
         }
 
         args.Handled = true;
@@ -93,7 +94,7 @@ public sealed class RetractableItemActionSystem : EntitySystem
         if (action.Comp.AttachedEntity == null)
             return;
 
-        if (_hands.GetActiveHand(action.Comp.AttachedEntity.Value) is not { } userHand)
+        if (_hands.GetActiveHand(action.Comp.AttachedEntity.Value) is not { })
             return;
 
         RetractRetractableItem(action.Comp.AttachedEntity.Value, ent, action.Owner);
@@ -128,7 +129,7 @@ public sealed class RetractableItemActionSystem : EntitySystem
         _audio.PlayPredicted(action.Comp.RetractSounds, holder, holder);
     }
 
-    private void SummonRetractableItem(EntityUid holder, EntityUid item, Hand hand, Entity<RetractableItemActionComponent?> action)
+    private void SummonRetractableItem(EntityUid holder, EntityUid item, string hand, Entity<RetractableItemActionComponent?> action)
     {
         if (!Resolve(action, ref action.Comp, false))
             return;
index c444b8936d853576c68265c1563ce80bfbe44a9f..cd2f38d47f6abf0bc8c7ebcce3c8c5d61ae8d4fd 100644 (file)
@@ -146,7 +146,7 @@ namespace Content.Shared.Stacks
             }
 
             // This is shit code until hands get fixed and give an easy way to enumerate over items, starting with the currently active item.
-            foreach (var held in Hands.EnumerateHeld(user, hands))
+            foreach (var held in Hands.EnumerateHeld((user, hands)))
             {
                 TryMergeStacks(item, held, out _, donorStack: itemStack);
 
index 389f696db28dd7d925d831b0b777bf3e8ce37e8c..a0a2257402bb59d37e78315a2faf7bd200acab8b 100644 (file)
@@ -141,7 +141,7 @@ public abstract class SharedStationSpawningSystem : EntitySystem
             {
                 var inhandEntity = EntityManager.SpawnEntity(prototype, coords);
 
-                if (_handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
+                if (_handsSystem.TryGetEmptyHand((entity, handsComponent), out var emptyHand))
                 {
                     _handsSystem.TryPickup(entity, inhandEntity, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
                 }
index 8143cb9cdd1c07dc0ef2689b280b8aab0aaf009d..adba19e0470c464d33554a762093b4ac1efc3e11 100644 (file)
@@ -689,7 +689,7 @@ public abstract class SharedStorageSystem : EntitySystem
             return;
 
         // If the user's active hand is empty, try pick up the item.
-        if (player.Comp.ActiveHandEntity == null)
+        if (!_sharedHandsSystem.TryGetActiveItem(player.AsNullable(), out var activeItem))
         {
             _adminLog.Add(
                 LogType.Storage,
@@ -709,11 +709,11 @@ public abstract class SharedStorageSystem : EntitySystem
         _adminLog.Add(
             LogType.Storage,
             LogImpact.Low,
-            $"{ToPrettyString(player):player} is interacting with {ToPrettyString(item):item} while it is stored in {ToPrettyString(storage):storage} using {ToPrettyString(player.Comp.ActiveHandEntity):used}");
+            $"{ToPrettyString(player):player} is interacting with {ToPrettyString(item):item} while it is stored in {ToPrettyString(storage):storage} using {ToPrettyString(activeItem):used}");
 
         // Else, interact using the held item
         if (_interactionSystem.InteractUsing(player,
-                player.Comp.ActiveHandEntity.Value,
+                activeItem.Value,
                 item,
                 Transform(item).Coordinates,
                 checkCanInteract: false))
@@ -1208,10 +1208,10 @@ public abstract class SharedStorageSystem : EntitySystem
     {
         if (!Resolve(ent.Owner, ref ent.Comp)
             || !Resolve(player.Owner, ref player.Comp)
-            || player.Comp.ActiveHandEntity == null)
+            || !_sharedHandsSystem.TryGetActiveItem(player, out var activeItem))
             return false;
 
-        var toInsert = player.Comp.ActiveHandEntity;
+        var toInsert = activeItem;
 
         if (!CanInsert(ent, toInsert.Value, out var reason, ent.Comp))
         {
@@ -1219,7 +1219,7 @@ public abstract class SharedStorageSystem : EntitySystem
             return false;
         }
 
-        if (!_sharedHandsSystem.CanDrop(player, toInsert.Value, player.Comp))
+        if (!_sharedHandsSystem.CanDrop(player, toInsert.Value))
         {
             _popupSystem.PopupClient(Loc.GetString("comp-storage-cant-drop", ("entity", toInsert.Value)), ent, player);
             return false;
@@ -1933,7 +1933,7 @@ public abstract class SharedStorageSystem : EntitySystem
 
         if (held)
         {
-            if (!_sharedHandsSystem.IsHolding(player, itemUid, out _))
+            if (!_sharedHandsSystem.IsHolding(player.AsNullable(), itemUid, out _))
                 return false;
         }
         else
index 2b971ae7bbf2310552103c3e46b970dc03dd7f2c..49be1805033be604b8cb09586ee02d26e4284a83 100644 (file)
@@ -104,8 +104,8 @@ public abstract class SharedStrippableSystem : EntitySystem
 
         var hasEnt = _inventorySystem.TryGetSlotEntity(strippable, args.Slot, out var held, inventory);
 
-        if (userHands.ActiveHandEntity != null && !hasEnt)
-            StartStripInsertInventory((user, userHands), strippable.Owner, userHands.ActiveHandEntity.Value, args.Slot);
+        if (_handsSystem.GetActiveItem((user, userHands)) is { } activeItem && !hasEnt)
+            StartStripInsertInventory((user, userHands), strippable.Owner, activeItem, args.Slot);
         else if (hasEnt)
             StartStripRemoveInventory(user, strippable.Owner, held!.Value, args.Slot);
     }
@@ -124,11 +124,10 @@ public abstract class SharedStrippableSystem : EntitySystem
         if (!target.Comp.CanBeStripped)
             return;
 
-        if (!_handsSystem.TryGetHand(target.Owner, handId, out var handSlot))
-            return;
+        var heldEntity = _handsSystem.GetHeldItem(target.Owner, handId);
 
         // Is the target a handcuff?
-        if (TryComp<VirtualItemComponent>(handSlot.HeldEntity, out var virtualItem) &&
+        if (TryComp<VirtualItemComponent>(heldEntity, out var virtualItem) &&
             TryComp<CuffableComponent>(target.Owner, out var cuffable) &&
             _cuffableSystem.GetAllCuffs(cuffable).Contains(virtualItem.BlockingEntity))
         {
@@ -136,10 +135,10 @@ public abstract class SharedStrippableSystem : EntitySystem
             return;
         }
 
-        if (user.Comp.ActiveHandEntity != null && handSlot.HeldEntity == null)
-            StartStripInsertHand(user, target, user.Comp.ActiveHandEntity.Value, handId, targetStrippable);
-        else if (handSlot.HeldEntity != null)
-            StartStripRemoveHand(user, target, handSlot.HeldEntity.Value, handId, targetStrippable);
+        if (_handsSystem.GetActiveItem(user.AsNullable()) is { } activeItem && heldEntity == null)
+            StartStripInsertHand(user, target, activeItem, handId, targetStrippable);
+        else if (heldEntity != null)
+            StartStripRemoveHand(user, target, heldEntity.Value, handId, targetStrippable);
     }
 
     /// <summary>
@@ -154,16 +153,10 @@ public abstract class SharedStrippableSystem : EntitySystem
         if (!Resolve(user, ref user.Comp))
             return false;
 
-        if (user.Comp.ActiveHand == null)
-            return false;
-
-        if (user.Comp.ActiveHandEntity == null)
-            return false;
-
-        if (user.Comp.ActiveHandEntity != held)
+        if (!_handsSystem.TryGetActiveItem(user, out var activeItem) || activeItem != held)
             return false;
 
-        if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHand))
+        if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHandId!))
         {
             _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"));
             return false;
@@ -210,10 +203,14 @@ public abstract class SharedStrippableSystem : EntitySystem
         var (time, stealth) = GetStripTimeModifiers(user, target, held, slotDef.StripTime);
 
         if (!stealth)
+        {
             _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert",
                                                         ("user", Identity.Entity(user, EntityManager)),
-                                                        ("item", user.Comp.ActiveHandEntity!.Value)),
-                                                        target, target, PopupType.Large);
+                                                        ("item", _handsSystem.GetActiveItem((user, user.Comp))!.Value)),
+                                                        target,
+                                                        target,
+                                                        PopupType.Large);
+        }
 
         var prefix = stealth ? "stealthily " : "";
         _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot");
@@ -246,7 +243,7 @@ public abstract class SharedStrippableSystem : EntitySystem
         if (!CanStripInsertInventory(user, target, held, slot))
             return;
 
-        if (!_handsSystem.TryDrop(user, handsComp: user.Comp))
+        if (!_handsSystem.TryDrop(user))
             return;
 
         _inventorySystem.TryEquip(user, target, held, slot, triggerHandContact: true);
@@ -305,10 +302,15 @@ public abstract class SharedStrippableSystem : EntitySystem
             if (IsStripHidden(slotDef, user))
                 _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, target, PopupType.Large);
             else
+            {
                 _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner",
                                                             ("user", Identity.Entity(user, EntityManager)),
                                                             ("item", item)),
-                                                            target, target, PopupType.Large);
+                                                            target,
+                                                            target,
+                                                            PopupType.Large);
+
+            }
         }
 
         var prefix = stealth ? "stealthily " : "";
@@ -368,23 +370,16 @@ public abstract class SharedStrippableSystem : EntitySystem
         if (!target.Comp.CanBeStripped)
             return false;
 
-        if (user.Comp.ActiveHand == null)
+        if (!_handsSystem.TryGetActiveItem(user, out var activeItem) || activeItem != held)
             return false;
 
-        if (user.Comp.ActiveHandEntity == null)
-            return false;
-
-        if (user.Comp.ActiveHandEntity != held)
-            return false;
-
-        if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHand))
+        if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHandId!))
         {
             _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"));
             return false;
         }
 
-        if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp) ||
-            !_handsSystem.CanPickupToHand(target, user.Comp.ActiveHandEntity.Value, handSlot, checkActionBlocker: false, target.Comp))
+        if (!_handsSystem.CanPickupToHand(target, activeItem.Value, handName, checkActionBlocker: false, target.Comp))
         {
             _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-put-message", ("owner", Identity.Entity(target, EntityManager))));
             return false;
@@ -414,10 +409,15 @@ public abstract class SharedStrippableSystem : EntitySystem
         var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay);
 
         if (!stealth)
+        {
             _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert-hand",
                                                         ("user", Identity.Entity(user, EntityManager)),
-                                                        ("item", user.Comp.ActiveHandEntity!.Value)),
-                                                        target, target, PopupType.Large);
+                                                        ("item", _handsSystem.GetActiveItem(user)!.Value)),
+                                                        target,
+                                                        target,
+                                                        PopupType.Large);
+
+        }
 
         var prefix = stealth ? "stealthily " : "";
         _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
@@ -452,7 +452,7 @@ public abstract class SharedStrippableSystem : EntitySystem
         if (!CanStripInsertHand(user, target, held, handName))
             return;
 
-        _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: user.Comp);
+        _handsSystem.TryDrop(user, checkActionBlocker: false);
         _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: stealth, animate: !stealth, handsComp: target.Comp);
         _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
 
@@ -474,22 +474,22 @@ public abstract class SharedStrippableSystem : EntitySystem
         if (!target.Comp.CanBeStripped)
             return false;
 
-        if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp))
+        if (!_handsSystem.TryGetHand(target, handName, out _))
         {
             _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", Identity.Entity(target, EntityManager))));
             return false;
         }
 
-        if (HasComp<VirtualItemComponent>(handSlot.HeldEntity))
+        if (!_handsSystem.TryGetHeldItem(target, handName, out var heldEntity))
             return false;
 
-        if (handSlot.HeldEntity == null)
+        if (HasComp<VirtualItemComponent>(heldEntity))
             return false;
 
-        if (handSlot.HeldEntity != item)
+        if (heldEntity != item)
             return false;
 
-        if (!_handsSystem.CanDropHeld(target, handSlot, false))
+        if (!_handsSystem.CanDropHeld(target, handName, false))
         {
             _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop-message", ("owner", Identity.Entity(target, EntityManager))));
             return false;
@@ -519,10 +519,13 @@ public abstract class SharedStrippableSystem : EntitySystem
         var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay);
 
         if (!stealth)
+        {
             _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner",
                                                         ("user", Identity.Entity(user, EntityManager)),
                                                         ("item", item)),
-                                                        target, target);
+                                                        target,
+                                                        target);
+        }
 
         var prefix = stealth ? "stealthily " : "";
         _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
@@ -560,7 +563,7 @@ public abstract class SharedStrippableSystem : EntitySystem
         if (!CanStripRemoveHand(user, target, item, handName))
             return;
 
-        _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: target.Comp);
+        _handsSystem.TryDrop(target, item, checkActionBlocker: false);
         _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: !stealth, handsComp: user.Comp);
         _adminLogger.Add(LogType.Stripping, LogImpact.High, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
 
@@ -580,13 +583,17 @@ public abstract class SharedStrippableSystem : EntitySystem
         {
             if ( ev.Event.InsertOrRemove && !CanStripInsertInventory((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
                 !ev.Event.InsertOrRemove && !CanStripRemoveInventory(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName))
-                    ev.Cancel();
+            {
+                ev.Cancel();
+            }
         }
         else
         {
             if ( ev.Event.InsertOrRemove && !CanStripInsertHand((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
                 !ev.Event.InsertOrRemove && !CanStripRemoveHand(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName))
-                    ev.Cancel();
+            {
+                ev.Cancel();
+            }
         }
     }
 
index 7eb195c0b19d31fb8d21f12621dd4a3254fbc437..a3f9a033df214ba90cd3fd1e82d3488e91b5abe0 100644 (file)
@@ -108,10 +108,10 @@ public sealed partial class ActivatableUISystem : EntitySystem
 
             if (component.InHandsOnly)
             {
-                if (!_hands.IsHolding(args.User, uid, out var hand, args.Hands))
+                if (!_hands.IsHolding((args.User, args.Hands), uid, out var hand ))
                     return false;
 
-                if (component.RequireActiveHand && args.Hands.ActiveHand != hand)
+                if (component.RequireActiveHand && args.Hands.ActiveHandId != hand)
                     return false;
             }
         }
@@ -202,10 +202,10 @@ public sealed partial class ActivatableUISystem : EntitySystem
             if (!TryComp(user, out HandsComponent? hands))
                 return false;
 
-            if (!_hands.IsHolding(user, uiEntity, out var hand, hands))
+            if (!_hands.IsHolding((user, hands), uiEntity, out var hand))
                 return false;
 
-            if (aui.RequireActiveHand && hands.ActiveHand != hand)
+            if (aui.RequireActiveHand && hands.ActiveHandId != hand)
                 return false;
         }
 
index cf9120f5f331b7257ed3dae830ca33318c2c799e..953abc81077038163f138c6c72e073bc528627e5 100644 (file)
@@ -12,6 +12,7 @@ using Content.Shared.Database;
 using Content.Shared.FixedPoint;
 using Content.Shared.Hands;
 using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
 using Content.Shared.Inventory;
@@ -51,6 +52,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
     [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
     [Dependency] protected readonly ActionBlockerSystem Blocker = default!;
     [Dependency] protected readonly DamageableSystem Damageable = default!;
+    [Dependency] private   readonly SharedHandsSystem _hands = default!;
     [Dependency] private   readonly InventorySystem _inventory = default!;
     [Dependency] private   readonly MeleeSoundSystem _meleeSound = default!;
     [Dependency] protected readonly MobStateSystem MobState = default!;
@@ -288,15 +290,14 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
         }
 
         // Use inhands entity if we got one.
-        if (EntityManager.TryGetComponent(entity, out HandsComponent? hands) &&
-            hands.ActiveHandEntity is { } held)
+        if (_hands.TryGetActiveItem(entity, out var held))
         {
             // Make sure the entity is a weapon AND it doesn't need
             // to be equipped to be used (E.g boxing gloves).
             if (EntityManager.TryGetComponent(held, out melee) &&
                 !melee.MustBeEquippedToUse)
             {
-                weaponUid = held;
+                weaponUid = held.Value;
                 return true;
             }
 
@@ -858,9 +859,9 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
 
         EntityUid? inTargetHand = null;
 
-        if (targetHandsComponent?.ActiveHand is { IsEmpty: false })
+        if (_hands.TryGetActiveItem(target.Value, out var activeHeldEntity))
         {
-            inTargetHand = targetHandsComponent.ActiveHand.HeldEntity!.Value;
+            inTargetHand = activeHeldEntity.Value;
         }
 
         var attemptEvent = new DisarmAttemptEvent(target.Value, user, inTargetHand);
index e790973538ffb6915ed33aeb886d632563edb498..d27efa4d76a76ba4f16f468aa67100cdf22573e8 100644 (file)
@@ -1,7 +1,7 @@
 using System.Numerics;
 using Content.Shared.CombatMode;
 using Content.Shared.Hands;
-using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
 using Content.Shared.Movement.Events;
 using Content.Shared.Physics;
@@ -25,6 +25,7 @@ public abstract class SharedGrapplingGunSystem : EntitySystem
     [Dependency] private readonly INetManager _netManager = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly SharedJointSystem _joints = default!;
     [Dependency] private readonly SharedGunSystem _gun = default!;
     [Dependency] private readonly SharedPhysicsSystem _physics = default!;
@@ -80,9 +81,11 @@ public abstract class SharedGrapplingGunSystem : EntitySystem
 
     private void OnGrapplingReel(RequestGrapplingReelMessage msg, EntitySessionEventArgs args)
     {
-        var player = args.SenderSession.AttachedEntity;
-        if (!TryComp<HandsComponent>(player, out var hands) ||
-            !TryComp<GrapplingGunComponent>(hands.ActiveHandEntity, out var grappling))
+        if (args.SenderSession.AttachedEntity is not { } player)
+            return;
+
+        if (!_hands.TryGetActiveItem(player, out var activeItem) ||
+            !TryComp<GrapplingGunComponent>(activeItem, out var grappling))
         {
             return;
         }
@@ -94,7 +97,7 @@ public abstract class SharedGrapplingGunSystem : EntitySystem
             return;
         }
 
-        SetReeling(hands.ActiveHandEntity.Value, grappling, msg.Reeling, player.Value);
+        SetReeling(activeItem.Value, grappling, msg.Reeling, player);
     }
 
     private void OnWeightlessMove(ref CanWeightlessMoveEvent ev)
index f6702569bfaad2a854b50dc974cd66129af89c6c..0f6baf06f257fdaf57c28e2225ab82f6ecfbca43 100644 (file)
@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Buckle.Components;
 using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Movement.Events;
@@ -23,6 +24,7 @@ public abstract partial class SharedTetherGunSystem : EntitySystem
 {
     [Dependency] private readonly INetManager _netManager = default!;
     [Dependency] private readonly ActionBlockerSystem _blocker = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly MobStateSystem _mob = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -139,14 +141,14 @@ public abstract partial class SharedTetherGunSystem : EntitySystem
         gunUid = null;
         gun = null;
 
-        if (!TryComp<HandsComponent>(user, out var hands) ||
-            !TryComp(hands.ActiveHandEntity, out gun) ||
+        if (!_hands.TryGetActiveItem(user, out var activeItem) ||
+            !TryComp(activeItem, out gun) ||
             _container.IsEntityInContainer(user))
         {
             return false;
         }
 
-        gunUid = hands.ActiveHandEntity.Value;
+        gunUid = activeItem.Value;
         return true;
     }
 
index bebfe6f92290bad8e14370cda31df029d6b3af18..b6dee7fc1bae45d1d02866c1c66cfdf80bc28f69 100644 (file)
@@ -10,7 +10,7 @@ using Content.Shared.Damage;
 using Content.Shared.Examine;
 using Content.Shared.Gravity;
 using Content.Shared.Hands;
-using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Popups;
 using Content.Shared.Projectiles;
 using Content.Shared.Tag;
@@ -49,6 +49,7 @@ public abstract partial class SharedGunSystem : EntitySystem
     [Dependency] protected readonly ISharedAdminLogManager Logs = default!;
     [Dependency] protected readonly DamageableSystem Damageable = default!;
     [Dependency] protected readonly ExamineSystemShared Examine = default!;
+    [Dependency] private   readonly SharedHandsSystem _hands = default!;
     [Dependency] private   readonly ItemSlotsSystem _slots = default!;
     [Dependency] private   readonly RechargeBasicEntityAmmoSystem _recharge = default!;
     [Dependency] protected readonly SharedActionsSystem Actions = default!;
@@ -173,8 +174,7 @@ public abstract partial class SharedGunSystem : EntitySystem
         gunEntity = default;
         gunComp = null;
 
-        if (EntityManager.TryGetComponent(entity, out HandsComponent? hands) &&
-            hands.ActiveHandEntity is { } held &&
+        if (_hands.GetActiveItem(entity) is { } held &&
             TryComp(held, out GunComponent? gun))
         {
             gunEntity = held;
index 83ed58388a10205d6522d9ab459c32df9002971b..c3375c3fe1bcc23f103b33a8ed78735bbccc86f1 100644 (file)
@@ -112,7 +112,7 @@ public abstract class SharedWieldableSystem : EntitySystem
 
     private void OnDeselectWieldable(EntityUid uid, WieldableComponent component, HandDeselectedEvent args)
     {
-        if (_hands.EnumerateHands(args.User).Count() > 2)
+        if (_hands.GetHandCount(uid) > 2)
             return;
 
         TryUnwield(uid, component, args.User);
@@ -168,7 +168,7 @@ public abstract class SharedWieldableSystem : EntitySystem
         if (args.Hands == null || !args.CanAccess || !args.CanInteract)
             return;
 
-        if (!_hands.IsHolding(args.User, uid, out _, args.Hands))
+        if (!_hands.IsHolding((args.User, args.Hands), uid, out _))
             return;
 
         // TODO VERB TOOLTIPS Make CanWield or some other function return string, set as verb tooltip and disable
@@ -252,7 +252,7 @@ public abstract class SharedWieldableSystem : EntitySystem
         }
 
         // Is it.. actually in one of their hands?
-        if (!_hands.IsHolding(user, uid, out _, hands))
+        if (!_hands.IsHolding((user, hands), uid, out _))
         {
             if (!quiet)
                 _popup.PopupClient(Loc.GetString("wieldable-component-not-in-hands", ("item", uid)), user, user);
@@ -373,7 +373,7 @@ public abstract class SharedWieldableSystem : EntitySystem
     /// <param name="force">If this is true we will bypass UnwieldAttemptEvent.</param>
     public void UnwieldAll(Entity<HandsComponent?> wielder, bool force = false)
     {
-        foreach (var held in _hands.EnumerateHeld(wielder.Owner, wielder.Comp))
+        foreach (var held in _hands.EnumerateHeld(wielder))
         {
             if (TryComp<WieldableComponent>(held, out var wieldable))
                 TryUnwield(held, wieldable, wielder, force);
diff --git a/Resources/Prototypes/Body/Prototypes/a_ghost.yml b/Resources/Prototypes/Body/Prototypes/a_ghost.yml
deleted file mode 100644 (file)
index 7d358a5..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-- type: body
-  id: Aghost
-  name: "aghost"
-  root: torso
-  slots:
-    torso:
-      part: TorsoHuman
-      connections:
-      - right_arm
-      - left_arm
-    right_arm:
-      part: RightArmHuman
-      connections:
-      - right_hand
-    left_arm:
-      part: LeftArmHuman
-      connections:
-      - left_hand
-    right_hand:
-      part: RightHandHuman
-    left_hand:
-      part: LeftHandHuman
index 2304c0d386d0b02cb6eabd978e950aab9640ee8d..0a5577c6d387b60cc1d3ea997d49dde37689ca0e 100644 (file)
     canInteract: true
   - type: GhostHearing
   - type: Hands
+    hands:
+      hand_right:
+        location: Right
+      hand_left:
+        location: Left
+    sortedHands:
+    - hand_right
+    - hand_left
   - type: ComplexInteraction
   - type: Puller
     needsHands: false
@@ -29,8 +37,6 @@
   - type: Physics
     ignorePaused: true
     bodyType: Kinematic
-  - type: Body
-    prototype: Aghost
   - type: Access
     groups:
     - AllAccess