using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Client.Examine; using Content.Client.Strip; using Content.Client.Verbs.UI; using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Inventory.VirtualItem; using Content.Shared.Item; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Player; using Robust.Shared.Timing; namespace Content.Client.Hands.Systems { [UsedImplicitly] public sealed class HandsSystem : SharedHandsSystem { [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IUserInterfaceManager _ui = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly StrippableSystem _stripSys = default!; [Dependency] private readonly ExamineSystem _examine = default!; public event Action? OnPlayerAddHand; public event Action? OnPlayerRemoveHand; public event Action? OnPlayerSetActiveHand; public event Action? OnPlayerHandsAdded; public event Action? OnPlayerHandsRemoved; public event Action? OnPlayerItemAdded; public event Action? OnPlayerItemRemoved; public event Action? OnPlayerHandBlocked; public event Action? OnPlayerHandUnblocked; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(HandlePlayerAttached); SubscribeLocalEvent(HandlePlayerDetached); SubscribeLocalEvent(OnHandsStartup); SubscribeLocalEvent(OnHandsShutdown); SubscribeLocalEvent(HandleComponentState); SubscribeLocalEvent(OnVisualsChanged); OnHandSetActive += OnHandActivated; } #region StateHandling private void HandleComponentState(EntityUid uid, HandsComponent component, ref ComponentHandleState args) { if (args.Current is not HandsComponentState state) return; var handsModified = component.Hands.Count != state.Hands.Count; // we need to check that, even if we have the same amount, that the individual hands didn't change. if (!handsModified) { foreach (var hand in component.Hands.Values) { if (state.Hands.Contains(hand)) continue; handsModified = true; break; } } var manager = EnsureComp(uid); if (handsModified) { List addedHands = new(); foreach (var hand in state.Hands) { if (component.Hands.ContainsKey(hand.Name)) continue; var container = _containerSystem.EnsureContainer(uid, hand.Name, manager); var newHand = new Hand(hand.Name, hand.Location, container); component.Hands.Add(hand.Name, newHand); addedHands.Add(newHand); } foreach (var name in component.Hands.Keys) { if (!state.HandNames.Contains(name)) { RemoveHand(uid, name, component); } } component.SortedHands.Clear(); component.SortedHands.AddRange(state.HandNames); var sorted = addedHands.OrderBy(hand => component.SortedHands.IndexOf(hand.Name)); foreach (var hand in sorted) { AddHand(uid, hand, component); } } _stripSys.UpdateUi(uid); if (component.ActiveHand == null && state.ActiveHand == null) return; //edge case if (component.ActiveHand != null && state.ActiveHand != component.ActiveHand.Name) { SetActiveHand(uid, component.Hands[state.ActiveHand!], component); } } #endregion public void ReloadHandButtons() { if (!TryGetPlayerHands(out var hands)) { return; } OnPlayerHandsAdded?.Invoke(hands); } public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? hands = null) { base.DoDrop(uid, hand, doDropInteraction, hands); if (TryComp(hand.HeldEntity, out SpriteComponent? sprite)) sprite.RenderOrder = EntityManager.CurrentTick.Value; } public EntityUid? GetActiveHandEntity() { return TryGetPlayerHands(out var hands) ? hands.ActiveHandEntity : null; } /// /// Get the hands component of the local player /// public bool TryGetPlayerHands([NotNullWhen(true)] out HandsComponent? hands) { var player = _playerManager.LocalEntity; hands = null; return player != null && TryComp(player.Value, out hands); } /// /// Called when a user clicked on their hands GUI /// public void UIHandClick(HandsComponent hands, string handName) { if (!hands.Hands.TryGetValue(handName, out var pressedHand)) return; if (hands.ActiveHand == null) return; var pressedEntity = pressedHand.HeldEntity; var activeEntity = hands.ActiveHand.HeldEntity; if (pressedHand == hands.ActiveHand && activeEntity != null) { // use item in hand // it will always be attack_self() in my heart. EntityManager.RaisePredictiveEvent(new RequestUseInHandEvent()); return; } if (pressedHand != hands.ActiveHand && pressedEntity == null) { // change active hand EntityManager.RaisePredictiveEvent(new RequestSetHandEvent(handName)); return; } if (pressedHand != hands.ActiveHand && pressedEntity != null && activeEntity != null) { // use active item on held item EntityManager.RaisePredictiveEvent(new RequestHandInteractUsingEvent(pressedHand.Name)); return; } if (pressedHand != hands.ActiveHand && pressedEntity != null && activeEntity == null) { // move the item to the active hand EntityManager.RaisePredictiveEvent(new RequestMoveHandItemEvent(pressedHand.Name)); } } /// /// Called when a user clicks on the little "activation" icon in the hands GUI. This is currently only used /// by storage (backpacks, etc). /// public void UIHandActivate(string handName) { EntityManager.RaisePredictiveEvent(new RequestActivateInHandEvent(handName)); } public void UIInventoryExamine(string handName) { if (!TryGetPlayerHands(out var hands) || !hands.Hands.TryGetValue(handName, out var hand) || hand.HeldEntity is not { Valid: true } entity) { return; } _examine.DoExamine(entity); } /// /// Called when a user clicks on the little "activation" icon in the hands GUI. This is currently only used /// by storage (backpacks, etc). /// public void UIHandOpenContextMenu(string handName) { if (!TryGetPlayerHands(out var hands) || !hands.Hands.TryGetValue(handName, out var hand) || hand.HeldEntity is not { Valid: true } entity) { return; } _ui.GetUIController().OpenVerbMenu(entity); } public void UIHandAltActivateItem(string handName) { RaisePredictiveEvent(new RequestHandAltInteractEvent(handName)); } #region visuals protected override void HandleEntityInserted(EntityUid uid, HandsComponent hands, EntInsertedIntoContainerMessage args) { base.HandleEntityInserted(uid, hands, args); if (!hands.Hands.TryGetValue(args.Container.ID, out var hand)) return; UpdateHandVisuals(uid, args.Entity, hand); _stripSys.UpdateUi(uid); if (uid != _playerManager.LocalEntity) return; OnPlayerItemAdded?.Invoke(hand.Name, args.Entity); if (HasComp(args.Entity)) OnPlayerHandBlocked?.Invoke(hand.Name); } protected override void HandleEntityRemoved(EntityUid uid, HandsComponent hands, EntRemovedFromContainerMessage args) { base.HandleEntityRemoved(uid, hands, args); if (!hands.Hands.TryGetValue(args.Container.ID, out var hand)) return; UpdateHandVisuals(uid, args.Entity, hand); _stripSys.UpdateUi(uid); if (uid != _playerManager.LocalEntity) return; OnPlayerItemRemoved?.Invoke(hand.Name, args.Entity); if (HasComp(args.Entity)) OnPlayerHandUnblocked?.Invoke(hand.Name); } /// /// Update the players sprite with new in-hand visuals. /// private void UpdateHandVisuals(EntityUid uid, EntityUid held, Hand hand, HandsComponent? handComp = null, SpriteComponent? sprite = null) { if (!Resolve(uid, ref handComp, ref sprite, false)) return; // visual update might involve changes to the entity's effective sprite -> need to update hands GUI. if (uid == _playerManager.LocalEntity) OnPlayerItemAdded?.Invoke(hand.Name, held); if (!handComp.ShowInHands) return; // Remove old layers. We could also just set them to invisible, but as items may add arbitrary layers, this // may eventually bloat the player with lots of layers. if (handComp.RevealedLayers.TryGetValue(hand.Location, out var revealedLayers)) { foreach (var key in revealedLayers) { sprite.RemoveLayer(key); } revealedLayers.Clear(); } else { revealedLayers = new(); handComp.RevealedLayers[hand.Location] = revealedLayers; } if (hand.HeldEntity == null) { // the held item was removed. RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true); return; } var ev = new GetInhandVisualsEvent(uid, hand.Location); RaiseLocalEvent(held, ev); if (ev.Layers.Count == 0) { RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true); return; } // add the new layers foreach (var (key, layerData) in ev.Layers) { if (!revealedLayers.Add(key)) { Log.Warning($"Duplicate key for in-hand visuals: {key}. Are multiple components attempting to modify the same layer? Entity: {ToPrettyString(held)}"); continue; } var index = sprite.LayerMapReserveBlank(key); // In case no RSI is given, use the item's base RSI as a default. This cuts down on a lot of unnecessary yaml entries. if (layerData.RsiPath == null && layerData.TexturePath == null && sprite[index].Rsi == null) { if (TryComp(held, out var itemComponent) && itemComponent.RsiPath != null) sprite.LayerSetRSI(index, itemComponent.RsiPath); else if (TryComp(held, out SpriteComponent? clothingSprite)) sprite.LayerSetRSI(index, clothingSprite.BaseRSI); } sprite.LayerSetData(index, layerData); } RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true); } private void OnVisualsChanged(EntityUid uid, HandsComponent component, VisualsChangedEvent args) { // update hands visuals if this item is in a hand (rather then inventory or other container). if (component.Hands.TryGetValue(args.ContainerId, out var hand)) { UpdateHandVisuals(uid, GetEntity(args.Item), hand, component); } } #endregion #region Gui private void HandlePlayerAttached(EntityUid uid, HandsComponent component, LocalPlayerAttachedEvent args) { OnPlayerHandsAdded?.Invoke(component); } private void HandlePlayerDetached(EntityUid uid, HandsComponent component, LocalPlayerDetachedEvent args) { OnPlayerHandsRemoved?.Invoke(); } private void OnHandsStartup(EntityUid uid, HandsComponent component, ComponentStartup args) { if (_playerManager.LocalEntity == uid) OnPlayerHandsAdded?.Invoke(component); } private void OnHandsShutdown(EntityUid uid, HandsComponent component, ComponentShutdown args) { if (_playerManager.LocalEntity == uid) OnPlayerHandsRemoved?.Invoke(); } #endregion private void AddHand(EntityUid uid, Hand newHand, HandsComponent? handsComp = null) { AddHand(uid, newHand.Name, newHand.Location, handsComp); } public override void AddHand(EntityUid uid, string handName, HandLocation handLocation, HandsComponent? handsComp = null) { base.AddHand(uid, handName, handLocation, handsComp); if (uid == _playerManager.LocalEntity) OnPlayerAddHand?.Invoke(handName, handLocation); if (handsComp == null) return; if (handsComp.ActiveHand == null) SetActiveHand(uid, handsComp.Hands[handName], handsComp); } public override void RemoveHand(EntityUid uid, string handName, HandsComponent? handsComp = null) { if (uid == _playerManager.LocalEntity && handsComp != null && handsComp.Hands.ContainsKey(handName) && uid == _playerManager.LocalEntity) { OnPlayerRemoveHand?.Invoke(handName); } base.RemoveHand(uid, handName, handsComp); } private void OnHandActivated(Entity? ent) { if (ent is not { } hand) return; if (_playerManager.LocalEntity != hand.Owner) return; if (hand.Comp.ActiveHand == null) { OnPlayerSetActiveHand?.Invoke(null); return; } OnPlayerSetActiveHand?.Invoke(hand.Comp.ActiveHand.Name); } } }