From f2816e8081f5d59515fab2ed9375f081ab6c6576 Mon Sep 17 00:00:00 2001 From: collinlunn <60152240+collinlunn@users.noreply.github.com> Date: Mon, 21 Jun 2021 02:21:20 -0700 Subject: [PATCH] Moves Hands to shared, some prediction (#3829) * HandsGuiState * Gui state setting methods * code cleanup * Removes TryGetHands * ClientHand * Gui Hands * Refactor WIP 1 * Hand index * refactors 2 * wip 3 * wip 4 * wiip 4 * wip 5 * wip 6 * wip 7 * wip 8 * wip 9 * wip 11 * Hand ui mostly looks fine * hands gui cleanup 1 * cleanup 2 * wip 13 * hand enabled * stuff * Hands gui gap fix * onpressed test * hand gui buttons events work * bag activation works * fix item use * todo comment * hands activate fix * Moves Client Hands back to using strings to identify active hand * fixes action hand highlighting * diff fix * serverhand * SharedHand * SharedHand, IReadOnlyHand * Client Hands only stores SharedHand * cleanup server hands * server hand container shutdown * misc renames, refactors of serverhand * stuff 1 * stuff 3 * server hand refactor 1 * Undo API changes to remove massive diff * More API name fixes * server hands cleanup 2 * cleanup 3 * dropping cleanup * Cleanup 4 * MoveItemFromHand * Stuff * region sorting * Hand Putter methods cleanup * stuff 2 * Merges all of serverhand and clienthand into sharedhand * Other hands systems, hack to make inhands update (gui state set every frame, visualzier updated every frame) * GetFinalDropCoordinates cleanup * SwapHands cleanup * Moves server hands code to shared hands * Fixed hand selected and deselected * Naming fixes * Server hands system cleanup * Hands privacy fixes * Client hand updates when containers are modified * HeldItemVisualizer * Fixes hand gui item status panel * method name fix * Swap hands prediction * Dropping prediction * Fixes pickup entity animation * Fixes HeldItemsVisualizer * moves item pickup to shared * PR cleanup * fixes hand enabling/disabling * build fix * Conflict fixes * Fixes pickup animation * Uses component directed message subscriptions * event unsubscriptions in hand system * unsubscribe fix * CanInsertEntityIntoHand checks if hand is enabled * Moving items from one hand to another checks if the hands can pick up and drop * Fixes stop pulling not re-enabling hand * Fixes pickup animation for entities containers on the floor * Fixes using held items * Fixes multiple hands guis appearing * test fix * removes obsolete system sunsubscribes * Checks IsFirstTimePredicted before playing drop animation * fixes hand item deleted crash * Uses Get to get other system * Replaces AppearanceComponent with SharedAppearanceComponent * Replaces EnsureComponent with TryGetComponent * Improves event class names * Moves property up to top of class * Moves code for determining the hand visualizer rsi state into the visualizer instead of being determined on hand component * Eventbus todo comment * Yaml fix for changed visualizer name * Makes HandsVisuals a byte * Removes state from HandsVisualizer * Fixes hand using interaction method name * Namespace changes fixes * Fix for changed hand interaction method * missing } * conflict build fix * Moves cleint HandsSystem to correct folder * Moved namespace fix for interaction test * Moves Handsvisualizer ot correct folder * Moves SharedHandsSystem to correct folder * Fixes errors from moving namespace of hand systems * Fixes PDA component changes * Fixes ActionsComponent diff * Fixes inventory component diff * fixes null ref * Replaces obsolete Loc.GetString usage with fluent translations * Fluent for hands disarming * SwapHands and Drop user input specify to the server which hand * Pickup animation WorldPosiiton todo * Cleans up hands gui subscription handling * Fixes change in ActionBlockerSystem access * Namespace references fixes * HandsComponent PlayerAttached/Detached messages are handled through eventbus * Fixes GasCanisterSystem drop method usage * Fix gameticker equipping method at new location Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> --- .../Actions/ClientActionsComponent.cs | 8 +- Content.Client/Hands/HandsComponent.cs | 409 +++----- Content.Client/Hands/HandsGui.cs | 363 ++++--- Content.Client/Hands/HandsSystem.cs | 70 +- Content.Client/Hands/HandsVisualizer.cs | 96 ++ .../Items/Components/ItemComponent.cs | 7 +- .../Items/Managers/ItemSlotManager.cs | 3 +- .../Weapons/Ranged/RangedWeaponSystem.cs | 8 +- .../Tests/Buckle/BuckleTest.cs | 4 +- .../Components/ActionBlocking/HandCuffTest.cs | 3 +- .../Components/Mobs/ActionsComponentTests.cs | 2 +- .../Click/InteractionSystemTests.cs | 11 +- .../Operators/Inventory/DropEntityOperator.cs | 3 +- .../Components/IdCardConsoleComponent.cs | 6 +- .../Binary/EntitySystems/GasCanisterSystem.cs | 5 +- .../Cuffs/Components/CuffableComponent.cs | 6 +- Content.Server/Cuffs/CuffableSystem.cs | 3 +- .../GameTicking/GameTicker.Spawning.cs | 2 +- .../Hands/Components/HandsComponent.cs | 966 +++++------------- .../Hands/Components/IHandsComponent.cs | 62 +- Content.Server/Hands/HandsSystem.cs | 192 +--- .../Interaction/InteractionSystem.cs | 10 +- .../Components/InventoryComponent.cs | 2 +- Content.Server/Items/ItemComponent.cs | 18 +- Content.Server/PDA/PDAComponent.cs | 13 +- Content.Server/Strip/StrippableComponent.cs | 8 +- .../Components/ItemActionsComponent.cs | 2 +- .../Hands/Components/SharedHandsComponent.cs | 953 +++++++++++++++-- Content.Shared/Hands/IEquippedHand.cs | 8 +- Content.Shared/Hands/IUnequippedHand.cs | 8 +- Content.Shared/Hands/SharedHandsSystem.cs | 79 ++ Content.Shared/Item/SharedItemComponent.cs | 23 +- .../Entities/Mobs/Species/human.yml | 1 + 33 files changed, 1829 insertions(+), 1525 deletions(-) create mode 100644 Content.Client/Hands/HandsVisualizer.cs create mode 100644 Content.Shared/Hands/SharedHandsSystem.cs diff --git a/Content.Client/Actions/ClientActionsComponent.cs b/Content.Client/Actions/ClientActionsComponent.cs index 4719dc3697..6df12b2581 100644 --- a/Content.Client/Actions/ClientActionsComponent.cs +++ b/Content.Client/Actions/ClientActionsComponent.cs @@ -226,11 +226,11 @@ namespace Content.Client.Actions StopHighlightingItemSlots(); // figure out if it's in hand or inventory and highlight it - foreach (var hand in _handsComponent!.Hands) + foreach (var hand in _handsComponent!.Gui!.Hands) { - if (hand.Entity != item || hand.Button == null) continue; - _highlightingItemSlots.Add(hand.Button); - hand.Button.Highlight(true); + if (hand.HeldItem != item || hand.HandButton == null) continue; + _highlightingItemSlots.Add(hand.HandButton); + hand.HandButton.Highlight(true); return; } diff --git a/Content.Client/Hands/HandsComponent.cs b/Content.Client/Hands/HandsComponent.cs index 1e1f7315ad..0299384e33 100644 --- a/Content.Client/Hands/HandsComponent.cs +++ b/Content.Client/Hands/HandsComponent.cs @@ -1,16 +1,14 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using Content.Client.Animations; using Content.Client.HUD; using Content.Shared.Hands.Components; using Content.Shared.Item; -using Robust.Client.GameObjects; -using Robust.Client.ResourceManagement; +using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Network; using Robust.Shared.Players; +using Robust.Shared.Timing; using Robust.Shared.ViewVariables; namespace Content.Client.Hands @@ -22,239 +20,50 @@ namespace Content.Client.Hands { [Dependency] private readonly IGameHud _gameHud = default!; - private HandsGui? _gui; - - private readonly List _hands = new(); - - [ViewVariables] public IReadOnlyList Hands => _hands; - - [ViewVariables] public string? ActiveIndex { get; private set; } - - [ViewVariables] private ISpriteComponent? _sprite; - - [ViewVariables] public IEntity? ActiveHand => GetEntity(ActiveIndex); - - public override bool IsHolding(IEntity entity) - { - foreach (var hand in _hands) - { - if (hand.Entity == entity) - { - return true; - } - } - return false; - } - - private void AddHand(Hand hand) - { - _sprite?.LayerMapReserveBlank($"hand-{hand.Name}"); - _hands.Insert(hand.Index, hand); - } - - public Hand? GetHand(string? name) - { - return Hands.FirstOrDefault(hand => hand.Name == name); - } - - private bool TryHand(string name, [NotNullWhen(true)] out Hand? hand) - { - return (hand = GetHand(name)) != null; - } - - public IEntity? GetEntity(string? handName) - { - if (handName == null) - { - return null; - } - - return GetHand(handName)?.Entity; - } + [ViewVariables] + public HandsGui? Gui { get; private set; } protected override void OnRemove() { + ClearGui(); base.OnRemove(); - - _gui?.Dispose(); - } - - protected override void Initialize() - { - base.Initialize(); - - if (Owner.TryGetComponent(out _sprite)) - { - foreach (var hand in _hands) - { - _sprite.LayerMapReserveBlank($"hand-{hand.Name}"); - UpdateHandSprites(hand); - } - } } public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) { - if (curState == null) - { - return; - } - - var cast = (HandsComponentState) curState; - foreach (var sharedHand in cast.Hands) - { - if (!TryHand(sharedHand.Name, out var hand)) - { - hand = new Hand(this, sharedHand, Owner.EntityManager); - AddHand(hand); - } - else - { - hand.Location = sharedHand.Location; - - hand.Entity = sharedHand.EntityUid.HasValue - ? Owner.EntityManager.GetEntity(sharedHand.EntityUid.Value) - : null; - } - - hand.Enabled = sharedHand.Enabled; - - UpdateHandSprites(hand); - } - - foreach (var currentHand in _hands.ToList()) - { - if (cast.Hands.All(newHand => newHand.Name != currentHand.Name)) - { - _hands.Remove(currentHand); - _gui?.RemoveHand(currentHand); - HideHand(currentHand); - } - } - - ActiveIndex = cast.ActiveIndex; - - _gui?.UpdateHandIcons(); - RefreshInHands(); - } - - private void HideHand(Hand hand) - { - _sprite?.LayerSetVisible($"hand-{hand.Name}", false); - } - - private void UpdateHandSprites(Hand hand) - { - if (_sprite == null) - { - return; - } - - var entity = hand.Entity; - var name = hand.Name; - - if (entity == null) - { - if (_sprite.LayerMapTryGet($"hand-{name}", out var layer)) - { - _sprite.LayerSetVisible(layer, false); - } - - return; - } - - if (!entity.TryGetComponent(out SharedItemComponent? item)) + if (curState is not HandsComponentState state) return; - if (item.RsiPath == null) + Hands.Clear(); + + foreach (var handState in state.Hands) { - _sprite.LayerSetVisible($"hand-{name}", false); + var newHand = new Hand(handState.Name, handState.Enabled, handState.Location); + Hands.Add(newHand); } - else + ActiveHand = state.ActiveHand; + + UpdateHandContainers(); + UpdateHandVisualizer(); + UpdateHandsGuiState(); + } + + public void SettupGui() + { + if (Gui == null) { - var rsi = IoCManager.Resolve().GetResource(SharedSpriteComponent.TextureRoot / item.RsiPath).RSI; - - var handName = hand.Location.ToString().ToLowerInvariant(); - var prefix = item.EquippedPrefix; - var state = prefix != null ? $"{prefix}-inhand-{handName}" : $"inhand-{handName}"; - - if (!rsi.TryGetState(state, out _)) - return; - - var color = item.Color; - - _sprite.LayerSetColor($"hand-{name}", color); - _sprite.LayerSetVisible($"hand-{name}", true); - _sprite.LayerSetState($"hand-{name}", state, rsi); + Gui = new HandsGui(); + _gameHud.HandsContainer.AddChild(Gui); + Gui.HandClick += args => OnHandClick(args.HandClicked); + Gui.HandActivate += args => OnActivateInHand(args.HandUsed); + UpdateHandsGuiState(); } } - public void RefreshInHands() + public void ClearGui() { - if (!Initialized) return; - - foreach (var hand in _hands) - { - UpdateHandSprites(hand); - } - } - - protected override void Startup() - { - base.Startup(); - ActiveIndex = _hands.LastOrDefault()?.Name; - } - - public override void HandleMessage(ComponentMessage message, IComponent? component) - { - base.HandleMessage(message, component); - - switch (message) - { - case HandEnabledMsg msg: - { - var hand = GetHand(msg.Name); - - if (hand?.Button == null) - { - break; - } - - hand.Button.Blocked.Visible = false; - - break; - } - case HandDisabledMsg msg: - { - var hand = GetHand(msg.Name); - - if (hand?.Button == null) - { - break; - } - - hand.Button.Blocked.Visible = true; - - break; - } - } - } - - public void PlayerDetached() { _gui?.Parent?.RemoveChild(_gui); } - - public void PlayerAttached() - { - if (_gui == null) - { - _gui = new HandsGui(); - } - else - { - _gui.Parent?.RemoveChild(_gui); - } - - _gameHud.HandsContainer.AddChild(_gui); - _gui.UpdateHandIcons(); + Gui?.Dispose(); + Gui = null; } public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null) @@ -263,94 +72,126 @@ namespace Content.Client.Hands switch (message) { - case AnimatePickupEntityMessage msg: - { - if (Owner.EntityManager.TryGetEntity(msg.EntityId, out var entity)) - { - ReusableAnimations.AnimateEntityPickup(entity, msg.EntityPosition, Owner.Transform.WorldPosition); - } + case PickupAnimationMessage msg: + RunPickupAnimation(msg); break; - } } } - public void SendChangeHand(string index) + public override void HandsModified() { - SendNetworkMessage(new ClientChangedHandMsg(index)); + base.HandsModified(); + + UpdateHandContainers(); + UpdateHandVisualizer(); + UpdateHandsGuiState(); } - public void AttackByInHand(string index) + private void OnHandClick(string handClicked) { - SendNetworkMessage(new ClientAttackByInHandMsg(index)); - } + if (!TryGetHand(handClicked, out var pressedHand)) + return; - public void UseActiveHand() - { - if (GetEntity(ActiveIndex) != null) - { - SendNetworkMessage(new UseInHandMsg()); - } - } - - public void ActivateItemInHand(string handIndex) - { - if (GetEntity(handIndex) == null) + if (!TryGetActiveHand(out var activeHand)) + return; + + var pressedEntity = pressedHand.HeldEntity; + var activeEntity = activeHand.HeldEntity; + + if (pressedHand == activeHand && activeEntity != null) { + SendNetworkMessage(new UseInHandMsg()); //use item in hand return; } - SendNetworkMessage(new ActivateInHandMsg(handIndex)); - } - } - - public class Hand - { - private bool _enabled = true; - - public Hand(HandsComponent parent, SharedHand hand, IEntityManager manager, HandButton? button = null) - { - Parent = parent; - Index = hand.Index; - Name = hand.Name; - Location = hand.Location; - Button = button; - - if (!hand.EntityUid.HasValue) + if (pressedHand != activeHand && pressedEntity == null) { + SendNetworkMessage(new ClientChangedHandMsg(pressedHand.Name)); //swap hand return; } - manager.TryGetEntity(hand.EntityUid.Value, out var entity); - Entity = entity; + if (pressedHand != activeHand && pressedEntity != null && activeEntity != null) + { + SendNetworkMessage(new ClientAttackByInHandMsg(pressedHand.Name)); //use active item on held item + return; + } + + if (pressedHand != activeHand && pressedEntity != null && activeEntity == null) + { + SendNetworkMessage(new MoveItemFromHandMsg(pressedHand.Name)); //move item in hand to active hand + return; + } } - private HandsComponent Parent { get; } - public int Index { get; } - public string Name { get; } - public HandLocation Location { get; set; } - public IEntity? Entity { get; set; } - public HandButton? Button { get; set; } - - public bool Enabled + private void OnActivateInHand(string handActivated) { - get => _enabled; - set + SendNetworkMessage(new ActivateInHandMsg(handActivated)); + } + + public void UpdateHandContainers() + { + if (!Owner.TryGetComponent(out var containerMan)) + return; + + foreach (var hand in Hands) { - if (_enabled == value) + if (hand.Container == null) { - return; + containerMan.TryGetContainer(hand.Name, out var container); + hand.Container = container; } - - _enabled = value; - Parent.Dirty(); - - var message = value - ? (ComponentMessage) new HandEnabledMsg(Name) - : new HandDisabledMsg(Name); - - Parent.HandleMessage(message, Parent); - Parent.Owner.SendMessage(Parent, message); } } + + public void UpdateHandVisualizer() + { + if (Owner.TryGetComponent(out SharedAppearanceComponent? appearance)) + appearance.SetData(HandsVisuals.VisualState, GetHandsVisualState()); + } + + public void UpdateHandsGuiState() + { + Gui?.SetState(GetHandsGuiState()); + } + + private HandsGuiState GetHandsGuiState() + { + var handStates = new List(); + + foreach (var hand in ReadOnlyHands) + { + var handState = new GuiHand(hand.Name, hand.Location, hand.HeldEntity, hand.Enabled); + handStates.Add(handState); + } + return new HandsGuiState(handStates, ActiveHand); + } + + private HandsVisualState GetHandsVisualState() + { + var hands = new List(); + foreach (var hand in ReadOnlyHands) + { + if (hand.HeldEntity == null) + continue; + + if (!hand.HeldEntity.TryGetComponent(out SharedItemComponent? item) || item.RsiPath == null) + continue; + + var handState = new HandVisualState(item.RsiPath, item.EquippedPrefix, hand.Location, item.Color); + hands.Add(handState); + } + return new(hands); + } + + private void RunPickupAnimation(PickupAnimationMessage msg) + { + if (!Owner.EntityManager.TryGetEntity(msg.EntityUid, out var entity)) + return; + + if (!IoCManager.Resolve().IsFirstTimePredicted) + return; + + ReusableAnimations.AnimateEntityPickup(entity, msg.InitialPosition, msg.PickupDirection); + } } } diff --git a/Content.Client/Hands/HandsGui.cs b/Content.Client/Hands/HandsGui.cs index 76dbca4678..22a7e643e3 100644 --- a/Content.Client/Hands/HandsGui.cs +++ b/Content.Client/Hands/HandsGui.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Client.HUD; @@ -15,246 +16,242 @@ using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Timing; +using Robust.Shared.ViewVariables; namespace Content.Client.Hands { public class HandsGui : Control { - [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; [Dependency] private readonly IGameHud _gameHud = default!; [Dependency] private readonly INetConfigurationManager _configManager = default!; - private Texture _leftHandTexture; - private Texture _middleHandTexture; - private Texture _rightHandTexture; + private Texture StorageTexture => _gameHud.GetHudTexture("back.png"); + private Texture BlockedTexture => _resourceCache.GetTexture("/Textures/Interface/Inventory/blocked.png"); - private readonly ItemStatusPanel _topPanel; + private ItemStatusPanel StatusPanel { get; } - private readonly HBoxContainer _guiContainer; - private readonly VBoxContainer _handsColumn; - private readonly HBoxContainer _handsContainer; + private HBoxContainer HandsContainer { get; } + + [ViewVariables] + public IReadOnlyList Hands => _hands; + private List _hands = new(); + + private string? ActiveHand { get; set; } + + public Action? HandClick; //TODO: Move to Eventbus + + public Action? HandActivate; //TODO: Move to Eventbus public HandsGui() { IoCManager.InjectDependencies(this); + _configManager.OnValueChanged(CCVars.HudTheme, UpdateHudTheme); - _configManager.OnValueChanged(CCVars.HudTheme, UpdateHudTheme, invokeImmediately: true); - - AddChild(_guiContainer = new HBoxContainer + AddChild(new HBoxContainer { SeparationOverride = 0, HorizontalAlignment = HAlignment.Center, Children = { - (_handsColumn = new VBoxContainer + new VBoxContainer { Children = { - (_topPanel = ItemStatusPanel.FromSide(HandLocation.Middle)), - (_handsContainer = new HBoxContainer{HorizontalAlignment = HAlignment.Center}) + (StatusPanel = ItemStatusPanel.FromSide(HandLocation.Middle)), + (HandsContainer = new HBoxContainer() { HorizontalAlignment = HAlignment.Center } ), } - }), + }, } }); - _leftHandTexture = _gameHud.GetHudTexture("hand_l.png"); - _middleHandTexture = _gameHud.GetHudTexture("hand_l.png"); - _rightHandTexture = _gameHud.GetHudTexture("hand_r.png"); } - private void UpdateHudTheme(int idx) + public void SetState(HandsGuiState state) { - if (!_gameHud.ValidateHudTheme(idx)) + ActiveHand = state.ActiveHand; + _hands = state.GuiHands; + UpdateGui(); + } + + private void UpdateGui() + { + HandsContainer.DisposeAllChildren(); + + foreach (var hand in _hands) { - return; + var newButton = MakeHandButton(hand.HandLocation); + HandsContainer.AddChild(newButton); + hand.HandButton = newButton; + + var handName = hand.Name; + newButton.OnPressed += args => OnHandPressed(args, handName); + newButton.OnStoragePressed += args => OnStoragePressed(handName); + + newButton.Blocked.Visible = !hand.Enabled; + + _itemSlotManager.SetItemSlot(newButton, hand.HeldItem); } - _leftHandTexture = _gameHud.GetHudTexture("hand_l.png"); - _middleHandTexture = _gameHud.GetHudTexture("hand_l.png"); - _rightHandTexture = _gameHud.GetHudTexture("hand_r.png"); - UpdateHandIcons(); - } - - private Texture HandTexture(HandLocation location) - { - switch (location) + if (TryGetActiveHand(out var activeHand)) { - case HandLocation.Left: - return _leftHandTexture; - case HandLocation.Middle: - return _middleHandTexture; - case HandLocation.Right: - return _rightHandTexture; - default: - throw new ArgumentOutOfRangeException(nameof(location), location, null); + activeHand.HandButton.SetActiveHand(true); + StatusPanel.Update(activeHand.HeldItem); } } - /// - /// Adds a new hand to this control - /// - /// The hand to add to this control - /// - /// The actual location of the button. The right hand is drawn - /// on the LEFT of the screen. - /// - private void AddHand(Hand hand, HandLocation buttonLocation) + private void OnHandPressed(GUIBoundKeyEventArgs args, string handName) { - var textureName = "hand_l.png"; - if(buttonLocation == HandLocation.Right) - { - textureName = "hand_r.png"; - } - var buttonTexture = HandTexture(buttonLocation); - var storageTexture = _gameHud.GetHudTexture("back.png"); - var blockedTexture = _resourceCache.GetTexture("/Textures/Interface/Inventory/blocked.png"); - var button = new HandButton(buttonTexture, storageTexture, textureName, blockedTexture, buttonLocation); - var slot = hand.Name; - - button.OnPressed += args => HandKeyBindDown(args, slot); - button.OnStoragePressed += args => _OnStoragePressed(args, slot); - - _handsContainer.AddChild(button); - hand.Button = button; - } - - public void RemoveHand(Hand hand) - { - var button = hand.Button; - - if (button != null) - { - _handsContainer.RemoveChild(button); - } - } - - /// - /// Gets the hands component controlling this gui - /// - /// - /// true if successful and false if failure - private bool TryGetHands([NotNullWhen(true)] out HandsComponent? hands) - { - hands = default; - - var entity = _playerManager?.LocalPlayer?.ControlledEntity; - return entity != null && entity.TryGetComponent(out hands); - } - - public void UpdateHandIcons() - { - if (Parent == null) - { - return; - } - - UpdateDraw(); - - if (!TryGetHands(out var component)) - { - return; - } - - // TODO: Remove button on remove hand - - var hands = component.Hands.OrderByDescending(x => x.Location).ToArray(); - for (var i = 0; i < hands.Length; i++) - { - var hand = hands[i]; - - if (hand.Button == null) - { - AddHand(hand, hand.Location); - } - - hand.Button!.Button.Texture = HandTexture(hand.Location); - hand.Button!.SetPositionInParent(i); - _itemSlotManager.SetItemSlot(hand.Button, hand.Entity); - - hand.Button!.SetActiveHand(component.ActiveIndex == hand.Name); - } - - } - - private void HandKeyBindDown(GUIBoundKeyEventArgs args, string slotName) - { - if (!TryGetHands(out var hands)) - { - return; - } - - if (args.Function == ContentKeyFunctions.MouseMiddle) - { - hands.SendChangeHand(slotName); - args.Handle(); - return; - } - - var entity = hands.GetEntity(slotName); - if (entity == null) - { - if (args.Function == EngineKeyFunctions.UIClick && hands.ActiveIndex != slotName) - { - hands.SendChangeHand(slotName); - args.Handle(); - } - - return; - } - - if (_itemSlotManager.OnButtonPressed(args, entity)) - { - args.Handle(); - return; - } - if (args.Function == EngineKeyFunctions.UIClick) { - if (hands.ActiveIndex == slotName) - { - hands.UseActiveHand(); - } - else - { - hands.AttackByInHand(slotName); - } - - args.Handle(); + HandClick?.Invoke(new HandClickEventArgs(handName)); + } + else if (TryGetHand(handName, out var hand)) + { + _itemSlotManager.OnButtonPressed(args, hand.HeldItem); } } - private void _OnStoragePressed(GUIBoundKeyEventArgs args, string handIndex) + private void OnStoragePressed(string handName) { - if (args.Function != EngineKeyFunctions.UIClick || !TryGetHands(out var hands)) - { - return; - } - - hands.ActivateItemInHand(handIndex); + HandActivate?.Invoke(new HandActivateEventArgs(handName)); } - private void UpdatePanels() + private bool TryGetActiveHand([NotNullWhen(true)] out GuiHand? activeHand) { - if (!TryGetHands(out var component)) - { - return; - } + TryGetHand(ActiveHand, out activeHand); + return activeHand != null; + } - foreach (var hand in component.Hands) - { - _itemSlotManager.UpdateCooldown(hand.Button, hand.Entity); - } + private bool TryGetHand(string? handName, [NotNullWhen(true)] out GuiHand? foundHand) + { + foundHand = null; - _topPanel.Update(component.GetEntity(component.ActiveIndex)); + if (handName == null) + return false; + + foreach (var hand in _hands) + { + if (hand.Name == handName) + foundHand = hand; + } + return foundHand != null; } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); - UpdatePanels(); + + foreach (var hand in _hands) + _itemSlotManager.UpdateCooldown(hand.HandButton, hand.HeldItem); + } + + private HandButton MakeHandButton(HandLocation buttonLocation) + { + var buttonTextureName = buttonLocation switch + { + HandLocation.Right => "hand_r.png", + _ => "hand_l.png" + }; + var buttonTexture = _gameHud.GetHudTexture(buttonTextureName); + + return new HandButton(buttonTexture, StorageTexture, buttonTextureName, BlockedTexture, buttonLocation); + } + + private void UpdateHudTheme(int idx) + { + UpdateGui(); + } + + public class HandClickEventArgs + { + public string HandClicked { get; } + + public HandClickEventArgs(string handClicked) + { + HandClicked = handClicked; + } + } + + public class HandActivateEventArgs + { + public string HandUsed { get; } + + public HandActivateEventArgs(string handUsed) + { + HandUsed = handUsed; + } + } + } + + /// + /// Info on a set of hands to be displayed. + /// + public class HandsGuiState + { + /// + /// The set of hands to be displayed. + /// + [ViewVariables] + public List GuiHands { get; } = new(); + + /// + /// The name of the currently active hand. + /// + [ViewVariables] + public string? ActiveHand { get; } + + public HandsGuiState(List guiHands, string? activeHand = null) + { + GuiHands = guiHands; + ActiveHand = activeHand; + } + } + + /// + /// Info on an individual hand to be displayed. + /// + public class GuiHand + { + /// + /// The name of this hand. + /// + [ViewVariables] + public string Name { get; } + + /// + /// Where this hand is located. + /// + [ViewVariables] + public HandLocation HandLocation { get; } + + /// + /// The item being held in this hand. + /// + [ViewVariables] + public IEntity? HeldItem { get; } + + /// + /// The button in the gui associated with this hand. Assumed to be set by gui shortly after being received from the client HandsComponent. + /// + [ViewVariables] + public HandButton HandButton { get; set; } = default!; + + /// + /// If this hand can be used by the player. + /// + [ViewVariables] + public bool Enabled { get; } + + public GuiHand(string name, HandLocation handLocation, IEntity? heldItem, bool enabled) + { + Name = name; + HandLocation = handLocation; + HeldItem = heldItem; + Enabled = enabled; } } } diff --git a/Content.Client/Hands/HandsSystem.cs b/Content.Client/Hands/HandsSystem.cs index d1e2520dfa..4ba3a73c6f 100644 --- a/Content.Client/Hands/HandsSystem.cs +++ b/Content.Client/Hands/HandsSystem.cs @@ -1,16 +1,80 @@ +using Content.Shared.Hands; +using Content.Shared.Hands.Components; +using Content.Shared.Input; using Robust.Client.GameObjects; +using Robust.Shared.Containers; using Robust.Shared.GameObjects; +using Robust.Shared.Input.Binding; +using Robust.Shared.Map; +using Robust.Shared.Players; namespace Content.Client.Hands { - internal class HandsSystem : EntitySystem + internal sealed class HandsSystem : SharedHandsSystem { public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((_, component, _) => component.PlayerAttached()); - SubscribeLocalEvent((_, component, _) => component.PlayerDetached()); + SubscribeLocalEvent((_, component, _) => component.SettupGui()); + SubscribeLocalEvent((_, component, _) => component.ClearGui()); + + CommandBinds.Builder + .Bind(ContentKeyFunctions.SwapHands, InputCmdHandler.FromDelegate(SwapHandsPressed)) + .Bind(ContentKeyFunctions.Drop, new PointerInputCmdHandler(DropPressed)) + .Register(); + } + + public override void Shutdown() + { + CommandBinds.Unregister(); + base.Shutdown(); + } + + private void SwapHandsPressed(ICommonSession? session) + { + if (session == null) + return; + + var player = session.AttachedEntity; + + if (player == null) + return; + + if (!player.TryGetComponent(out SharedHandsComponent? hands)) + return; + + if (!hands.TryGetSwapHandsResult(out var nextHand)) + return; + + EntityManager.RaisePredictiveEvent(new RequestSetHandEvent(nextHand)); + } + + private bool DropPressed(ICommonSession? session, EntityCoordinates coords, EntityUid uid) + { + if (session == null) + return false; + + var player = session.AttachedEntity; + + if (player == null) + return false; + + if (!player.TryGetComponent(out SharedHandsComponent? hands)) + return false; + + var activeHand = hands.ActiveHand; + + if (activeHand == null) + return false; + + EntityManager.RaisePredictiveEvent(new RequestDropHeldEntityEvent(activeHand, coords)); + return true; + } + + protected override void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args) + { + component.HandsModified(); } } } diff --git a/Content.Client/Hands/HandsVisualizer.cs b/Content.Client/Hands/HandsVisualizer.cs new file mode 100644 index 0000000000..53a632fc2c --- /dev/null +++ b/Content.Client/Hands/HandsVisualizer.cs @@ -0,0 +1,96 @@ +using Content.Shared.Hands.Components; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.ResourceManagement; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using System; +using System.Collections.Generic; + +namespace Content.Client.Hands +{ + [UsedImplicitly] + public class HandsVisualizer : AppearanceVisualizer + { + + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + if (!component.Owner.TryGetComponent(out var sprite)) return; + if (!component.TryGetData(HandsVisuals.VisualState, out HandsVisualState visualState)) return; + + foreach (HandLocation location in Enum.GetValues(typeof(HandLocation))) + { + var layerKey = LocationToLayerKey(location); + if (sprite.LayerMapTryGet(layerKey, out var layer)) + { + sprite.RemoveLayer(layer); + sprite.LayerMapRemove(layer); + } + } + + var resourceCache = IoCManager.Resolve(); + var hands = visualState.Hands; + + foreach (var hand in hands) + { + var rsi = resourceCache.GetResource(SharedSpriteComponent.TextureRoot / hand.RsiPath).RSI; + + var layerKey = LocationToLayerKey(hand.Location); + sprite.LayerMapReserveBlank(layerKey); + + var layer = sprite.LayerMapGet(layerKey); + sprite.LayerSetVisible(layer, true); + sprite.LayerSetRSI(layer, rsi); + sprite.LayerSetColor(layer, hand.Color); + + var state = $"inhand-{hand.Location.ToString().ToLowerInvariant()}"; + if (hand.EquippedPrefix != null) + state = $"{hand.EquippedPrefix}-" + state; + + sprite.LayerSetState(layer, state); + } + } + + private string LocationToLayerKey(HandLocation location) + { + return location.ToString(); + } + } + + public enum HandsVisuals : byte + { + VisualState + } + + public class HandsVisualState + { + public List Hands { get; } = new(); + + public HandsVisualState(List hands) + { + Hands = hands; + } + } + + public class HandVisualState + { + public string RsiPath { get; } + + public string? EquippedPrefix { get; } + + public HandLocation Location { get; } + + public Color Color { get; } + + public HandVisualState(string rsiPath, string? equippedPrefix, HandLocation location, Color color) + { + RsiPath = rsiPath; + EquippedPrefix = equippedPrefix; + Location = location; + Color = color; + } + } +} diff --git a/Content.Client/Items/Components/ItemComponent.cs b/Content.Client/Items/Components/ItemComponent.cs index 9b497e9bbd..f26d349996 100644 --- a/Content.Client/Items/Components/ItemComponent.cs +++ b/Content.Client/Items/Components/ItemComponent.cs @@ -10,18 +10,13 @@ namespace Content.Client.Items.Components [ComponentReference(typeof(SharedItemComponent))] public class ItemComponent : SharedItemComponent { - public override bool TryPutInHand(IEntity user) - { - return false; - } - protected override void OnEquippedPrefixChange() { if (!Owner.TryGetContainer(out var container)) return; if (container.Owner.TryGetComponent(out HandsComponent? hands)) - hands.RefreshInHands(); + hands.UpdateHandVisualizer(); } } } diff --git a/Content.Client/Items/Managers/ItemSlotManager.cs b/Content.Client/Items/Managers/ItemSlotManager.cs index 8bbcaacc8a..c9a74cb10c 100644 --- a/Content.Client/Items/Managers/ItemSlotManager.cs +++ b/Content.Client/Items/Managers/ItemSlotManager.cs @@ -1,4 +1,4 @@ -using Content.Client.Examine; +using Content.Client.Examine; using Content.Client.Items.UI; using Content.Client.Storage; using Content.Client.Verbs; @@ -104,6 +104,7 @@ namespace Content.Client.Items.Managers } if (entity == null || + entity.Deleted || !entity.TryGetComponent(out ItemCooldownComponent? cooldown) || !cooldown.CooldownStart.HasValue || !cooldown.CooldownEnd.HasValue) diff --git a/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs b/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs index f74a1f322d..85de5e4d55 100644 --- a/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs +++ b/Content.Client/Weapons/Ranged/RangedWeaponSystem.cs @@ -1,6 +1,7 @@ -using System; +using System; using Content.Client.CombatMode; using Content.Client.Hands; +using Content.Shared.Hands.Components; using Content.Shared.Weapons.Ranged.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; @@ -56,13 +57,12 @@ namespace Content.Client.Weapons.Ranged } var entity = _playerManager.LocalPlayer?.ControlledEntity; - if (entity == null || !entity.TryGetComponent(out HandsComponent? hands)) + if (entity == null || !entity.TryGetComponent(out SharedHandsComponent? hands)) { return; } - var held = hands.ActiveHand; - if (held == null || !held.TryGetComponent(out ClientRangedWeaponComponent? weapon)) + if (!hands.TryGetActiveHeldEntity(out var held) || !held.TryGetComponent(out ClientRangedWeaponComponent? weapon)) { _blocked = true; return; diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs index 513d9cd9f8..c196876916 100644 --- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs @@ -266,7 +266,7 @@ namespace Content.IntegrationTests.Tests.Buckle Assert.True(buckle.Buckled); // With items in all hands - foreach (var slot in hands.Hands) + foreach (var slot in hands.HandNames) { Assert.NotNull(hands.GetItem(slot)); } @@ -288,7 +288,7 @@ namespace Content.IntegrationTests.Tests.Buckle Assert.True(buckle.Buckled); // Now with no item in any hand - foreach (var slot in hands.Hands) + foreach (var slot in hands.HandNames) { Assert.Null(hands.GetItem(slot)); } diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs index 5109c330f3..fbde337fa9 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs @@ -80,8 +80,9 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking // Test to ensure a player with 4 hands will still only have 2 hands cuffed AddHand(cuffed.Owner); AddHand(cuffed.Owner); + Assert.That(cuffed.CuffedHandCount, Is.EqualTo(2)); - Assert.That(hands.Hands.Count(), Is.EqualTo(4)); + Assert.That(hands.HandNames.Count(), Is.EqualTo(4)); // Test to give a player with 4 hands 2 sets of cuffs cuffed.TryAddNewCuffs(human, secondCuffs); diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs index eb129cc71c..497e735a82 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs @@ -330,7 +330,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs { // drop the item, and the item actions should go away serverPlayerEnt.GetComponent() - .Drop(serverFlashlight, serverPlayerEnt.Transform.Coordinates, false); + .TryDropEntity(serverFlashlight, serverPlayerEnt.Transform.Coordinates, false); Assert.That(serverActionsComponent.ItemActionStates().ContainsKey(serverFlashlight.Uid), Is.False); }); diff --git a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs index a642c627f9..473119e481 100644 --- a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs +++ b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs @@ -11,6 +11,7 @@ using Content.Server.Interaction; using Content.Server.Items; using Content.Shared.Interaction; using Content.Shared.Weapons.Melee; +using Content.Shared.Hands.Components; namespace Content.IntegrationTests.Tests.Interaction.Click { @@ -66,7 +67,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click server.Assert(() => { user = entityManager.SpawnEntity(null, coords); - user.EnsureComponent().AddHand("hand"); + user.EnsureComponent().AddHand("hand", HandLocation.Left); target = entityManager.SpawnEntity(null, coords); item = entityManager.SpawnEntity(null, coords); item.EnsureComponent(); @@ -137,7 +138,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click server.Assert(() => { user = entityManager.SpawnEntity(null, coords); - user.EnsureComponent().AddHand("hand"); + user.EnsureComponent().AddHand("hand", HandLocation.Left); target = entityManager.SpawnEntity(null, new MapCoordinates((1.9f, 0), mapId)); item = entityManager.SpawnEntity(null, coords); item.EnsureComponent(); @@ -207,7 +208,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click server.Assert(() => { user = entityManager.SpawnEntity(null, coords); - user.EnsureComponent().AddHand("hand"); + user.EnsureComponent().AddHand("hand", HandLocation.Left); target = entityManager.SpawnEntity(null, new MapCoordinates((InteractionSystem.InteractionRange - 0.1f, 0), mapId)); item = entityManager.SpawnEntity(null, coords); item.EnsureComponent(); @@ -277,7 +278,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click server.Assert(() => { user = entityManager.SpawnEntity(null, coords); - user.EnsureComponent().AddHand("hand"); + user.EnsureComponent().AddHand("hand", HandLocation.Left); target = entityManager.SpawnEntity(null, new MapCoordinates((InteractionSystem.InteractionRange, 0), mapId)); item = entityManager.SpawnEntity(null, coords); item.EnsureComponent(); @@ -349,7 +350,7 @@ namespace Content.IntegrationTests.Tests.Interaction.Click server.Assert(() => { user = entityManager.SpawnEntity(null, coords); - user.EnsureComponent().AddHand("hand"); + user.EnsureComponent().AddHand("hand", HandLocation.Left); target = entityManager.SpawnEntity(null, coords); item = entityManager.SpawnEntity(null, coords); item.EnsureComponent(); diff --git a/Content.Server/AI/Operators/Inventory/DropEntityOperator.cs b/Content.Server/AI/Operators/Inventory/DropEntityOperator.cs index 8eaa2ad8a6..1808434ed8 100644 --- a/Content.Server/AI/Operators/Inventory/DropEntityOperator.cs +++ b/Content.Server/AI/Operators/Inventory/DropEntityOperator.cs @@ -20,8 +20,7 @@ namespace Content.Server.AI.Operators.Inventory /// public override Outcome Execute(float frameTime) { - if (!_owner.TryGetComponent(out HandsComponent? handsComponent) || - !handsComponent.TryHand(_entity, out _)) + if (!_owner.TryGetComponent(out HandsComponent? handsComponent)) { return Outcome.Failed; } diff --git a/Content.Server/Access/Components/IdCardConsoleComponent.cs b/Content.Server/Access/Components/IdCardConsoleComponent.cs index 627fe8ccb4..5debc68a1a 100644 --- a/Content.Server/Access/Components/IdCardConsoleComponent.cs +++ b/Content.Server/Access/Components/IdCardConsoleComponent.cs @@ -159,7 +159,7 @@ namespace Content.Server.Access.Components return; } - if (!hands.Drop(hands.ActiveHand, container)) + if (!hands.TryPutHandIntoContainer(hands.ActiveHand, container)) { Owner.PopupMessage(user, Loc.GetString("access-id-card-console-component-cannot-let-go-error")); return; @@ -216,11 +216,11 @@ namespace Content.Server.Access.Components void IActivate.Activate(ActivateEventArgs eventArgs) { - if(!eventArgs.User.TryGetComponent(out ActorComponent? actor)) + if (!eventArgs.User.TryGetComponent(out ActorComponent? actor)) { return; } - if(!Powered) return; + if (!Powered) return; UserInterface?.Open(actor.PlayerSession); } diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasCanisterSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasCanisterSystem.cs index bd0cc9f397..f29b4895b3 100644 --- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasCanisterSystem.cs +++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasCanisterSystem.cs @@ -193,10 +193,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems if (!args.User.InRangeUnobstructed(canister, SharedInteractionSystem.InteractionRange, popup: true)) return; - if (!hands.Drop(args.Used, canister.Transform.Coordinates)) - return; - - if (!container.Insert(args.Used)) + if (!hands.TryPutEntityIntoContainer(args.Used, container)) return; args.Handled = true; diff --git a/Content.Server/Cuffs/Components/CuffableComponent.cs b/Content.Server/Cuffs/Components/CuffableComponent.cs index 0e3c3fa623..7fc5d27503 100644 --- a/Content.Server/Cuffs/Components/CuffableComponent.cs +++ b/Content.Server/Cuffs/Components/CuffableComponent.cs @@ -113,7 +113,7 @@ namespace Content.Server.Cuffs.Components } Container.Insert(handcuff); - CanStillInteract = Owner.TryGetComponent(out HandsComponent? ownerHands) && ownerHands.Hands.Count() > CuffedHandCount; + CanStillInteract = Owner.TryGetComponent(out HandsComponent? ownerHands) && ownerHands.HandNames.Count() > CuffedHandCount; OnCuffedStateChanged?.Invoke(); UpdateAlert(); @@ -136,7 +136,7 @@ namespace Content.Server.Cuffs.Components if (!Owner.TryGetComponent(out HandsComponent? handsComponent)) return; var itemCount = handsComponent.GetAllHeldItems().Count(); - var freeHandCount = handsComponent.Hands.Count() - CuffedHandCount; + var freeHandCount = handsComponent.HandNames.Count() - CuffedHandCount; if (freeHandCount < itemCount) { @@ -279,7 +279,7 @@ namespace Content.Server.Cuffs.Components } } - CanStillInteract = Owner.TryGetComponent(out HandsComponent? handsComponent) && handsComponent.Hands.Count() > CuffedHandCount; + CanStillInteract = Owner.TryGetComponent(out HandsComponent? handsComponent) && handsComponent.HandNames.Count() > CuffedHandCount; OnCuffedStateChanged?.Invoke(); UpdateAlert(); Dirty(); diff --git a/Content.Server/Cuffs/CuffableSystem.cs b/Content.Server/Cuffs/CuffableSystem.cs index c05f7e21f2..66f374ea34 100644 --- a/Content.Server/Cuffs/CuffableSystem.cs +++ b/Content.Server/Cuffs/CuffableSystem.cs @@ -1,6 +1,7 @@ -#nullable enable +#nullable enable using Content.Server.Cuffs.Components; using Content.Server.Hands.Components; +using Content.Shared.Hands.Components; using Content.Shared.Cuffs; using JetBrains.Annotations; using Robust.Shared.GameObjects; diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index 272a9ad3cd..840af56302 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -201,7 +201,7 @@ namespace Content.Server.GameTicking foreach (var (hand, prototype) in inhand) { var inhandEntity = _entityManager.SpawnEntity(prototype, entity.Transform.Coordinates); - handsComponent.PutInHand(inhandEntity.GetComponent(), hand); + handsComponent.TryPickupEntity(hand, inhandEntity, checkActionBlocker: false); } } } diff --git a/Content.Server/Hands/Components/HandsComponent.cs b/Content.Server/Hands/Components/HandsComponent.cs index 83701223a4..61a4b53661 100644 --- a/Content.Server/Hands/Components/HandsComponent.cs +++ b/Content.Server/Hands/Components/HandsComponent.cs @@ -1,5 +1,4 @@ #nullable enable -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -8,12 +7,9 @@ using Content.Server.Interaction; using Content.Server.Items; using Content.Server.Notification; using Content.Server.Pulling; -using Content.Shared.ActionBlocker; using Content.Shared.Audio; using Content.Shared.Body.Part; -using Content.Shared.DragDrop; using Content.Shared.Hands.Components; -using Content.Shared.Item; using Content.Shared.Notification.Managers; using Content.Shared.Physics.Pull; using Content.Shared.Pulling.Components; @@ -23,14 +19,10 @@ using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; -using Robust.Shared.Log; using Robust.Shared.Map; -using Robust.Shared.Maths; using Robust.Shared.Network; -using Robust.Shared.Physics; using Robust.Shared.Player; using Robust.Shared.Players; -using Robust.Shared.ViewVariables; namespace Content.Server.Hands.Components { @@ -42,709 +34,133 @@ namespace Content.Server.Hands.Components { [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; - private string? _activeHand; - private uint _nextHand; - - public event Action? OnItemChanged; - - [ViewVariables(VVAccess.ReadWrite)] - public string? ActiveHand - { - get => _activeHand; - set - { - if (value != null && GetHand(value) == null) - { - throw new ArgumentException($"No hand '{value}'"); - } - - _activeHand = value; - Dirty(); - } - } - - [ViewVariables] private readonly List _hands = new(); - - public IEnumerable Hands => _hands.Select(h => h.Name); - - // Mostly arbitrary. - public const float PickupRange = 2; - - [ViewVariables] public int Count => _hands.Count; - - // TODO: This does not serialize what objects are held. - protected override void Startup() - { - base.Startup(); - ActiveHand = _hands.LastOrDefault()?.Name; - } - - public IEnumerable GetAllHeldItems() - { - foreach (var hand in _hands) - { - if (hand.Entity != null) - { - yield return hand.Entity.GetComponent(); - } - } - } - - public override bool IsHolding(IEntity entity) - { - foreach (var hand in _hands) - { - if (hand.Entity == entity) - { - return true; - } - } - return false; - } - - private Hand? GetHand(string? name) - { - return _hands.FirstOrDefault(hand => hand.Name == name); - } - - public ItemComponent? GetItem(string? handName) - { - return GetHand(handName)?.Entity?.GetComponent(); - } - - public bool TryGetItem(string handName, [NotNullWhen(true)] out ItemComponent? item) - { - return (item = GetItem(handName)) != null; - } - - public ItemComponent? GetActiveHand => ActiveHand == null - ? null - : GetItem(ActiveHand); - - /// - /// Enumerates over the enabled hand keys, - /// returning the active hand first. - /// - public IEnumerable ActivePriorityEnumerable() - { - if (ActiveHand != null) - { - yield return ActiveHand; - } - - foreach (var hand in _hands) - { - if (hand.Name == ActiveHand) - { - continue; - } - - if (!hand.Enabled) - { - continue; - } - - yield return hand.Name; - } - } - - public bool PutInHand(ItemComponent item, bool mobCheck = true) - { - foreach (var hand in ActivePriorityEnumerable()) - { - if (PutInHand(item, hand, false, mobCheck)) - { - OnItemChanged?.Invoke(); - - return true; - } - } - - return false; - } - - public bool PutInHand(ItemComponent item, string index, bool fallback = true, bool mobChecks = true) - { - var hand = GetHand(index); - if (!CanPutInHand(item, index, mobChecks) || hand == null) - { - return fallback && PutInHand(item); - } - - Dirty(); - - var oldParent = item.Owner.Transform.Parent; - var oldPosition = item.Owner.Transform.Coordinates; - var contained = item.Owner.IsInContainer(); - var success = hand.Container.Insert(item.Owner); - if (success) - { - //If the entity isn't in a container, and it isn't located exactly at our position (i.e. in our own storage), then we can safely play the animation - if (oldParent != Owner.Transform && !contained) - { - SendNetworkMessage(new AnimatePickupEntityMessage(item.Owner.Uid, oldPosition)); - } - item.Owner.Transform.LocalPosition = Vector2.Zero; - OnItemChanged?.Invoke(); - } - - _entitySystemManager.GetEntitySystem().EquippedHandInteraction(Owner, item.Owner, - ToSharedHand(hand)); - - _entitySystemManager.GetEntitySystem().HandSelectedInteraction(Owner, item.Owner); - - return success; - } - - /// - /// Drops the item if doesn't have hands. - /// - public static void PutInHandOrDropStatic(IEntity mob, ItemComponent item, bool mobCheck = true) - { - if (!mob.TryGetComponent(out HandsComponent? hands)) - { - DropAtFeet(mob, item); - return; - } - - hands.PutInHandOrDrop(item, mobCheck); - } - - public void PutInHandOrDrop(ItemComponent item, bool mobCheck = true) - { - if (!PutInHand(item, mobCheck)) - { - DropAtFeet(Owner, item); - } - } - - private static void DropAtFeet(IEntity mob, ItemComponent item) - { - item.Owner.Transform.Coordinates = mob.Transform.Coordinates; - } - - public bool CanPutInHand(ItemComponent item, bool mobCheck = true) - { - if (mobCheck && !EntitySystem.Get().CanPickup(Owner)) - return false; - - foreach (var handName in ActivePriorityEnumerable()) - { - // We already did a mobCheck, so let's not waste cycles. - if (CanPutInHand(item, handName, false)) - { - return true; - } - } - - return false; - } - - public bool CanPutInHand(ItemComponent item, string index, bool mobCheck = true) - { - if (mobCheck && !EntitySystem.Get().CanPickup(Owner)) - return false; - - var hand = GetHand(index); - - return hand != null && - hand.Enabled && - hand.Container.CanInsert(item.Owner); - } - - /// - /// Calls the Dropped Interaction with the item. - /// - /// The itemcomponent of the item to be dropped - /// Check if the item can be dropped - /// If the item was dropped intentionally - /// True if IDropped.Dropped was called, otherwise false - private bool DroppedInteraction(ItemComponent item, bool doMobChecks, bool intentional) - { - var interactionSystem = _entitySystemManager.GetEntitySystem(); - if (doMobChecks) - { - if (!interactionSystem.TryDroppedInteraction(Owner, item.Owner, intentional)) - return false; - } - else - { - interactionSystem.DroppedInteraction(Owner, item.Owner, intentional); - } - return true; - } - - public bool TryHand(IEntity entity, [NotNullWhen(true)] out string? handName) - { - handName = null; - - foreach (var hand in _hands) - { - if (hand.Entity == entity) - { - handName = hand.Name; - return true; - } - } - - return false; - } - - public bool Drop(string slot, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true, bool intentional = true) - { - var hand = GetHand(slot); - if (!CanDrop(slot, doMobChecks) || !coords.IsValid(Owner.EntityManager) || hand?.Entity == null) - { - return false; - } - - var item = hand.Entity.GetComponent(); - - if (!hand.Container.Remove(hand.Entity)) - { - return false; - } - - _entitySystemManager.GetEntitySystem().UnequippedHandInteraction(Owner, item.Owner, - ToSharedHand(hand)); - - if (doDropInteraction && !DroppedInteraction(item, false, intentional)) - return false; - - item.RemovedFromSlot(); - item.Owner.Transform.Coordinates = coords; - - if (item.Owner.TryGetComponent(out var spriteComponent)) - { - spriteComponent.RenderOrder = item.Owner.EntityManager.CurrentTick.Value; - } - - if (Owner.TryGetContainer(out var container)) - { - container.Insert(item.Owner); - } - - OnItemChanged?.Invoke(); - - Dirty(); - return true; - } - - - public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true, bool intentional = true) - { - if (slot == null) - { - throw new ArgumentNullException(nameof(slot)); - } - - if (targetContainer == null) - { - throw new ArgumentNullException(nameof(targetContainer)); - } - - var hand = GetHand(slot); - if (!CanDrop(slot, doMobChecks) || hand?.Entity == null) - { - return false; - } - - if (!hand.Container.CanRemove(hand.Entity)) - { - return false; - } - - if (!targetContainer.CanInsert(hand.Entity)) - { - return false; - } - - var item = hand.Entity.GetComponent(); - - if (!hand.Container.Remove(hand.Entity)) - { - throw new InvalidOperationException(); - } - - _entitySystemManager.GetEntitySystem().UnequippedHandInteraction(Owner, item.Owner, - ToSharedHand(hand)); - - if (doDropInteraction && !DroppedInteraction(item, doMobChecks, intentional)) - return false; - - item.RemovedFromSlot(); - - if (!targetContainer.Insert(item.Owner)) - { - throw new InvalidOperationException(); - } - - OnItemChanged?.Invoke(); - - Dirty(); - return true; - } - - public bool Drop(IEntity entity, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true, bool intentional = true) - { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } - - if (!TryHand(entity, out var slot)) - { - throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity)); - } - - return Drop(slot, coords, doMobChecks, doDropInteraction, intentional); - } - - public bool Drop(string slot, bool mobChecks = true, bool doDropInteraction = true, bool intentional = true) - { - return Drop(slot, Owner.Transform.Coordinates, mobChecks, doDropInteraction, intentional); - } - - public bool Drop(IEntity entity, bool mobChecks = true, bool doDropInteraction = true, bool intentional = true) - { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } - - if (!TryHand(entity, out var slot)) - { - throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity)); - } - - return Drop(slot, Owner.Transform.Coordinates, mobChecks, doDropInteraction, intentional); - } - - public bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true, bool intentional = true) - { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } - - if (!TryHand(entity, out var slot)) - { - throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity)); - } - - return Drop(slot, targetContainer, doMobChecks, doDropInteraction, intentional); - } - - /// - /// Checks whether an item can be dropped from the specified slot. - /// - /// The slot to check for. - /// - /// True if there is an item in the slot and it can be dropped, false otherwise. - /// - public bool CanDrop(string name, bool mobCheck = true) - { - var hand = GetHand(name); - - if (mobCheck && !EntitySystem.Get().CanDrop(Owner)) - return false; - - if (hand?.Entity == null) - return false; - - return hand.Container.CanRemove(hand.Entity); - } - - public void AddHand(string name) - { - if (HasHand(name)) - { - throw new InvalidOperationException($"Hand '{name}' already exists."); - } - - var container = ContainerHelpers.CreateContainer(Owner, $"hand {_nextHand++}"); - container.OccludesLight = false; - var hand = new Hand(this, name, container); - - _hands.Add(hand); - - ActiveHand ??= name; - - OnItemChanged?.Invoke(); - Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new HandCountChangedEvent(Owner)); - - Dirty(); - } - - public void RemoveHand(string name) - { - var hand = GetHand(name); - if (hand == null) - { - throw new InvalidOperationException($"Hand '{name}' does not exist."); - } - - Drop(hand.Name, false); - hand!.Dispose(); - _hands.Remove(hand); - - if (name == ActiveHand) - { - _activeHand = _hands.FirstOrDefault()?.Name; - } - - OnItemChanged?.Invoke(); - Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new HandCountChangedEvent(Owner)); - - Dirty(); - } - - public bool HasHand(string name) - { - return _hands.Any(hand => hand.Name == name); - } - - public override ComponentState GetComponentState(ICommonSession player) - { - var hands = new SharedHand[_hands.Count]; - - for (var i = 0; i < _hands.Count; i++) - { - var hand = _hands[i].ToShared(i, IndexToHandLocation(i)); - hands[i] = hand; - } - - return new HandsComponentState(hands, ActiveHand); - } - - private HandLocation IndexToHandLocation(int index) - { - return index == 0 - ? HandLocation.Right - : index == _hands.Count - 1 - ? HandLocation.Left - : HandLocation.Middle; - } - - private SharedHand ToSharedHand(Hand hand) - { - var index = _hands.IndexOf(hand); - return hand.ToShared(index, IndexToHandLocation(index)); - } - - public void SwapHands() - { - if (ActiveHand == null) - { - return; - } - - var hand = GetHand(ActiveHand); - if (hand == null) - { - throw new InvalidOperationException($"No hand found with name {ActiveHand}"); - } - - var index = _hands.IndexOf(hand); - index++; - if (index == _hands.Count) - { - index = 0; - } - - ActiveHand = _hands[index].Name; - } - - public void ActivateItem() - { - var used = GetActiveHand?.Owner; - if (used != null) - { - var interactionSystem = _entitySystemManager.GetEntitySystem(); - interactionSystem.TryUseInteraction(Owner, used); - } - } - - public bool ThrowItem() - { - var item = GetActiveHand?.Owner; - if (item != null) - { - var interactionSystem = _entitySystemManager.GetEntitySystem(); - return interactionSystem.TryThrowInteraction(Owner, item); - } - - return false; - } + int IDisarmedAct.Priority => int.MaxValue; // We want this to be the last disarm act to run. public override void HandleMessage(ComponentMessage message, IComponent? component) { base.HandleMessage(message, component); - if (message is PullMessage pullMessage && - pullMessage.Puller.Owner != Owner) - { - return; - } - switch (message) { case PullAttemptMessage msg: - if (!_hands.Any(hand => hand.Enabled)) - { - msg.Cancelled = true; - } - + AttemptPull(msg); break; - case PullStartedMessage _: - var firstFreeHand = _hands.FirstOrDefault(hand => hand.Enabled); - - if (firstFreeHand == null) - { - break; - } - - firstFreeHand.Enabled = false; - + case PullStartedMessage: + StartPulling(); break; - case PullStoppedMessage _: - var firstOccupiedHand = _hands.FirstOrDefault(hand => !hand.Enabled); - - if (firstOccupiedHand == null) - { - break; - } - - firstOccupiedHand.Enabled = true; - - break; - case HandDisabledMsg msg: - Drop(msg.Name, false); + case PullStoppedMessage: + StopPulling(); break; } } - public override async void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null) + public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null) { base.HandleNetworkMessage(message, channel, session); - if (session == null) - { - throw new ArgumentNullException(nameof(session)); - } - switch (message) { case ClientChangedHandMsg msg: - { - var playerEntity = session.AttachedEntity; - - if (playerEntity == Owner && HasHand(msg.Index)) - { - ActiveHand = msg.Index; - } - + ActiveHand = msg.HandName; break; - } - case ClientAttackByInHandMsg msg: - { - var hand = GetHand(msg.Index); - if (hand == null) - { - Logger.WarningS("go.comp.hands", "Got a ClientAttackByInHandMsg with invalid hand name '{0}'", - msg.Index); - return; - } - - var playerEntity = session.AttachedEntity; - var used = GetActiveHand?.Owner; - - if (playerEntity == Owner && hand.Entity != null) - { - var interactionSystem = _entitySystemManager.GetEntitySystem(); - if (used != null) - { - await interactionSystem.InteractUsing(Owner, used, hand.Entity, - EntityCoordinates.Invalid); - } - else - { - var entity = hand.Entity; - interactionSystem.InteractHand(Owner, entity); - } - } - + InteractHandWithActiveHand(msg.HandName); break; - } - - case UseInHandMsg _: - { - var playerEntity = session.AttachedEntity; - var used = GetActiveHand?.Owner; - - if (playerEntity == Owner && used != null) - { - var interactionSystem = _entitySystemManager.GetEntitySystem(); - interactionSystem.TryUseInteraction(Owner, used); - } - + case UseInHandMsg: + UseActiveHeldEntity(); break; - } - case ActivateInHandMsg msg: - { - var playerEntity = session.AttachedEntity; - var used = GetItem(msg.Index)?.Owner; - - if (playerEntity == Owner && used != null) - { - var interactionSystem = _entitySystemManager.GetEntitySystem(); - interactionSystem.TryInteractionActivate(Owner, used); - } + ActivateHeldEntity(msg.HandName); + break; + case MoveItemFromHandMsg msg: + TryMoveHeldEntityToActiveHand(msg.HandName); break; - } } } - public void HandleSlotModifiedMaybe(ContainerModifiedMessage message) + protected override void OnHeldEntityRemovedFromHand(IEntity heldEntity, HandState handState) { - foreach (var hand in _hands) + if (heldEntity.TryGetComponent(out ItemComponent? item)) { - if (hand.Container != message.Container) - { - continue; - } - - Dirty(); - - if (!message.Entity.TryGetComponent(out IPhysBody? physics)) - { - return; - } - - // set velocity to zero - physics.LinearVelocity = Vector2.Zero; - return; + item.RemovedFromSlot(); + _entitySystemManager.GetEntitySystem().UnequippedHandInteraction(Owner, heldEntity, handState); + } + if (heldEntity.TryGetComponent(out SpriteComponent? sprite)) + { + sprite.RenderOrder = heldEntity.EntityManager.CurrentTick.Value; } } + protected override void DoEquippedHandInteraction(IEntity entity, HandState handState) + { + _entitySystemManager.GetEntitySystem().EquippedHandInteraction(Owner, entity, handState); + } + + protected override void DoDroppedInteraction(IEntity heldEntity, bool intentionalDrop) + { + _entitySystemManager.GetEntitySystem().DroppedInteraction(Owner, heldEntity, intentionalDrop); + } + + protected override void DoHandSelectedInteraction(IEntity entity) + { + _entitySystemManager.GetEntitySystem().HandSelectedInteraction(Owner, entity); + } + + protected override void DoHandDeselectedInteraction(IEntity entity) + { + _entitySystemManager.GetEntitySystem().HandDeselectedInteraction(Owner, entity); + } + + protected override async void DoInteraction(IEntity activeHeldEntity, IEntity heldEntity) + { + await _entitySystemManager.GetEntitySystem() + .InteractUsing(Owner, activeHeldEntity, heldEntity, EntityCoordinates.Invalid); + } + + protected override void DoActivate(IEntity heldEntity) + { + _entitySystemManager.GetEntitySystem() + .TryInteractionActivate(Owner, heldEntity); + } + + protected override void DoUse(IEntity heldEntity) + { + _entitySystemManager.GetEntitySystem() + .TryUseInteraction(Owner, heldEntity); + } + + protected override void HandlePickupAnimation(IEntity entity) + { + var pickupDirection = Owner.Transform.WorldPosition; + + var outermostEntity = entity; + while (outermostEntity.TryGetContainer(out var container)) //TODO: Use WorldPosition instead of this loop + outermostEntity = container.Owner; + + var initialPosition = outermostEntity.Transform.Coordinates; + + if (pickupDirection == initialPosition.ToMapPos(Owner.EntityManager)) + return; + + SendNetworkMessage(new PickupAnimationMessage(entity.Uid, pickupDirection, initialPosition)); + } + + #region Pull/Disarm + void IBodyPartAdded.BodyPartAdded(BodyPartAddedEventArgs args) { if (args.Part.PartType != BodyPartType.Hand) - { return; - } - AddHand(args.Slot); + var handLocation = ReadOnlyHands.Count == 0 ? HandLocation.Right : HandLocation.Left; //TODO: make hand body part have a handlocation? + + AddHand(args.Slot, handLocation); } void IBodyPartRemoved.BodyPartRemoved(BodyPartRemovedEventArgs args) { if (args.Part.PartType != BodyPartType.Hand) - { return; - } RemoveHand(args.Slot); } @@ -766,13 +182,13 @@ namespace Content.Server.Hands.Components { if (ActiveHand != null && Drop(ActiveHand, false)) { - source.PopupMessageOtherClients(Loc.GetString("hands-component-disarm-success-others-message!",("source", source.Name),("target", target.Name))); - source.PopupMessageCursor(Loc.GetString("hands-component-disarm-success-message",("target", target.Name))); + source.PopupMessageOtherClients(Loc.GetString("hands-component-disarm-success-others-message", ("disarmer", source.Name), ("disarmed", target.Name))); + source.PopupMessageCursor(Loc.GetString("hands-component-disarm-success-message", ("disarmed", target.Name))); } else { - source.PopupMessageOtherClients(Loc.GetString("hands-component-shove-success-others-message",("source", source.Name),("target", target.Name))); - source.PopupMessageCursor(Loc.GetString("hands-component-shove-success-message",("target", target.Name))); + source.PopupMessageOtherClients(Loc.GetString("hands-component-shove-success-others-message", ("shover", source.Name), ("shoved", target.Name))); + source.PopupMessageCursor(Loc.GetString("hands-component-shove-success-message", ("shoved", target.Name))); } } } @@ -780,9 +196,6 @@ namespace Content.Server.Hands.Components return true; } - // We want this to be the last disarm act to run. - int IDisarmedAct.Priority => int.MaxValue; - private bool BreakPulls() { // What is this API?? @@ -792,64 +205,179 @@ namespace Content.Server.Hands.Components return pullable.TryStopPull(); } - } - public class Hand : IDisposable - { - private bool _enabled = true; - - public Hand(HandsComponent parent, string name, ContainerSlot container) + private void AttemptPull(PullAttemptMessage msg) { - Parent = parent; - Name = name; - Container = container; - } - - private HandsComponent Parent { get; } - public string Name { get; } - public IEntity? Entity => Container.ContainedEntity; - public ContainerSlot Container { get; } - - public bool Enabled - { - get => _enabled; - set + if (!ReadOnlyHands.Any(hand => hand.Enabled)) { - if (_enabled == value) - { - return; - } - - _enabled = value; - Parent.Dirty(); - - var message = value - ? (ComponentMessage) new HandEnabledMsg(Name) - : new HandDisabledMsg(Name); - - Parent.HandleMessage(message, Parent); - Parent.Owner.SendMessage(Parent, message); + msg.Cancelled = true; } } - public void Dispose() + private void StartPulling() { - Container.Shutdown(); // TODO verify this + var firstFreeHand = Hands.FirstOrDefault(hand => hand.Enabled); + + if (firstFreeHand == null) + return; + + DisableHand(firstFreeHand); } - public SharedHand ToShared(int index, HandLocation location) + private void StopPulling() { - return new(index, Name, Entity?.Uid, location, Enabled); - } - } + var firstOccupiedHand = Hands.FirstOrDefault(hand => !hand.Enabled); - public class HandCountChangedEvent : EntityEventArgs - { - public HandCountChangedEvent(IEntity sender) - { - Sender = sender; + if (firstOccupiedHand == null) + return; + + EnableHand(firstOccupiedHand); } - public IEntity Sender { get; } + #endregion + + #region Old public methods + + public IEnumerable HandNames => ReadOnlyHands.Select(h => h.Name); + + public int Count => ReadOnlyHands.Count; + + /// + /// Returns a list of all hand names, with the active hand being first. + /// + public IEnumerable ActivePriorityEnumerable() + { + if (ActiveHand != null) + yield return ActiveHand; + + foreach (var hand in ReadOnlyHands) + { + if (hand.Name == ActiveHand || !hand.Enabled) + continue; + + yield return hand.Name; + } + } + + /// + /// Attempts to use the active held item. + /// + public void ActivateItem() + { + UseActiveHeldEntity(); + } + + /// + /// Tries to drop the contents of a hand directly under the player. + /// + public bool Drop(string handName, bool checkActionBlocker = true, bool intentionalDrop = true) + { + return TryDropHandToFloor(handName, checkActionBlocker, intentionalDrop); + } + + /// + /// Tries to drop an entity in a hand directly under the player. + /// + public bool Drop(IEntity entity, bool checkActionBlocker = true, bool intentionalDrop = true) + { + return TryDropEntityToFloor(entity, checkActionBlocker, intentionalDrop); + } + + /// + /// Tries to unequip contents of a hand directly into a container. + /// + public bool Drop(IEntity entity, BaseContainer targetContainer, bool checkActionBlocker = true) + { + return TryPutEntityIntoContainer(entity, targetContainer, checkActionBlocker); + } + + /// + /// Tries to get the ItemComponent on the entity held by a hand. + /// + public ItemComponent? GetItem(string handName) + { + if (!TryGetHeldEntity(handName, out var heldEntity)) + return null; + + heldEntity.TryGetComponent(out ItemComponent? item); + return item; + } + + /// + /// Tries to get the ItemComponent on the entity held by a hand. + /// + public bool TryGetItem(string handName, [NotNullWhen(true)] out ItemComponent? item) + { + item = null; + + if (!TryGetHeldEntity(handName, out var heldEntity)) + return false; + + return heldEntity.TryGetComponent(out item); + } + + /// + /// Tries to get the ItemComponent off the entity in the active hand. + /// + public ItemComponent? GetActiveHand + { + get + { + if (!TryGetActiveHeldEntity(out var heldEntity)) + return null; + + heldEntity.TryGetComponent(out ItemComponent? item); + return item; + } + } + + public IEnumerable GetAllHeldItems() + { + foreach (var entity in GetAllHeldEntities()) + { + if (entity.TryGetComponent(out ItemComponent? item)) + yield return item; + } + } + + /// + /// Checks if any hand can pick up an item. + /// + public bool CanPutInHand(ItemComponent item, bool mobCheck = true) + { + var entity = item.Owner; + + if (mobCheck && !PlayerCanPickup()) + return false; + + foreach (var hand in Hands) + { + if (CanInsertEntityIntoHand(hand, entity)) + return true; + } + return false; + } + + /// + /// Attempts to put an item into the active hand, or any other hand if it cannot. + /// + public bool PutInHand(ItemComponent item, bool checkActionBlocker = true) + { + return TryPutInActiveHandOrAny(item.Owner, checkActionBlocker); + } + + /// + /// Puts an item any hand, prefering the active hand, or puts it on the floor under the player. + /// + public void PutInHandOrDrop(ItemComponent item, bool checkActionBlocker = true) + { + var entity = item.Owner; + + if (!TryPutInActiveHandOrAny(entity, checkActionBlocker)) + entity.Transform.Coordinates = Owner.Transform.Coordinates; + } + + #endregion } } + diff --git a/Content.Server/Hands/Components/IHandsComponent.cs b/Content.Server/Hands/Components/IHandsComponent.cs index a048576493..c0d138742e 100644 --- a/Content.Server/Hands/Components/IHandsComponent.cs +++ b/Content.Server/Hands/Components/IHandsComponent.cs @@ -21,7 +21,7 @@ namespace Content.Server.Hands.Components /// /// The hands in this component. /// - IEnumerable Hands { get; } + IEnumerable HandNames { get; } /// /// The hand name of the currently active hand. @@ -61,18 +61,6 @@ namespace Content.Server.Hands.Components /// True if the item was inserted, false otherwise. bool PutInHand(ItemComponent item, bool mobCheck = true); - /// - /// Puts an item into a specific hand. - /// - /// The item to put in the hand. - /// The name of the hand to put the item into. - /// - /// If true and the provided hand is full, the method will fall back to - /// Whether to perform an ActionBlocker check to the entity. - /// - /// True if the item was inserted into a hand, false otherwise. - bool PutInHand(ItemComponent item, string index, bool fallback=true, bool mobCheck = true); - /// /// Checks to see if an item can be put in any hand. /// @@ -81,43 +69,19 @@ namespace Content.Server.Hands.Components /// True if the item can be inserted, false otherwise. bool CanPutInHand(ItemComponent item, bool mobCheck = true); - /// - /// Checks to see if an item can be put in the specified hand. - /// - /// The item to check for. - /// The name for the hand to check for. - /// Whether to perform an ActionBlocker check to the entity. - /// True if the item can be inserted, false otherwise. - bool CanPutInHand(ItemComponent item, string index, bool mobCheck = true); - - /// - /// Finds the hand slot holding the specified entity, if any. - /// - /// The entity to look for in our hands. - /// - /// The name of the hand slot if the entity is indeed held, - /// otherwise. - /// - /// - /// true if the entity is held, false otherwise - /// - bool TryHand(IEntity entity, [NotNullWhen(true)] out string? handName); - /// /// Drops the item contained in the slot to the same position as our entity. /// /// The slot of which to drop to drop the item. - /// Whether to check the for the mob or not. - /// Whether to perform Dropped interactions. + /// Whether to check the for the mob or not.True on success, false if something blocked the drop. - bool Drop(string slot, bool mobChecks = true, bool doDropInteraction = true, bool intentional = true); + bool Drop(string slot, bool mobChecks = true, bool intentional = true); /// /// Drops an item held by one of our hand slots to the same position as our owning entity. /// /// The item to drop. /// Whether to check the for the mob or not. - /// Whether to perform Dropped interactions. /// True on success, false if something blocked the drop. /// /// Thrown if is null. @@ -125,7 +89,7 @@ namespace Content.Server.Hands.Components /// /// Thrown if is not actually held in any hand. /// - bool Drop(IEntity entity, bool mobChecks = true, bool doDropInteraction = true, bool intentional = true); + bool Drop(IEntity entity, bool mobChecks = true, bool intentional = true); /// /// Drops the item in a slot. @@ -133,9 +97,8 @@ namespace Content.Server.Hands.Components /// The slot to drop the item from. /// /// Whether to check the for the mob or not. - /// Whether to perform Dropped interactions. /// True if an item was dropped, false otherwise. - bool Drop(string slot, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true, bool intentional = true); + bool TryDropHand(string slot, EntityCoordinates coords, bool doMobChecks = true, bool intentional = true); /// /// Drop the specified entity in our hands to a certain position. @@ -147,7 +110,6 @@ namespace Content.Server.Hands.Components /// The entity to drop, must be held in one of the hands. /// The coordinates to drop the entity at. /// Whether to check the for the mob or not. - /// Whether to perform Dropped interactions. /// /// True if the drop succeeded, /// false if it failed (due to failing to eject from our hand slot, etc...) @@ -158,22 +120,21 @@ namespace Content.Server.Hands.Components /// /// Thrown if is not actually held in any hand. /// - bool Drop(IEntity entity, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true, bool intentional = true); + bool TryDropEntity(IEntity entity, EntityCoordinates coords, bool doMobChecks = true, bool intentional = true); /// /// Drop the item contained in a slot into another container. /// /// The slot of which to drop the entity. /// The container to drop into. - /// Whether to check the for the mob or not. - /// Whether to perform Dropped interactions. + /// Whether to check the for the mob or not. /// True on success, false if something was blocked (insertion or removal). /// /// Thrown if dry-run checks reported OK to remove and insert, /// but practical remove or insert returned false anyways. /// This is an edge-case that is currently unhandled. /// - bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true, bool intentional = true); + bool TryPutHandIntoContainer(string slot, BaseContainer targetContainer, bool doMobChecks = true); /// /// Drops an item in one of the hands into a container. @@ -181,7 +142,6 @@ namespace Content.Server.Hands.Components /// The item to drop. /// The container to drop into. /// Whether to check the for the mob or not. - /// Whether to perform Dropped interactions. /// True on success, false if something was blocked (insertion or removal). /// /// Thrown if dry-run checks reported OK to remove and insert, @@ -194,7 +154,7 @@ namespace Content.Server.Hands.Components /// /// Thrown if is not actually held in any hand. /// - bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true, bool intentional = true); + bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true); /// /// Checks whether the item in the specified hand can be dropped. @@ -213,7 +173,7 @@ namespace Content.Server.Hands.Components /// /// Thrown if a hand with specified name already exists. /// - void AddHand(string name); + void AddHand(string name, HandLocation handLocation); /// /// Removes a hand from this hands component. @@ -230,7 +190,5 @@ namespace Content.Server.Hands.Components /// The hand name to check. /// True if the hand exists, false otherwise. bool HasHand(string name); - - void HandleSlotModifiedMaybe(ContainerModifiedMessage message); } } diff --git a/Content.Server/Hands/HandsSystem.cs b/Content.Server/Hands/HandsSystem.cs index 88e2d8b091..a96c8e2693 100644 --- a/Content.Server/Hands/HandsSystem.cs +++ b/Content.Server/Hands/HandsSystem.cs @@ -9,10 +9,9 @@ using Content.Server.Stack; using Content.Server.Storage.Components; using Content.Server.Throwing; using Content.Shared.Examine; +using Content.Shared.Hands; +using Content.Shared.Hands.Components; using Content.Shared.Input; -using Content.Shared.Interaction; -using Content.Shared.Movement.Components; -using Content.Shared.Notification; using Content.Shared.Notification.Managers; using JetBrains.Annotations; using Robust.Server.Player; @@ -28,22 +27,15 @@ using static Content.Shared.Inventory.EquipmentSlotDefines; namespace Content.Server.Hands { [UsedImplicitly] - internal sealed class HandsSystem : EntitySystem + internal sealed class HandsSystem : SharedHandsSystem { - private const float ThrowForce = 1.5f; // Throwing force of mobs in Newtons - - /// public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(HandleContainerModified); - SubscribeLocalEvent(HandleContainerModified); SubscribeLocalEvent(HandleExamined); CommandBinds.Builder - .Bind(ContentKeyFunctions.SwapHands, InputCmdHandler.FromDelegate(HandleSwapHands)) - .Bind(ContentKeyFunctions.Drop, new PointerInputCmdHandler(HandleDrop)) .Bind(ContentKeyFunctions.ActivateItemInHand, InputCmdHandler.FromDelegate(HandleActivateItem)) .Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem)) .Bind(ContentKeyFunctions.SmartEquipBackpack, InputCmdHandler.FromDelegate(HandleSmartEquipBackpack)) @@ -51,7 +43,6 @@ namespace Content.Server.Hands .Register(); } - /// public override void Shutdown() { base.Shutdown(); @@ -59,14 +50,6 @@ namespace Content.Server.Hands CommandBinds.Unregister(); } - private static void HandleContainerModified(ContainerModifiedMessage args) - { - if (args.Container.Owner.TryGetComponent(out IHandsComponent? handsComponent)) - { - handsComponent.HandleSlotModifiedMaybe(args); - } - } - //TODO: Actually shows all items/clothing/etc. private void HandleExamined(EntityUid uid, HandsComponent component, ExaminedEvent args) { @@ -76,142 +59,69 @@ namespace Content.Server.Hands } } - private static bool TryGetAttachedComponent(IPlayerSession? session, [NotNullWhen(true)] out T? component) - where T : Component + protected override void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args) { - component = default; - - var ent = session?.AttachedEntity; - - if (ent == null || !ent.IsValid() || !ent.TryGetComponent(out T? comp)) - { - return false; - } - - component = comp; - return true; + component.Dirty(); } - private static void HandleSwapHands(ICommonSession? session) + private bool TryGetHandsComp(ICommonSession? session, [NotNullWhen(true)] out SharedHandsComponent? hands) { - if (!TryGetAttachedComponent(session as IPlayerSession, out HandsComponent? handsComp)) - { - return; - } + hands = default; - var interactionSystem = Get(); - - var oldItem = handsComp.GetActiveHand; - - handsComp.SwapHands(); - - var newItem = handsComp.GetActiveHand; - - if (oldItem != null) - { - interactionSystem.HandDeselectedInteraction(handsComp.Owner, oldItem.Owner); - } - - if (newItem != null) - { - interactionSystem.HandSelectedInteraction(handsComp.Owner, newItem.Owner); - } - } - - private bool HandleDrop(ICommonSession? session, EntityCoordinates coords, EntityUid uid) - { - var ent = ((IPlayerSession?) session)?.AttachedEntity; - - if (ent == null || !ent.IsValid()) + if (session is not IPlayerSession playerSession) return false; - if (!ent.TryGetComponent(out HandsComponent? handsComp)) - return false; - - if (handsComp.ActiveHand == null || handsComp.GetActiveHand == null) - return false; - - // It's important to note that the calculations are done in map coordinates (they're absolute). - // They're translated back to EntityCoordinates at the end. - var entMap = ent.Transform.MapPosition; - var targetPos = coords.ToMapPos(EntityManager); - var dropVector = targetPos - entMap.Position; - var targetVector = Vector2.Zero; - - if (dropVector != Vector2.Zero) - { - var targetLength = MathF.Min(dropVector.Length, SharedInteractionSystem.InteractionRange - 0.001f); // InteractionRange is reduced due to InRange not dealing with floating point error - var newCoords = new MapCoordinates(dropVector.Normalized * targetLength + entMap.Position, entMap.MapId); - var rayLength = Get().UnobstructedDistance(entMap, newCoords, ignoredEnt: ent); - targetVector = dropVector.Normalized * rayLength; - } - - var resultMapCoordinates = new MapCoordinates(entMap.Position + targetVector, entMap.MapId); - var resultEntCoordinates = EntityCoordinates.FromMap(coords.GetParent(EntityManager), resultMapCoordinates); - handsComp.Drop(handsComp.ActiveHand, resultEntCoordinates); - - return true; - } - - private static void HandleActivateItem(ICommonSession? session) - { - if (!TryGetAttachedComponent(session as IPlayerSession, out HandsComponent? handsComp)) - return; - - handsComp.ActivateItem(); - } - - private bool HandleThrowItem(ICommonSession? session, EntityCoordinates coords, EntityUid uid) - { - var playerEnt = ((IPlayerSession?) session)?.AttachedEntity; + var playerEnt = playerSession?.AttachedEntity; if (playerEnt == null || !playerEnt.IsValid()) return false; - if (!playerEnt.TryGetComponent(out HandsComponent? handsComp)) + playerEnt.TryGetComponent(out hands); + return hands != null; + } + + private void HandleActivateItem(ICommonSession? session) + { + if (!TryGetHandsComp(session, out var hands)) + return; + + hands.UseActiveHeldEntity(); + } + + private bool HandleThrowItem(ICommonSession? session, EntityCoordinates coords, EntityUid uid) + { + if (session is not IPlayerSession playerSession) return false; - if (handsComp.ActiveHand == null || !handsComp.CanDrop(handsComp.ActiveHand)) + var playerEnt = playerSession.AttachedEntity; + + if (playerEnt == null || !playerEnt.IsValid() || !playerEnt.TryGetComponent(out SharedHandsComponent? hands)) return false; - var throwEnt = handsComp.GetItem(handsComp.ActiveHand)?.Owner; - - if (throwEnt == null) + if (!hands.TryGetActiveHeldEntity(out var throwEnt)) return false; - if (!handsComp.ThrowItem()) + if (!Get().TryThrowInteraction(hands.Owner, throwEnt)) return false; - // throw the item, split off from a stack if it's meant to be thrown individually - if (!throwEnt.TryGetComponent(out StackComponent? stackComp) || stackComp.Count < 2 || !stackComp.ThrowIndividually) + if (throwEnt.TryGetComponent(out StackComponent? stack) && stack.Count > 1 && stack.ThrowIndividually) { - handsComp.Drop(handsComp.ActiveHand); - } - else - { - var splitStack = Get().Split(throwEnt.Uid, stackComp, 1, playerEnt.Transform.Coordinates); + var splitStack = Get().Split(throwEnt.Uid, stack, 1, playerEnt.Transform.Coordinates); if (splitStack == null) return false; throwEnt = splitStack; } + else if (!hands.TryDropEntityToFloor(throwEnt)) + return false; var direction = coords.ToMapPos(EntityManager) - playerEnt.Transform.WorldPosition; - if (direction == Vector2.Zero) return true; + if (direction == Vector2.Zero) + return true; - direction = direction.Normalized * MathF.Min(direction.Length, 8.0f); - var yeet = direction * ThrowForce * 15; - - // Softer yeet in weightlessness - if (playerEnt.IsWeightless()) - { - throwEnt.TryThrow(yeet / 4, playerEnt, 10.0f); - } - else - { - throwEnt.TryThrow(yeet, playerEnt); - } + var throwVec = direction.Normalized * MathF.Min(direction.Length, hands.ThrowRange) * hands.ThrowForceMultiplier; + throwEnt.TryThrow(throwVec, playerEnt); return true; } @@ -228,26 +138,26 @@ namespace Content.Server.Hands private void HandleSmartEquip(ICommonSession? session, Slots equipmentSlot) { - var plyEnt = ((IPlayerSession?) session)?.AttachedEntity; + if (session is not IPlayerSession playerSession) + return; + + var plyEnt = playerSession.AttachedEntity; if (plyEnt == null || !plyEnt.IsValid()) return; - if (!plyEnt.TryGetComponent(out HandsComponent? handsComp) || - !plyEnt.TryGetComponent(out InventoryComponent? inventoryComp)) + if (!plyEnt.TryGetComponent(out SharedHandsComponent? hands) || + !plyEnt.TryGetComponent(out InventoryComponent? inventory)) return; - if (!inventoryComp.TryGetSlotItem(equipmentSlot, out ItemComponent? equipmentItem) - || !equipmentItem.Owner.TryGetComponent(out var storageComponent)) + if (!inventory.TryGetSlotItem(equipmentSlot, out ItemComponent? equipmentItem) || + !equipmentItem.Owner.TryGetComponent(out ServerStorageComponent? storageComponent)) { - plyEnt.PopupMessage(Loc.GetString("hands-system-missing-equipment-slot", - ("slotName", SlotNames[equipmentSlot].ToLower()))); + plyEnt.PopupMessage(Loc.GetString("hands-system-missing-equipment-slot", ("equipment", SlotNames[equipmentSlot].ToLower()))); return; } - var heldItem = handsComp.GetItem(handsComp.ActiveHand)?.Owner; - - if (heldItem != null) + if (hands.ActiveHandIsHoldingEntity()) { storageComponent.PlayerInsertHeldEntity(plyEnt); } @@ -255,14 +165,16 @@ namespace Content.Server.Hands { if (storageComponent.StoredEntities.Count == 0) { - plyEnt.PopupMessage(Loc.GetString("hands-system-empty-equipment-slot", - ("slotName", SlotNames[equipmentSlot].ToLower()))); + plyEnt.PopupMessage(Loc.GetString("hands-system-empty-equipment-slot", ("equipment", SlotNames[equipmentSlot].ToLower()))); } else { var lastStoredEntity = Enumerable.Last(storageComponent.StoredEntities); if (storageComponent.Remove(lastStoredEntity)) - handsComp.PutInHandOrDrop(lastStoredEntity.GetComponent()); + { + if (!hands.TryPickupEntityToActiveHand(lastStoredEntity)) + lastStoredEntity.Transform.Coordinates = plyEnt.Transform.Coordinates; + } } } } diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index 6baeadffc1..8ec860beab 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -575,7 +575,7 @@ namespace Content.Server.Interaction /// Calls EquippedHand on all components that implement the IEquippedHand interface /// on an item. /// - public void EquippedHandInteraction(IEntity user, IEntity item, SharedHand hand) + public void EquippedHandInteraction(IEntity user, IEntity item, HandState hand) { var equippedHandMessage = new EquippedHandEvent(user, item, hand); RaiseLocalEvent(item.Uid, equippedHandMessage); @@ -594,7 +594,7 @@ namespace Content.Server.Interaction /// Calls UnequippedHand on all components that implement the IUnequippedHand interface /// on an item. /// - public void UnequippedHandInteraction(IEntity user, IEntity item, SharedHand hand) + public void UnequippedHandInteraction(IEntity user, IEntity item, HandState hand) { var unequippedHandMessage = new UnequippedHandEvent(user, item, hand); RaiseLocalEvent(item.Uid, unequippedHandMessage); @@ -761,7 +761,7 @@ namespace Content.Server.Interaction var ev = new WideAttackEvent(item, user, coordinates); RaiseLocalEvent(item.Uid, ev, false); - if(ev.Handled) + if (ev.Handled) return; } else @@ -769,7 +769,7 @@ namespace Content.Server.Interaction var ev = new ClickAttackEvent(item, user, coordinates, targetUid); RaiseLocalEvent(item.Uid, ev, false); - if(ev.Handled) + if (ev.Handled) return; } } @@ -785,7 +785,7 @@ namespace Content.Server.Interaction // TODO: Make this saner? // Attempt to do unarmed combat. We don't check for handled just because at this point it doesn't matter. - if(wideAttack) + if (wideAttack) RaiseLocalEvent(user.Uid, new WideAttackEvent(user, user, coordinates), false); else RaiseLocalEvent(user.Uid, new ClickAttackEvent(user, user, coordinates, targetUid), false); diff --git a/Content.Server/Inventory/Components/InventoryComponent.cs b/Content.Server/Inventory/Components/InventoryComponent.cs index f35374f01f..6ba250d52d 100644 --- a/Content.Server/Inventory/Components/InventoryComponent.cs +++ b/Content.Server/Inventory/Components/InventoryComponent.cs @@ -522,7 +522,7 @@ namespace Content.Server.Inventory.Components var activeItem = hands.GetActiveHand; if (activeHand != null && activeItem != null && activeItem.Owner.TryGetComponent(out ItemComponent? clothing)) { - hands.Drop(activeHand, doDropInteraction: false); + hands.TryDropNoInteraction(); if (!Equip(msg.Inventoryslot, clothing, true, out var reason)) { hands.PutInHand(clothing); diff --git a/Content.Server/Items/ItemComponent.cs b/Content.Server/Items/ItemComponent.cs index 4441757f64..853d9ffb61 100644 --- a/Content.Server/Items/ItemComponent.cs +++ b/Content.Server/Items/ItemComponent.cs @@ -31,23 +31,6 @@ namespace Content.Server.Items } } - public override bool TryPutInHand(IEntity user) - { - if (!CanPickup(user)) - return false; - - if (!user.TryGetComponent(out IHandsComponent? hands)) - return false; - - var activeHand = hands.ActiveHand; - - if (activeHand == null) - return false; - - hands.PutInHand(this, activeHand, false); - return true; - } - [Verb] public sealed class PickUpVerb : Verb { @@ -74,3 +57,4 @@ namespace Content.Server.Items } } } + diff --git a/Content.Server/PDA/PDAComponent.cs b/Content.Server/PDA/PDAComponent.cs index 91f1a25d93..0b01d5ed7d 100644 --- a/Content.Server/PDA/PDAComponent.cs +++ b/Content.Server/PDA/PDAComponent.cs @@ -10,6 +10,7 @@ using Content.Server.Items; using Content.Server.PDA.Managers; using Content.Server.UserInterface; using Content.Shared.ActionBlocker; +using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Notification.Managers; @@ -123,19 +124,21 @@ namespace Content.Server.PDA case PDAUplinkBuyListingMessage buyMsg: { - if (message.Session.AttachedEntity == null) + var player = message.Session.AttachedEntity; + if (player == null) break; if (!_uplinkManager.TryPurchaseItem(_syndicateUplinkAccount, buyMsg.ItemId, - message.Session.AttachedEntity.Transform.Coordinates, out var entity)) + player.Transform.Coordinates, out var entity)) { SendNetworkMessage(new PDAUplinkInsufficientFundsMessage(), message.Session.ConnectedClient); break; } - HandsComponent.PutInHandOrDropStatic( - message.Session.AttachedEntity, - entity.GetComponent()); + if (!player.TryGetComponent(out HandsComponent? hands) || !entity.TryGetComponent(out ItemComponent? item)) + break; + + hands.PutInHandOrDrop(item); SendNetworkMessage(new PDAUplinkBuySuccessMessage(), message.Session.ConnectedClient); break; diff --git a/Content.Server/Strip/StrippableComponent.cs b/Content.Server/Strip/StrippableComponent.cs index 07d7dc6495..05a4927e41 100644 --- a/Content.Server/Strip/StrippableComponent.cs +++ b/Content.Server/Strip/StrippableComponent.cs @@ -128,7 +128,7 @@ namespace Content.Server.Strip return dictionary; } - foreach (var hand in hands.Hands) + foreach (var hand in hands.HandNames) { dictionary[hand] = hands.GetItem(hand)?.Owner.Name ?? "None"; } @@ -243,7 +243,7 @@ namespace Content.Server.Strip return false; } - if (!hands.CanPutInHand(item, hand, false)) + if (!hands.CanPickupEntity(hand, item.Owner, checkActionBlocker: false)) { user.PopupMessageCursor(Loc.GetString("strippable-component-cannot-put-message",("owner", Owner))); return false; @@ -267,8 +267,8 @@ namespace Content.Server.Strip var result = await doAfterSystem.DoAfter(doAfterArgs); if (result != DoAfterStatus.Finished) return; - userHands.Drop(hand, false); - hands.PutInHand(item!, hand, false, false); + userHands.Drop(hand); + hands.TryPickupEntity(hand, item!.Owner, checkActionBlocker: false); UpdateSubscribed(); } diff --git a/Content.Shared/Actions/Components/ItemActionsComponent.cs b/Content.Shared/Actions/Components/ItemActionsComponent.cs index d433248bff..70df7310dd 100644 --- a/Content.Shared/Actions/Components/ItemActionsComponent.cs +++ b/Content.Shared/Actions/Components/ItemActionsComponent.cs @@ -42,7 +42,7 @@ namespace Content.Shared.Actions.Components /// /// hand it's currently in, null if not in a hand. /// - public SharedHand? InHand { get; private set; } + public HandState? InHand { get; private set; } /// /// Entity currently holding this in hand or equip slot. Null if not held. diff --git a/Content.Shared/Hands/Components/SharedHandsComponent.cs b/Content.Shared/Hands/Components/SharedHandsComponent.cs index d1b62de09a..fb02eefccc 100644 --- a/Content.Shared/Hands/Components/SharedHandsComponent.cs +++ b/Content.Shared/Hands/Components/SharedHandsComponent.cs @@ -1,51 +1,876 @@ #nullable enable using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.ActionBlocker; +using Content.Shared.Interaction; using Content.Shared.NetIDs; +using Robust.Shared.Containers; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Players; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.ViewVariables; namespace Content.Shared.Hands.Components { public abstract class SharedHandsComponent : Component, ISharedHandsComponent { public sealed override string Name => "Hands"; + public sealed override uint? NetID => ContentNetIDs.HANDS; - /// true if the item is in one of the hands - public abstract bool IsHolding(IEntity item); + public event Action? OnItemChanged; //TODO: Try to replace C# event + + /// + /// The name of the currently active hand. + /// + [ViewVariables(VVAccess.ReadWrite)] + public string? ActiveHand + { + get => _activeHand; + set + { + if (value != null && !HasHand(value)) + { + Logger.Warning($"{nameof(SharedHandsComponent)} on {Owner} tried to set its active hand to {value}, which was not a hand."); + return; + } + if (value == null && Hands.Count != 0) + { + Logger.Error($"{nameof(SharedHandsComponent)} on {Owner} tried to set its active hand to null, when it still had another hand."); + _activeHand = Hands[0].Name; + return; + } + if (value != ActiveHand) + { + DeselectActiveHeldEntity(); + _activeHand = value; + SelectActiveHeldEntity(); + + HandsModified(); + } + } + } + private string? _activeHand; + + [ViewVariables] + public IReadOnlyList ReadOnlyHands => Hands; + protected readonly List Hands = new(); + + /// + /// The amount of throw impulse per distance the player is from the throw target. + /// + [DataField("throwForceMultiplier")] + [ViewVariables(VVAccess.ReadWrite)] + public float ThrowForceMultiplier { get; set; } = 14f; //should be tuned so that a thrown item lands about under the player's cursor + + /// + /// Distance after which longer throw targets stop increasing throw impulse. + /// + [DataField("throwRange")] + [ViewVariables(VVAccess.ReadWrite)] + public float ThrowRange { get; set; } = 8f; + + public override ComponentState GetComponentState(ICommonSession player) + { + var hands = new HandState[Hands.Count]; + + for (var i = 0; i < Hands.Count; i++) + { + var hand = Hands[i].ToHandState(); + hands[i] = hand; + } + return new HandsComponentState(hands, ActiveHand); + } + + public virtual void HandsModified() + { + Dirty(); + } + + public void AddHand(string handName, HandLocation handLocation) + { + if (HasHand(handName)) + return; + + var container = ContainerHelpers.CreateContainer(Owner, handName); + container.OccludesLight = false; + + Hands.Add(new Hand(handName, true, handLocation, container)); + + if (ActiveHand == null) + ActiveHand = handName; + + HandCountChanged(); + + HandsModified(); + } + + public void RemoveHand(string handName) + { + if (!TryGetHand(handName, out var hand)) + return; + + RemoveHand(hand); + } + + private void RemoveHand(Hand hand) + { + DropHeldEntityToFloor(hand, intentionalDrop: false); + hand.Container?.Shutdown(); + Hands.Remove(hand); + + if (ActiveHand == hand.Name) + ActiveHand = ReadOnlyHands.FirstOrDefault()?.Name; + + HandCountChanged(); + + HandsModified(); + } + + public bool HasHand(string handName) + { + foreach (var hand in Hands) + { + if (hand.Name == handName) + return true; + } + return false; + } + + private Hand? GetHand(string handName) + { + foreach (var hand in Hands) + { + if (hand.Name == handName) + return hand; + } + return null; + } + + private Hand? GetActiveHand() + { + if (ActiveHand == null) + return null; + + return GetHand(ActiveHand); + } + + protected bool TryGetHand(string handName, [NotNullWhen(true)] out Hand? foundHand) + { + foundHand = GetHand(handName); + return foundHand != null; + } + + protected bool TryGetActiveHand([NotNullWhen(true)] out Hand? activeHand) + { + activeHand = GetActiveHand(); + return activeHand != null; + } + + #region Held Entities + + public bool ActiveHandIsHoldingEntity() + { + if (!TryGetActiveHand(out var hand)) + return false; + + return hand.HeldEntity != null; + } + + public bool TryGetHeldEntity(string handName, [NotNullWhen(true)] out IEntity? heldEntity) + { + heldEntity = null; + + if (!TryGetHand(handName, out var hand)) + return false; + + heldEntity = hand.HeldEntity; + return heldEntity != null; + } + + public bool TryGetActiveHeldEntity([NotNullWhen(true)] out IEntity? heldEntity) + { + heldEntity = GetActiveHand()?.HeldEntity; + return heldEntity != null; + } + + public bool IsHolding(IEntity entity) + { + foreach (var hand in Hands) + { + if (hand.HeldEntity == entity) + return true; + } + return false; + } + + public IEnumerable GetAllHeldEntities() + { + foreach (var hand in ReadOnlyHands) + { + if (hand.HeldEntity != null) + yield return hand.HeldEntity; + } + } + + private bool TryGetHandHoldingEntity(IEntity entity, [NotNullWhen(true)] out Hand? handFound) + { + handFound = null; + + foreach (var hand in Hands) + { + if (hand.HeldEntity == entity) + { + handFound = hand; + return true; + } + } + return false; + } + + #endregion + + #region Dropping + + /// + /// Checks all the conditions relevant to a player being able to drop an item. + /// + public bool CanDrop(string handName, bool checkActionBlocker = true) + { + if (!TryGetHand(handName, out var hand)) + return false; + + if (!CanRemoveHeldEntityFromHand(hand)) + return false; + + if (checkActionBlocker && !PlayerCanDrop()) + return false; + + return true; + } + + /// + /// Tries to drop the contents of the active hand to the target location. + /// + public bool TryDropActiveHand(EntityCoordinates targetDropLocation, bool doMobChecks = true, bool intentional = true) + { + if (!TryGetActiveHand(out var hand)) + return false; + + return TryDropHeldEntity(hand, targetDropLocation, doMobChecks, intentional); + } + + /// + /// Tries to drop the contents of a hand to the target location. + /// + public bool TryDropHand(string handName, EntityCoordinates targetDropLocation, bool checkActionBlocker = true, bool intentional = true) + { + if (!TryGetHand(handName, out var hand)) + return false; + + return TryDropHeldEntity(hand, targetDropLocation, checkActionBlocker, intentional); + } + + /// + /// Tries to drop a held entity to the target location. + /// + public bool TryDropEntity(IEntity entity, EntityCoordinates coords, bool doMobChecks = true, bool intentional = true) + { + if (!TryGetHandHoldingEntity(entity, out var hand)) + return false; + + return TryDropHeldEntity(hand, coords, doMobChecks, intentional); + } + + /// + /// Attempts to move the contents of a hand into a container that is not another hand, without dropping it on the floor inbetween. + /// + public bool TryPutHandIntoContainer(string handName, BaseContainer targetContainer, bool checkActionBlocker = true) + { + if (!TryGetHand(handName, out var hand)) + return false; + + if (!CanPutHeldEntityIntoContainer(hand, targetContainer, checkActionBlocker)) + return false; + + PutHeldEntityIntoContainer(hand, targetContainer); + return true; + } + + /// + /// Attempts to move a held item from a hand into a container that is not another hand, without dropping it on the floor inbetween. + /// + public bool TryPutEntityIntoContainer(IEntity entity, BaseContainer targetContainer, bool checkActionBlocker = true) + { + if (!TryGetHandHoldingEntity(entity, out var hand)) + return false; + + if (!CanPutHeldEntityIntoContainer(hand, targetContainer, checkActionBlocker)) + return false; + + PutHeldEntityIntoContainer(hand, targetContainer); + return true; + } + + /// + /// Tries to drop the contents of a hand directly under the player. + /// + public bool TryDropHandToFloor(string handName, bool checkActionBlocker = true, bool intentionalDrop = true) + { + if (!TryGetHand(handName, out var hand)) + return false; + + return TryDropHeldEntity(hand, Owner.Transform.Coordinates, checkActionBlocker, intentionalDrop); + } + + /// + /// Tries to drop a held entity directly under the player. + /// + public bool TryDropEntityToFloor(IEntity entity, bool checkActionBlocker = true, bool intentionalDrop = true) + { + if (!TryGetHandHoldingEntity(entity, out var hand)) + return false; + + return TryDropHeldEntity(hand, Owner.Transform.Coordinates, checkActionBlocker, intentionalDrop); + } + + /// + /// Tries to remove the item in the active hand, without dropping it. + /// For transfering the held item to anothe rlocation, like an inventory slot, + /// which souldn't trigger the drop interaction + public bool TryDropNoInteraction() + { + if (!TryGetActiveHand(out var hand)) + return false; + + if (!CanRemoveHeldEntityFromHand(hand)) + return false; + + RemoveHeldEntityFromHand(hand); + return true; + } + + /// + /// Checks if the contents of a hand is able to be removed from its container. + /// + private bool CanRemoveHeldEntityFromHand(Hand hand) + { + var heldEntity = hand.HeldEntity; + + if (heldEntity == null) + return false; + + var handContainer = hand.Container; + if (handContainer == null) + return false; + + if (!handContainer.CanRemove(heldEntity)) + return false; + + return true; + } + + /// + /// Checks if the player is allowed to perform drops. + /// + private bool PlayerCanDrop() + { + if (!IoCManager.Resolve().GetEntitySystem().CanDrop(Owner)) + return false; + + return true; + } + + /// + /// Removes the contents of a hand from its container. Assumes that the removal is allowed. + /// + private void RemoveHeldEntityFromHand(Hand hand) + { + var heldEntity = hand.HeldEntity; + + if (heldEntity == null) + return; + + var handContainer = hand.Container; + if (handContainer == null) + return; + + if (hand.Name == ActiveHand) + DeselectActiveHeldEntity(); + + if (!handContainer.Remove(heldEntity)) + { + Logger.Error($"{nameof(SharedHandsComponent)} on {Owner} could not remove {heldEntity} from {handContainer}."); + return; + } + OnHeldEntityRemovedFromHand(heldEntity, hand.ToHandState()); + + HandsModified(); + } + + /// + /// Drops a hands contents to the target location. + /// + private void DropHeldEntity(Hand hand, EntityCoordinates targetDropLocation, bool intentionalDrop = true) + { + var heldEntity = hand.HeldEntity; + + if (heldEntity == null) + return; + + RemoveHeldEntityFromHand(hand); + + DoDroppedInteraction(heldEntity, intentionalDrop); + + heldEntity.Transform.Coordinates = GetFinalDropCoordinates(targetDropLocation); + + OnItemChanged?.Invoke(); + } + + /// + /// Calculates the final location a dropped item will end up at, accounting for max drop range and collision along the targeted drop path. + /// + private EntityCoordinates GetFinalDropCoordinates(EntityCoordinates targetCoords) + { + var origin = Owner.Transform.MapPosition; + var other = targetCoords.ToMap(Owner.EntityManager); + + var dropLength = EntitySystem.Get().UnobstructedDistance(origin, other, ignoredEnt: Owner); + dropLength = MathF.Min(dropLength, SharedInteractionSystem.InteractionRange); + + var dropVector = origin.Position; + if (dropLength != 0) + dropVector += (other.Position - origin.Position).Normalized * dropLength; + + return targetCoords.WithPosition(dropVector); + } + + /// + /// Tries to drop a hands contents to the target location. + /// + private bool TryDropHeldEntity(Hand hand, EntityCoordinates location, bool checkActionBlocker, bool intentionalDrop = true) + { + if (!CanRemoveHeldEntityFromHand(hand)) + return false; + + if (checkActionBlocker && !PlayerCanDrop()) + return false; + + DropHeldEntity(hand, location, intentionalDrop); + return true; + } + + /// + /// Drops the contents of a hand directly under the player. + /// + private void DropHeldEntityToFloor(Hand hand, bool intentionalDrop = true) + { + DropHeldEntity(hand, Owner.Transform.Coordinates, intentionalDrop); + } + + private bool CanPutHeldEntityIntoContainer(Hand hand, IContainer targetContainer, bool checkActionBlocker) + { + var heldEntity = hand.HeldEntity; + + if (heldEntity == null) + return false; + + if (checkActionBlocker && !PlayerCanDrop()) + return false; + + if (!targetContainer.CanInsert(heldEntity)) + return false; + + return true; + } + + /// + /// For putting the contents of a hand into a container that is not another hand. + /// + private void PutHeldEntityIntoContainer(Hand hand, IContainer targetContainer) + { + var heldEntity = hand.HeldEntity; + + if (heldEntity == null) + return; + + RemoveHeldEntityFromHand(hand); + + if (!targetContainer.Insert(heldEntity)) + { + Logger.Error($"{nameof(SharedHandsComponent)} on {Owner} could not insert {heldEntity} into {targetContainer}."); + return; + } + } + + #endregion + + #region Pickup + + public bool CanPickupEntity(string handName, IEntity entity, bool checkActionBlocker = true) + { + if (!TryGetHand(handName, out var hand)) + return false; + + if (checkActionBlocker && !PlayerCanPickup()) + return false; + + if (!CanInsertEntityIntoHand(hand, entity)) + return false; + + return true; + } + + public bool CanPickupEntityToActiveHand(IEntity entity, bool checkActionBlocker = true) + { + if (!TryGetActiveHand(out var hand)) + return false; + + if (checkActionBlocker && !PlayerCanPickup()) + return false; + + if (!CanInsertEntityIntoHand(hand, entity)) + return false; + + return true; + } + + /// + /// Tries to pick up an entity to a specific hand. + /// + public bool TryPickupEntity(string handName, IEntity entity, bool checkActionBlocker = true) + { + if (!TryGetHand(handName, out var hand)) + return false; + + return TryPickupEntity(hand, entity, checkActionBlocker); + } + + public bool TryPickupEntityToActiveHand(IEntity entity, bool checkActionBlocker = true) + { + if (!TryGetActiveHand(out var hand)) + return false; + + return TryPickupEntity(hand, entity, checkActionBlocker); + } + + /// + /// Checks if an entity can be put into a hand's container. + /// + protected bool CanInsertEntityIntoHand(Hand hand, IEntity entity) + { + if (!hand.Enabled) + return false; + + var handContainer = hand.Container; + if (handContainer == null) + return false; + + if (!handContainer.CanInsert(entity)) + return false; + + return true; + } + + /// + /// Checks if the player is allowed to perform pickup actions. + /// + /// + protected bool PlayerCanPickup() + { + if (!IoCManager.Resolve().GetEntitySystem().CanPickup(Owner)) + return false; + + return true; + } + + /// + /// Puts an entity into the player's hand, assumes that the insertion is allowed. + /// + private void PutEntityIntoHand(Hand hand, IEntity entity) + { + var handContainer = hand.Container; + if (handContainer == null) + return; + + if (!handContainer.Insert(entity)) + { + Logger.Error($"{nameof(SharedHandsComponent)} on {Owner} could not insert {entity} into {handContainer}."); + return; + } + + DoEquippedHandInteraction(entity, hand.ToHandState()); + + if (hand.Name == ActiveHand) + SelectActiveHeldEntity(); + + entity.Transform.LocalPosition = Vector2.Zero; + + OnItemChanged?.Invoke(); + + HandsModified(); + } + + private bool TryPickupEntity(Hand hand, IEntity entity, bool checkActionBlocker = true) + { + if (!CanInsertEntityIntoHand(hand, entity)) + return false; + + if (checkActionBlocker && !PlayerCanPickup()) + return false; + + HandlePickupAnimation(entity); + PutEntityIntoHand(hand, entity); + return true; + } + + #endregion + + #region Hand Interactions + + /// + /// Get the name of the hand that a swap hands would result in. + /// + public bool TryGetSwapHandsResult([NotNullWhen(true)] out string? nextHand) + { + nextHand = null; + + if (!TryGetActiveHand(out var activeHand) || Hands.Count == 1) + return false; + + var newActiveIndex = Hands.IndexOf(activeHand) + 1; + var finalHandIndex = Hands.Count - 1; + if (newActiveIndex > finalHandIndex) + newActiveIndex = 0; + + nextHand = ReadOnlyHands[newActiveIndex].Name; + return true; + } + + /// + /// Attempts to interact with the item in a hand using the active held item. + /// + public void InteractHandWithActiveHand(string handName) + { + if (!TryGetActiveHeldEntity(out var activeHeldEntity)) + return; + + if (!TryGetHeldEntity(handName, out var heldEntity)) + return; + + if (activeHeldEntity == heldEntity) + return; + + DoInteraction(activeHeldEntity, heldEntity); + } + + public void UseActiveHeldEntity() + { + if (!TryGetActiveHeldEntity(out var heldEntity)) + return; + + DoUse(heldEntity); + } + + public void ActivateHeldEntity(string handName) + { + if (!TryGetHeldEntity(handName, out var heldEntity)) + return; + + DoActivate(heldEntity); + } + + /// + /// Moves an entity from one hand to the active hand. + /// + public bool TryMoveHeldEntityToActiveHand(string handName, bool checkActionBlocker = true) + { + if (!TryGetHand(handName, out var hand) || !TryGetActiveHand(out var activeHand)) + return false; + + if (!TryGetHeldEntity(handName, out var heldEntity)) + return false; + + if (!CanInsertEntityIntoHand(activeHand, heldEntity) || !CanRemoveHeldEntityFromHand(hand)) + return false; + + if (checkActionBlocker && (!PlayerCanDrop() || !PlayerCanPickup())) + return false; + + RemoveHeldEntityFromHand(hand); + PutEntityIntoHand(activeHand, heldEntity); + return true; + } + + #endregion + + private void DeselectActiveHeldEntity() + { + if (TryGetActiveHeldEntity(out var entity)) + DoHandDeselectedInteraction(entity); + } + + private void SelectActiveHeldEntity() + { + if (TryGetActiveHeldEntity(out var entity)) + DoHandSelectedInteraction(entity); + } + + private void HandCountChanged() + { + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new HandCountChangedEvent(Owner)); + } + + /// + /// Tries to pick up an entity into the active hand. If it cannot, tries to pick up the entity into each other hand. + /// + public bool TryPutInActiveHandOrAny(IEntity entity, bool checkActionBlocker = true) + { + return TryPutInAnyHand(entity, GetActiveHand(), checkActionBlocker); + } + + /// + /// Tries to pick up an entity into the priority hand, if provided. If it cannot, tries to pick up the entity into each other hand. + /// + public bool TryPutInAnyHand(IEntity entity, string? priorityHandName = null, bool checkActionBlocker = true) + { + Hand? priorityHand = null; + + if (priorityHandName != null) + priorityHand = GetHand(priorityHandName); + + return TryPutInAnyHand(entity, priorityHand, checkActionBlocker); + } + + /// + /// Tries to pick up an entity into the priority hand, if provided. If it cannot, tries to pick up the entity into each other hand. + /// + private bool TryPutInAnyHand(IEntity entity, Hand? priorityHand = null, bool checkActionBlocker = true) + { + if (priorityHand != null) + { + if (TryPickupEntity(priorityHand, entity, checkActionBlocker)) + return true; + } + + foreach (var hand in Hands) + { + if (TryPickupEntity(hand, entity, checkActionBlocker)) + return true; + } + return false; + } + + protected virtual void OnHeldEntityRemovedFromHand(IEntity heldEntity, HandState handState) { } + + protected virtual void DoDroppedInteraction(IEntity heldEntity, bool intentionalDrop) { } + + protected virtual void DoEquippedHandInteraction(IEntity entity, HandState handState) { } + + protected virtual void DoHandSelectedInteraction(IEntity entity) { } + + protected virtual void DoHandDeselectedInteraction(IEntity entity) { } + + protected virtual void DoInteraction(IEntity activeHeldEntity, IEntity heldEntity) { } + + protected virtual void DoUse(IEntity heldEntity) { } + + protected virtual void DoActivate(IEntity heldEntity) { } + + protected virtual void HandlePickupAnimation(IEntity entity) { } + + protected void EnableHand(Hand hand) + { + hand.Enabled = true; + Dirty(); + } + + protected void DisableHand(Hand hand) + { + hand.Enabled = false; + DropHeldEntityToFloor(hand, intentionalDrop: false); + Dirty(); + } + } + + public interface IReadOnlyHand + { + public string Name { get; } + + public bool Enabled { get; } + + public HandLocation Location { get; } + + public abstract IEntity? HeldEntity { get; } + } + + public class Hand : IReadOnlyHand + { + [ViewVariables] + public string Name { get; set; } + + [ViewVariables] + public bool Enabled { get; set; } + + [ViewVariables] + public HandLocation Location { get; set; } + + /// + /// The container used to hold the contents of this hand. Nullable because the client must get the containers via , + /// which may not be synced with the server when the client hands are created. + /// + [ViewVariables] + public IContainer? Container { get; set; } + + [ViewVariables] + public IEntity? HeldEntity => Container?.ContainedEntities?.FirstOrDefault(); + + public Hand(string name, bool enabled, HandLocation location, IContainer? container = null) + { + Name = name; + Enabled = enabled; + Location = location; + Container = container; + } + + public HandState ToHandState() + { + return new(Name, Location, Enabled); + } } [Serializable, NetSerializable] - public sealed class SharedHand + public sealed class HandState { - public readonly int Index; - public readonly string Name; - public readonly EntityUid? EntityUid; - public readonly HandLocation Location; - public readonly bool Enabled; + public string Name { get; } + public HandLocation Location { get; } + public bool Enabled { get; } - public SharedHand(int index, string name, EntityUid? entityUid, HandLocation location, bool enabled) + public HandState(string name, HandLocation location, bool enabled) { - Index = index; Name = name; - EntityUid = entityUid; Location = location; Enabled = enabled; } } - // The IDs of the items get synced over the network. [Serializable, NetSerializable] public class HandsComponentState : ComponentState { - public readonly SharedHand[] Hands; - public readonly string? ActiveIndex; + public HandState[] Hands { get; } + public string? ActiveHand { get; } - public HandsComponentState(SharedHand[] hands, string? activeIndex) : base(ContentNetIDs.HANDS) + public HandsComponentState(HandState[] hands, string? activeHand = null) : base(ContentNetIDs.HANDS) { Hands = hands; - ActiveIndex = activeIndex; + ActiveHand = activeHand; } } @@ -62,66 +887,68 @@ namespace Content.Shared.Hands.Components } /// - /// A message that calls the activate interaction on the item in Index. + /// A message that calls the activate interaction on the item in the specified hand. /// [Serializable, NetSerializable] public class ActivateInHandMsg : ComponentMessage { - public string Index { get; } + public string HandName { get; } - public ActivateInHandMsg(string index) + public ActivateInHandMsg(string handName) { Directed = true; - Index = index; + HandName = handName; } } + /// + /// Uses the item in the active hand on the item in the specified hand. + /// [Serializable, NetSerializable] public class ClientAttackByInHandMsg : ComponentMessage { - public string Index { get; } + public string HandName { get; } - public ClientAttackByInHandMsg(string index) + public ClientAttackByInHandMsg(string handName) { Directed = true; - Index = index; + HandName = handName; } } + /// + /// Moves an item from one hand to the active hand. + /// + [Serializable, NetSerializable] + public class MoveItemFromHandMsg : ComponentMessage + { + public string HandName { get; } + + public MoveItemFromHandMsg(string handName) + { + Directed = true; + HandName = handName; + } + } + + /// + /// Sets the player's active hand to a specified hand. + /// [Serializable, NetSerializable] public class ClientChangedHandMsg : ComponentMessage { - public string Index { get; } + public string HandName { get; } - public ClientChangedHandMsg(string index) + public ClientChangedHandMsg(string handName) { Directed = true; - Index = index; - } - } - - [Serializable, NetSerializable] - public class HandEnabledMsg : ComponentMessage - { - public string Name { get; } - - public HandEnabledMsg(string name) - { - Name = name; - } - } - - [Serializable, NetSerializable] - public class HandDisabledMsg : ComponentMessage - { - public string Name { get; } - - public HandDisabledMsg(string name) - { - Name = name; + HandName = handName; } } + /// + /// What side of the body this hand is on. + /// public enum HandLocation : byte { Left, @@ -129,19 +956,29 @@ namespace Content.Shared.Hands.Components Right } - /// - /// Component message for displaying an animation of an entity flying towards the owner of a HandsComponent - /// - [Serializable, NetSerializable] - public class AnimatePickupEntityMessage : ComponentMessage + public class HandCountChangedEvent : EntityEventArgs { - public readonly EntityUid EntityId; - public readonly EntityCoordinates EntityPosition; - public AnimatePickupEntityMessage(EntityUid entity, EntityCoordinates entityPosition) + public HandCountChangedEvent(IEntity sender) + { + Sender = sender; + } + + public IEntity Sender { get; } + } + + [Serializable, NetSerializable] + public class PickupAnimationMessage : ComponentMessage + { + public EntityUid EntityUid { get; } + public EntityCoordinates InitialPosition { get; } + public Vector2 PickupDirection { get; } + + public PickupAnimationMessage(EntityUid entityUid, Vector2 pickupDirection, EntityCoordinates initialPosition) { Directed = true; - EntityId = entity; - EntityPosition = entityPosition; + EntityUid = entityUid; + PickupDirection = pickupDirection; + InitialPosition = initialPosition; } } } diff --git a/Content.Shared/Hands/IEquippedHand.cs b/Content.Shared/Hands/IEquippedHand.cs index 3c9fc0043d..79bffd4f84 100644 --- a/Content.Shared/Hands/IEquippedHand.cs +++ b/Content.Shared/Hands/IEquippedHand.cs @@ -23,12 +23,12 @@ namespace Content.Shared.Hands public class EquippedHandEventArgs : UserEventArgs { - public EquippedHandEventArgs(IEntity user, SharedHand hand) : base(user) + public EquippedHandEventArgs(IEntity user, HandState hand) : base(user) { Hand = hand; } - public SharedHand Hand { get; } + public HandState Hand { get; } } /// @@ -50,9 +50,9 @@ namespace Content.Shared.Hands /// /// Hand that the item was placed into. /// - public SharedHand Hand { get; } + public HandState Hand { get; } - public EquippedHandEvent(IEntity user, IEntity equipped, SharedHand hand) + public EquippedHandEvent(IEntity user, IEntity equipped, HandState hand) { User = user; Equipped = equipped; diff --git a/Content.Shared/Hands/IUnequippedHand.cs b/Content.Shared/Hands/IUnequippedHand.cs index 9e9bf36be2..ccc24cbb0f 100644 --- a/Content.Shared/Hands/IUnequippedHand.cs +++ b/Content.Shared/Hands/IUnequippedHand.cs @@ -22,12 +22,12 @@ namespace Content.Shared.Hands public class UnequippedHandEventArgs : UserEventArgs { - public UnequippedHandEventArgs(IEntity user, SharedHand hand) : base(user) + public UnequippedHandEventArgs(IEntity user, HandState hand) : base(user) { Hand = hand; } - public SharedHand Hand { get; } + public HandState Hand { get; } } /// @@ -49,9 +49,9 @@ namespace Content.Shared.Hands /// /// Hand that the item is removed from. /// - public SharedHand Hand { get; } + public HandState Hand { get; } - public UnequippedHandEvent(IEntity user, IEntity unequipped, SharedHand hand) + public UnequippedHandEvent(IEntity user, IEntity unequipped, HandState hand) { User = user; Unequipped = unequipped; diff --git a/Content.Shared/Hands/SharedHandsSystem.cs b/Content.Shared/Hands/SharedHandsSystem.cs new file mode 100644 index 0000000000..923b864887 --- /dev/null +++ b/Content.Shared/Hands/SharedHandsSystem.cs @@ -0,0 +1,79 @@ +using Content.Shared.Hands.Components; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Serialization; +using System; + +namespace Content.Shared.Hands +{ + public abstract class SharedHandsSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandleContainerModified); + SubscribeLocalEvent(HandleContainerModified); + + SubscribeLocalEvent(HandleSetHand); + SubscribeNetworkEvent(HandleSetHand); + + SubscribeLocalEvent(HandleDrop); + SubscribeNetworkEvent(HandleDrop); + } + + private void HandleSetHand(RequestSetHandEvent msg, EntitySessionEventArgs eventArgs) + { + var entity = eventArgs.SenderSession?.AttachedEntity; + + if (entity == null || !entity.TryGetComponent(out SharedHandsComponent? hands)) + return; + + hands.ActiveHand = msg.HandName; + } + + private void HandleDrop(RequestDropHeldEntityEvent msg, EntitySessionEventArgs eventArgs) + { + var entity = eventArgs.SenderSession?.AttachedEntity; + + if (entity == null || !entity.TryGetComponent(out SharedHandsComponent? hands)) + return; + + hands.TryDropHand(msg.HandName, msg.DropTarget); + } + + protected abstract void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args); + } + + [Serializable, NetSerializable] + public class RequestSetHandEvent : EntityEventArgs + { + /// + /// The hand to be swapped to. + /// + public string HandName { get; } + + public RequestSetHandEvent(string handName) + { + HandName = handName; + } + } + + [Serializable, NetSerializable] + public class RequestDropHeldEntityEvent : EntityEventArgs + { + /// + /// The hand to drop from. + /// + public string HandName { get; } + + public EntityCoordinates DropTarget { get; } + + public RequestDropHeldEntityEvent(string handName, EntityCoordinates dropTarget) + { + HandName = handName; + DropTarget = dropTarget; + } + } +} diff --git a/Content.Shared/Item/SharedItemComponent.cs b/Content.Shared/Item/SharedItemComponent.cs index 8911285274..b8af477798 100644 --- a/Content.Shared/Item/SharedItemComponent.cs +++ b/Content.Shared/Item/SharedItemComponent.cs @@ -1,6 +1,7 @@ #nullable enable using System; using Content.Shared.ActionBlocker; +using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Interaction.Helpers; using Content.Shared.Inventory; @@ -136,14 +137,22 @@ namespace Content.Shared.Item bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) { - return TryPutInHand(eventArgs.User); - } + var user = eventArgs.User; - /// - /// Tries to put this item in a player's hands. - /// TODO: Move server implementation here once hands are in shared. - /// - public abstract bool TryPutInHand(IEntity user); + if (!CanPickup(user)) + return false; + + if (!user.TryGetComponent(out SharedHandsComponent? hands)) + return false; + + var activeHand = hands.ActiveHand; + + if (activeHand == null) + return false; + + hands.TryPickupEntityToActiveHand(Owner); + return true; + } protected virtual void OnEquippedPrefixChange() { } diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index b3ab345ca7..8c7b2f7b00 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -207,6 +207,7 @@ fireStackAlternateState: 3 - type: CreamPiedVisualizer state: creampie_human + - type: HandsVisualizer - type: CombatMode - type: Climbing - type: Cuffable