Files
tbd-station-14/Content.Server/GameObjects/Components/GUI/HandsComponent.cs
Vera Aguilera Puerto d81a5faac4 Adds disarm action (#2950)
* Adds disarming

* Disarm acts

* yaml

* much better icon for disarm

* Apply Remie's suggestions, improve code!
2021-01-09 20:31:34 +01:00

828 lines
25 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Pulling;
using Content.Server.GameObjects.EntitySystems.Click;
using Content.Server.Interfaces.GameObjects;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Server.Utility;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Pulling;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces;
using Content.Shared.Physics.Pull;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.EntitySystemMessages;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.Components.GUI
{
[RegisterComponent]
[ComponentReference(typeof(IHandsComponent))]
[ComponentReference(typeof(ISharedHandsComponent))]
[ComponentReference(typeof(SharedHandsComponent))]
public class HandsComponent : SharedHandsComponent, IHandsComponent, IBodyPartAdded, IBodyPartRemoved, IDisarmedAct
{
[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<Hand> _hands = new();
public IEnumerable<string> 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<ItemComponent> GetAllHeldItems()
{
foreach (var hand in _hands)
{
if (hand.Entity != null)
{
yield return hand.Entity.GetComponent<ItemComponent>();
}
}
}
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<ItemComponent>();
}
public bool TryGetItem(string handName, [NotNullWhen(true)] out ItemComponent? item)
{
return (item = GetItem(handName)) != null;
}
public ItemComponent? GetActiveHand => ActiveHand == null
? null
: GetItem(ActiveHand);
/// <summary>
/// Enumerates over the enabled hand keys,
/// returning the active hand first.
/// </summary>
public IEnumerable<string> 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 success = hand.Container.Insert(item.Owner);
if (success)
{
item.Owner.Transform.LocalPosition = Vector2.Zero;
OnItemChanged?.Invoke();
}
_entitySystemManager.GetEntitySystem<InteractionSystem>().EquippedHandInteraction(Owner, item.Owner,
ToSharedHand(hand));
_entitySystemManager.GetEntitySystem<InteractionSystem>().HandSelectedInteraction(Owner, item.Owner);
return success;
}
public void PutInHandOrDrop(ItemComponent item, bool mobCheck = true)
{
if (!PutInHand(item, mobCheck))
{
item.Owner.Transform.Coordinates = Owner.Transform.Coordinates;
}
}
public bool CanPutInHand(ItemComponent item, bool mobCheck = true)
{
if (mobCheck && !ActionBlockerSystem.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 && !ActionBlockerSystem.CanPickup(Owner))
return false;
var hand = GetHand(index);
return hand != null &&
hand.Enabled &&
hand.Container.CanInsert(item.Owner) == true;
}
/// <summary>
/// Calls the Dropped Interaction with the item.
/// </summary>
/// <param name="item">The itemcomponent of the item to be dropped</param>
/// <param name="doMobChecks">Check if the item can be dropped</param>
/// <returns>True if IDropped.Dropped was called, otherwise false</returns>
private bool DroppedInteraction(ItemComponent item, bool doMobChecks)
{
var interactionSystem = _entitySystemManager.GetEntitySystem<InteractionSystem>();
if (doMobChecks)
{
if (!interactionSystem.TryDroppedInteraction(Owner, item.Owner))
return false;
}
else
{
interactionSystem.DroppedInteraction(Owner, item.Owner);
}
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)
{
var hand = GetHand(slot);
if (!CanDrop(slot, doMobChecks) || hand?.Entity == null)
{
return false;
}
var item = hand.Entity.GetComponent<ItemComponent>();
if (!hand.Container.Remove(hand.Entity))
{
return false;
}
_entitySystemManager.GetEntitySystem<InteractionSystem>().UnequippedHandInteraction(Owner, item.Owner,
ToSharedHand(hand));
if (doDropInteraction && !DroppedInteraction(item, false))
return false;
item.RemovedFromSlot();
item.Owner.Transform.Coordinates = coords;
if (item.Owner.TryGetComponent<SpriteComponent>(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)
{
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<ItemComponent>();
if (!hand.Container.Remove(hand.Entity))
{
throw new InvalidOperationException();
}
_entitySystemManager.GetEntitySystem<InteractionSystem>().UnequippedHandInteraction(Owner, item.Owner,
ToSharedHand(hand));
if (doDropInteraction && !DroppedInteraction(item, doMobChecks))
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)
{
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);
}
public bool Drop(string slot, bool mobChecks = true, bool doDropInteraction = true)
{
return Drop(slot, Owner.Transform.Coordinates, mobChecks, doDropInteraction);
}
public bool Drop(IEntity entity, bool mobChecks = true, bool doDropInteraction = 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);
}
public bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = 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);
}
/// <summary>
/// Checks whether an item can be dropped from the specified slot.
/// </summary>
/// <param name="name">The slot to check for.</param>
/// <returns>
/// True if there is an item in the slot and it can be dropped, false otherwise.
/// </returns>
public bool CanDrop(string name, bool mobCheck = true)
{
var hand = GetHand(name);
if (mobCheck && !ActionBlockerSystem.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 = ContainerManagerComponent.Create<ContainerSlot>($"hand {_nextHand++}", Owner);
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()
{
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>();
interactionSystem.TryUseInteraction(Owner, used);
}
}
public bool ThrowItem()
{
var item = GetActiveHand?.Owner;
if (item != null)
{
var interactionSystem = _entitySystemManager.GetEntitySystem<InteractionSystem>();
return interactionSystem.TryThrowInteraction(Owner, item);
}
return false;
}
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;
}
break;
case PullStartedMessage _:
var firstFreeHand = _hands.FirstOrDefault(hand => hand.Enabled);
if (firstFreeHand == null)
{
break;
}
firstFreeHand.Enabled = false;
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);
break;
}
}
public override async 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;
}
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<InteractionSystem>();
if (used != null)
{
await interactionSystem.Interaction(Owner, used, hand.Entity,
EntityCoordinates.Invalid);
}
else
{
var entity = hand.Entity;
if (!Drop(entity))
{
break;
}
interactionSystem.Interaction(Owner, entity);
}
}
break;
}
case UseInHandMsg _:
{
var playerEntity = session.AttachedEntity;
var used = GetActiveHand?.Owner;
if (playerEntity == Owner && used != null)
{
var interactionSystem = _entitySystemManager.GetEntitySystem<InteractionSystem>();
interactionSystem.TryUseInteraction(Owner, used);
}
break;
}
case ActivateInHandMsg msg:
{
var playerEntity = session.AttachedEntity;
var used = GetItem(msg.Index)?.Owner;
if (playerEntity == Owner && used != null)
{
var interactionSystem = _entitySystemManager.GetEntitySystem<InteractionSystem>();
interactionSystem.TryInteractionActivate(Owner, used);
}
break;
}
}
}
public void HandleSlotModifiedMaybe(ContainerModifiedMessage message)
{
foreach (var hand in _hands)
{
if (hand.Container != message.Container)
{
continue;
}
Dirty();
if (!message.Entity.TryGetComponent(out IPhysicsComponent? physics))
{
return;
}
// set velocity to zero
physics.Stop();
return;
}
}
void IBodyPartAdded.BodyPartAdded(BodyPartAddedEventArgs args)
{
if (args.Part.PartType != BodyPartType.Hand)
{
return;
}
AddHand(args.Slot);
}
void IBodyPartRemoved.BodyPartRemoved(BodyPartRemovedEventArgs args)
{
if (args.Part.PartType != BodyPartType.Hand)
{
return;
}
RemoveHand(args.Slot);
}
bool IDisarmedAct.Disarmed(DisarmedActEventArgs eventArgs)
{
if (BreakPulls())
return false;
var source = eventArgs.Source;
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Effects/thudswoosh.ogg", source,
AudioHelpers.WithVariation(0.025f));
if (ActiveHand != null && Drop(ActiveHand, false))
{
source.PopupMessageOtherClients(Loc.GetString("{0} disarms {1}!", source.Name, eventArgs.Target.Name));
source.PopupMessageCursor(Loc.GetString("You disarm {0}!", eventArgs.Target.Name));
}
else
{
source.PopupMessageOtherClients(Loc.GetString("{0} shoves {1}!", source.Name, eventArgs.Target.Name));
source.PopupMessageCursor(Loc.GetString("You shove {0}!", eventArgs.Target.Name));
}
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??
if (!Owner.TryGetComponent(out SharedPullerComponent? puller)
|| puller.Pulling == null || !puller.Pulling.TryGetComponent(out PullableComponent? pullable))
return false;
return pullable.TryStopPull();
}
}
public class Hand : IDisposable
{
private bool _enabled = true;
public Hand(HandsComponent parent, string name, ContainerSlot container)
{
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 (_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);
}
}
public void Dispose()
{
Container.Shutdown(); // TODO verify this
}
public SharedHand ToShared(int index, HandLocation location)
{
return new(index, Name, Entity?.Uid, location, Enabled);
}
}
public class HandCountChangedEvent : EntitySystemMessage
{
public HandCountChangedEvent(IEntity sender)
{
Sender = sender;
}
public IEntity Sender { get; }
}
}