ECS verbs and update context menu (#4594)

* Functioning ECS verbs

Currently only ID card console works.

* Changed verb types and allow ID card insertions

* Verb GUI sorting and verb networking

* More networking, and shared components

* Clientside verbs work now.

* Verb enums changed to bitmask flags

* Verb Categories redo

* Fix range check

* GasTank Verb

* Remove unnecessary bodypart verb

* Buckle Verb

* buckle & unbuckle verbs

* Updated range checks

* Item cabinet verbs

* Add range user override

* construction verb

* Chemistry machine verbs

* Climb Verb

* Generalise pulled entity verbs

* ViewVariables Verb

* rejuvenate, delete, sentient, control verbs

* Outfit verb

* inrangeunoccluded and tubedirection verbs

* attach-to verbs

* remove unused verbs and move VV

* Rename DebugVerbSystem

* Ghost role and pointing verbs

* Remove global verbs

* Allow verbs to raise events

* Changing categories and simplifying debug verbs

* Add rotate and flip verbs

* fix rejuvenate test

* redo context menu

* new Add Gas debug verb

* Add Set Temperature debug verb

* Uncuff verb

* Disposal unit verbs

* Add pickup verb

* lock/unlock verb

* Remove verb type, add specific verb events

* rename verb messages -> events

* Context menu displays verbs by interaction type

* Updated context menu HandleMove

previously, checked if entities moved 1 tile from click location.

Now checks if entities moved out of view.

Now you can actually right-click interact with yourself while walking!

* Misc Verb menu GUI changes

* Fix non-human/ghost verbs

* Update types and categories

* Allow non-ghost/human to open context menu

* configuration verb

* tagger verb

* Morgue Verbs

* Medical Scanner Verbs

* Fix solution refactor merge issues

* Fix context menu in-view check

* Remove prepare GUI

* Redo verb restrictions

* Fix context menu UI

* Disposal Verbs

* Spill verb

* Light verb

* Hand Held light verb

* power cell verbs

* storage verbs

and adding names to insert/eject

* Pulling verb

* Close context menu on verb execution

* Strip verb

* AmmoBox verb

* fix pull verb

* gun barrel verbs

revolver verb
energy weapon verbs
Bolt action verb

* Magazine gun barrel  verbs

* Add charger verbs

* PDA verbs

* Transfer amount verb

* Add reagent verb

* make alt-click use ECS verbs

* Delete old verb files

* Magboot verb

* finalising tweaks

* context menu visibility changes

* code cleanup

* Update AdminAddReagentUI.cs

* Remove HasFlag

* Consistent verb keys

* Remove Linq, add comment

* Fix in-inventory check

* Update GUI text alignment and padding

* Added close-menu option

* Changed some "interaction" verbs to "activation"

* Remove verb keys, use sorted sets

* fix master merge

* update some verb text

* Undo Changes

Remove some new verbs that can be added later

undid some .ftl bugfixes, can and should be done separately

* fix merge

* Undo file rename

* fix merge

* Misc Cleanup

* remove contraction

* Fix keybinding issue

* fix comment

* merge fix

* fix merge

* fix merge

* fix merge

* fix merge

* fix open-close verbs

* adjust uncuff verb

* fix merge

and undo the renaming of SharedPullableComponent to PullableComponent. I'm tired of all of those merge conflicts
This commit is contained in:
Leon Friedrich
2021-10-05 14:29:03 +11:00
committed by GitHub
parent 1095c8fc08
commit 6cb58e608b
175 changed files with 3391 additions and 4305 deletions

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Eui;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Chemistry.Components;

View File

@@ -1,10 +1,12 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
@@ -12,6 +14,8 @@ namespace Content.Shared.Containers.ItemSlots
{
public class SharedItemSlotsSystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
public override void Initialize()
{
base.Initialize();
@@ -19,6 +23,9 @@ namespace Content.Shared.Containers.ItemSlots
SubscribeLocalEvent<SharedItemSlotsComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SharedItemSlotsComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SharedItemSlotsComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<SharedItemSlotsComponent, GetAlternativeVerbsEvent>(AddEjectVerbs);
SubscribeLocalEvent<SharedItemSlotsComponent, GetInteractionVerbsEvent>(AddInsertVerbs);
}
private void OnComponentInit(EntityUid uid, SharedItemSlotsComponent itemSlots, ComponentInit args)
@@ -55,6 +62,53 @@ namespace Content.Shared.Containers.ItemSlots
}
}
private void AddEjectVerbs(EntityUid uid, SharedItemSlotsComponent component, GetAlternativeVerbsEvent args)
{
if (args.Hands == null ||
!args.CanAccess ||
!args.CanInteract ||
!_actionBlockerSystem.CanPickup(args.User))
return;
foreach (var (slotName, slot) in component.Slots)
{
if (slot.ContainerSlot.ContainedEntity == null)
continue;
Verb verb = new();
// TODO ITEMSLOTS give item slot names localization strings?
// Basically: its much nicer to have "insert ID" instead of the much longer "Eject <full-in-game-username>'s ID card (assistant)"
verb.Text = slot.ContainerSlot.ContainedEntity.Name;
verb.Category = VerbCategory.Eject;
verb.Act = () => TryEjectContent(component, slotName, args.User);
args.Verbs.Add(verb);
}
}
private void AddInsertVerbs(EntityUid uid, SharedItemSlotsComponent component, GetInteractionVerbsEvent args)
{
if (args.Using == null ||
!args.CanAccess ||
!args.CanInteract ||
!_actionBlockerSystem.CanDrop(args.User))
return;
foreach (var (slotName, slot) in component.Slots)
{
if (!CanInsertContent(args.Using, slot))
continue;
Verb verb = new();
// TODO ITEMSLOTS give item slot names localization strings?
// Basically: its much nicer to have "insert ID" instead of the much longer "Insert <full-in-game-username>'s ID card (assistant)"
verb.Text = args.Using.Name;
verb.Category = VerbCategory.Insert;
verb.Act = () => InsertContent(component, slot, slotName, args.Using);
args.Verbs.Add(verb);
}
}
private void OnInteractUsing(EntityUid uid, SharedItemSlotsComponent itemSlots, InteractUsingEvent args)
{
if (args.Handled)
@@ -64,30 +118,27 @@ namespace Content.Shared.Containers.ItemSlots
}
/// <summary>
/// Tries to insert item in any fitting item slot from users hand
/// Tries to insert or swap an item in any fitting item slot from users hand. If a valid slot already contains an item, it will swap it out.
/// </summary>
/// <returns>False if failed to insert item</returns>
public bool TryInsertContent(SharedItemSlotsComponent itemSlots, IEntity item, IEntity user)
public bool TryInsertContent(SharedItemSlotsComponent itemSlots, IEntity item, IEntity user, SharedHandsComponent? hands = null)
{
foreach (var pair in itemSlots.Slots)
if (!Resolve(user.Uid, ref hands))
{
var slotName = pair.Key;
var slot = pair.Value;
itemSlots.Owner.PopupMessage(user, Loc.GetString("item-slots-try-insert-no-hands"));
return false;
}
foreach (var (slotName, slot) in itemSlots.Slots)
{
// check if item allowed in whitelist
if (slot.Whitelist != null && !slot.Whitelist.IsValid(item))
continue;
// check if slot is empty
// check if slot does not contain the item currently being inserted???
if (slot.ContainerSlot.Contains(item))
continue;
if (!user.TryGetComponent(out SharedHandsComponent? hands))
{
itemSlots.Owner.PopupMessage(user, Loc.GetString("item-slots-try-insert-no-hands"));
return true;
}
// get item inside container
IEntity? swap = null;
if (slot.ContainerSlot.ContainedEntity != null)
@@ -101,13 +152,7 @@ namespace Content.Shared.Containers.ItemSlots
if (swap != null)
hands.TryPutInAnyHand(swap);
// insert item
slot.ContainerSlot.Insert(item);
RaiseLocalEvent(itemSlots.Owner.Uid, new ItemSlotChanged(itemSlots, slotName, slot));
// play sound
if (slot.InsertSound != null)
SoundSystem.Play(Filter.Pvs(itemSlots.Owner), slot.InsertSound.GetSound(), itemSlots.Owner);
InsertContent(itemSlots, slot, slotName, item);
return true;
}
@@ -115,6 +160,32 @@ namespace Content.Shared.Containers.ItemSlots
return false;
}
public void InsertContent(SharedItemSlotsComponent itemSlots, ItemSlot slot, string slotName, IEntity item)
{
// insert item
slot.ContainerSlot.Insert(item);
RaiseLocalEvent(itemSlots.Owner.Uid, new ItemSlotChanged(itemSlots, slotName, slot));
// play sound
if (slot.InsertSound != null)
SoundSystem.Play(Filter.Pvs(itemSlots.Owner), slot.InsertSound.GetSound(), itemSlots.Owner);
}
/// <summary>
/// Can a given item be inserted into a slot, without ejecting the current item in that slot.
/// </summary>
public bool CanInsertContent(IEntity item, ItemSlot slot)
{
if (slot.ContainerSlot.ContainedEntity != null)
return false;
// check if item allowed in whitelist
if (slot.Whitelist != null && !slot.Whitelist.IsValid(item))
return false;
return true;
}
/// <summary>
/// Tries to insert item in known slot. Doesn't interact with user
/// </summary>
@@ -124,15 +195,10 @@ namespace Content.Shared.Containers.ItemSlots
if (!itemSlots.Slots.TryGetValue(slotName, out var slot))
return false;
if (slot.ContainerSlot.ContainedEntity != null)
if (!CanInsertContent(item, slot))
return false;
// check if item allowed in whitelist
if (slot.Whitelist != null && !slot.Whitelist.IsValid(item))
return false;
slot.ContainerSlot.Insert(item);
RaiseLocalEvent(itemSlots.Owner.Uid, new ItemSlotChanged(itemSlots, slotName, slot));
InsertContent(itemSlots, slot, slotName, item);
return true;
}

View File

@@ -109,7 +109,7 @@ namespace Content.Shared.Item
/// <summary>
/// If a player can pick up this item.
/// </summary>
public bool CanPickup(IEntity user)
public bool CanPickup(IEntity user, bool popup = true)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanPickup(user))
return false;
@@ -120,7 +120,7 @@ namespace Content.Shared.Item
if (!Owner.TryGetComponent(out IPhysBody? physics) || physics.BodyType == BodyType.Static)
return false;
return user.InRangeUnobstructed(Owner, ignoreInsideBlocker: true, popup: true);
return user.InRangeUnobstructed(Owner, ignoreInsideBlocker: true, popup: popup);
}
void IEquipped.Equipped(EquippedEventArgs eventArgs)

View File

@@ -34,7 +34,7 @@ namespace Content.Shared.Light.Component
public sealed override string Name => "ExpendableLight";
[ViewVariables(VVAccess.ReadOnly)]
protected ExpendableLightState CurrentState { get; set; }
public ExpendableLightState CurrentState { get; set; }
[ViewVariables]
[DataField("turnOnBehaviourID")]

View File

@@ -77,10 +77,14 @@ namespace Content.Shared.MedicalScanner
}
}
public bool CanInsert(IEntity entity)
{
return entity.HasComponent<SharedBodyComponent>();
}
bool IDragDropOn.CanDragDropOn(DragDropEvent eventArgs)
{
return eventArgs.Dragged.HasComponent<SharedBodyComponent>();
return CanInsert(eventArgs.Dragged);
}
public abstract bool DragDropOn(DragDropEvent eventArgs);

View File

@@ -16,7 +16,8 @@ namespace Content.Shared.Pulling.Components
// Before you try to add another type than SharedPullingStateManagementSystem, consider the can of worms you may be opening!
[NetworkedComponent()]
[Friend(typeof(SharedPullingStateManagementSystem))]
public abstract class SharedPullableComponent : Component
[RegisterComponent]
public class SharedPullableComponent : Component
{
public override string Name => "Pullable";

View File

@@ -1,4 +1,4 @@
using Content.Shared.Alert;
using Content.Shared.Alert;
using Content.Shared.Hands;
using Content.Shared.Movement.Components;
using Content.Shared.Physics.Pull;

View File

@@ -17,6 +17,8 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Players;
using Robust.Shared.IoC;
using Content.Shared.Verbs;
using Robust.Shared.Localization;
namespace Content.Shared.Pulling
{
@@ -63,11 +65,39 @@ namespace Content.Shared.Pulling
SubscribeLocalEvent<SharedPullableComponent, PullStartedMessage>(PullableHandlePullStarted);
SubscribeLocalEvent<SharedPullableComponent, PullStoppedMessage>(PullableHandlePullStopped);
SubscribeLocalEvent<SharedPullableComponent, GetOtherVerbsEvent>(AddPullVerbs);
CommandBinds.Builder
.Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(HandleMovePulledObject))
.Register<SharedPullingSystem>();
}
private void AddPullVerbs(EntityUid uid, SharedPullableComponent component, GetOtherVerbsEvent args)
{
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
return;
// Are they trying to pull themselves up by their bootstraps?
if (args.User == args.Target)
return;
//TODO VERB ICONS add pulling icon
if (component.Puller == args.User)
{
Verb verb = new();
verb.Text = Loc.GetString("pulling-verb-get-data-text-stop-pulling");
verb.Act = () => TryStopPull(component, args.User);
args.Verbs.Add(verb);
}
else if (CanPull(args.User, args.Target))
{
Verb verb = new();
verb.Text = Loc.GetString("pulling-verb-get-data-text");
verb.Act = () => TryStartPull(args.User, args.Target);
args.Verbs.Add(verb);
}
}
// Raise a "you are being pulled" alert if the pulled entity has alerts.
private static void PullableHandlePullStarted(EntityUid uid, SharedPullableComponent component, PullStartedMessage args)
{
@@ -227,7 +257,7 @@ namespace Content.Shared.Pulling
private void UpdatePulledRotation(IEntity puller, IEntity pulled)
{
// TODO: update once ComponentReference works with directed event bus.
if (!pulled.TryGetComponent(out SharedRotatableComponent? rotatable))
if (!pulled.TryGetComponent(out RotatableComponent? rotatable))
return;
if (!rotatable.RotateWhilePulling)

View File

@@ -4,7 +4,8 @@ using Robust.Shared.ViewVariables;
namespace Content.Shared.Rotatable
{
public abstract class SharedRotatableComponent : Component
[RegisterComponent]
public class RotatableComponent : Component
{
public override string Name => "Rotatable";

View File

@@ -1,54 +0,0 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Shared.Verbs
{
/// <summary>
/// A verb is an action in the right click menu of an entity.
/// Global verbs are visible on all entities, regardless of their components.
/// </summary>
/// <remarks>
/// To add a global verb to all entities,
/// define it and mark it with <see cref="GlobalVerbAttribute"/>
/// </remarks>
public abstract class GlobalVerb : VerbBase
{
/// <summary>
/// Gets the visible verb data for the user.
/// </summary>
/// <remarks>
/// Implementations should write into <paramref name="data"/> to return their data.
/// </remarks>
/// <param name="user">The entity of the user opening this menu.</param>
/// <param name="target">The entity this verb is being evaluated for.</param>
/// <param name="data">The data that must be filled in.</param>
/// <returns>The text string that is shown in the right click menu for this verb.</returns>
public abstract void GetData(IEntity user, IEntity target, VerbData data);
/// <summary>
/// Invoked when this verb is activated from the right click menu.
/// </summary>
/// <param name="user">The entity of the user opening this menu.</param>
/// <param name="target">The entity that is being acted upon.</param>
public abstract void Activate(IEntity user, IEntity target);
public VerbData GetData(IEntity user, IEntity target)
{
var data = new VerbData();
GetData(user, target, data);
return data;
}
}
/// <summary>
/// This attribute should be used on <see cref="GlobalVerb"/>. These are verbs which are on visible for all entities,
/// regardless of the components they contain.
/// </summary>
[MeansImplicitUse]
[BaseTypeRequired(typeof(GlobalVerb))]
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class GlobalVerbAttribute : Attribute
{
}
}

View File

@@ -1,15 +0,0 @@
using Robust.Shared.GameObjects;
namespace Content.Shared.Verbs
{
[RegisterComponent]
public class HideContextMenuComponent : Component, IShowContextMenu
{
public override string Name => "HideContextMenu";
public bool ShowContextMenu(IEntity examiner)
{
return false;
}
}
}

View File

@@ -1,9 +0,0 @@
using Robust.Shared.GameObjects;
namespace Content.Shared.Verbs
{
public interface IShowContextMenu : IComponent
{
bool ShowContextMenu(IEntity examiner);
}
}

View File

@@ -1,17 +0,0 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.Verbs
{
[Serializable, NetSerializable]
public class PlayerContainerVisibilityMessage : EntityEventArgs
{
public readonly bool CanSeeThrough;
public PlayerContainerVisibilityMessage(bool canSeeThrough)
{
CanSeeThrough = canSeeThrough;
}
}
}

View File

@@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Interaction;
using Content.Shared.Physics;
using Content.Shared.Examine;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Tag;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -15,18 +16,20 @@ namespace Content.Shared.Verbs
[Dependency] private readonly IEntityLookup _lookup = default!;
/// <summary>
/// Get all of the entities relevant for the contextmenu
/// Get all of the entities in an area for displaying on the context menu.
/// </summary>
/// <param name="player"></param>
/// <param name="targetPos"></param>
/// <param name="contextEntities"></param>
/// <param name="buffer">Whether we should slightly extend out the ignored range for the ray predicated</param>
/// <returns></returns>
public bool TryGetContextEntities(IEntity player, MapCoordinates targetPos, [NotNullWhen(true)] out List<IEntity>? contextEntities, bool buffer = false)
/// <param name="buffer">Whether we should slightly extend the entity search area.</param>
public bool TryGetContextEntities(IEntity player, MapCoordinates targetPos,
[NotNullWhen(true)] out List<IEntity>? contextEntities, bool buffer = false, bool ignoreVisibility = false)
{
contextEntities = null;
var length = buffer ? 1.0f: 0.5f;
// Check if we have LOS to the clicked-location.
if (!ignoreVisibility && !player.InRangeUnOccluded(targetPos, range: ExamineSystemShared.ExamineRange))
return false;
// Get entities
var length = buffer ? 1.0f : 0.5f;
var entities = _lookup.GetEntitiesIntersecting(
targetPos.MapId,
Box2.CenteredAround(targetPos.Position, (length, length)))
@@ -34,34 +37,113 @@ namespace Content.Shared.Verbs
if (entities.Count == 0) return false;
// TODO: Can probably do a faster distance check with EntityCoordinates given we don't need to get map stuff.
if (ignoreVisibility)
{
contextEntities = entities;
return true;
}
// Check if we have LOS to the clicked-location, otherwise no popup.
// perform visibility checks
var playerPos = player.Transform.MapPosition;
var vectorDiff = playerPos.Position - targetPos.Position;
var distance = vectorDiff.Length + 0.01f;
bool Ignored(IEntity entity)
foreach (var entity in entities.ToList())
{
return entities.Contains(entity) ||
entity == player ||
!entity.TryGetComponent(out OccluderComponent? occluder) ||
!occluder.Enabled;
if (entity.HasTag("HideContextMenu"))
{
entities.Remove(entity);
continue;
}
if (!ExamineSystemShared.InRangeUnOccluded(
playerPos,
entity.Transform.MapPosition,
ExamineSystemShared.ExamineRange,
null) )
{
entities.Remove(entity);
}
}
var mask = player.TryGetComponent(out SharedEyeComponent? eye) && eye.DrawFov
? CollisionGroup.Opaque
: CollisionGroup.None;
var result = Get<SharedInteractionSystem>().InRangeUnobstructed(playerPos, targetPos, distance, mask, Ignored);
if (!result)
{
if (entities.Count == 0)
return false;
}
contextEntities = entities;
return true;
}
/// <summary>
/// Raises a number of events in order to get all verbs of the given type(s)
/// </summary>
public Dictionary<VerbType, SortedSet<Verb>> GetVerbs(IEntity target, IEntity user, VerbType verbTypes)
{
Dictionary<VerbType, SortedSet<Verb>> verbs = new();
if ((verbTypes & VerbType.Interaction) == VerbType.Interaction)
{
GetInteractionVerbsEvent getVerbEvent = new(user, target);
RaiseLocalEvent(target.Uid, getVerbEvent);
verbs.Add(VerbType.Interaction, getVerbEvent.Verbs);
}
if ((verbTypes & VerbType.Activation) == VerbType.Activation)
{
GetActivationVerbsEvent getVerbEvent = new(user, target);
RaiseLocalEvent(target.Uid, getVerbEvent);
verbs.Add(VerbType.Activation, getVerbEvent.Verbs);
}
if ((verbTypes & VerbType.Alternative) == VerbType.Alternative)
{
GetAlternativeVerbsEvent getVerbEvent = new(user, target);
RaiseLocalEvent(target.Uid, getVerbEvent);
verbs.Add(VerbType.Alternative, getVerbEvent.Verbs);
}
if ((verbTypes & VerbType.Other) == VerbType.Other)
{
GetOtherVerbsEvent getVerbEvent = new(user, target);
RaiseLocalEvent(target.Uid, getVerbEvent);
verbs.Add(VerbType.Other, getVerbEvent.Verbs);
}
return verbs;
}
/// <summary>
/// Execute actions associated with the given verb.
/// </summary>
/// <remarks>
/// This will try to call delegates and raise any events for the given verb.
/// </remarks>
public bool TryExecuteVerb(Verb verb)
{
var executed = false;
// Maybe run a delegate
if (verb.Act != null)
{
executed = true;
verb.Act.Invoke();
}
// Maybe raise a local event
if (verb.LocalVerbEventArgs != null)
{
executed = true;
if (verb.LocalEventTarget.IsValid())
RaiseLocalEvent(verb.LocalEventTarget, verb.LocalVerbEventArgs);
else
RaiseLocalEvent(verb.LocalVerbEventArgs);
}
// maybe raise a network event
if (verb.NetworkVerbEventArgs != null)
{
executed = true;
RaiseNetworkEvent(verb.NetworkVerbEventArgs);
}
// return false if all of these were null
return executed;
}
}
}

View File

@@ -1,90 +1,166 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using System;
namespace Content.Shared.Verbs
{
/// <summary>
/// A verb is an action in the right click menu of an entity.
/// </summary>
/// <remarks>
/// To add a verb to an entity, define it as a nested class inside the owning component,
/// and mark it with <see cref="VerbAttribute"/>
/// </remarks>
[UsedImplicitly]
public abstract class Verb : VerbBase
[Flags]
public enum VerbType
{
/// <summary>
/// Gets the visible verb data for the user.
/// </summary>
/// <remarks>
/// Implementations should write into <paramref name="data"/> to return their data.
/// </remarks>
/// <param name="user">The entity of the user opening this menu.</param>
/// <param name="component">The component instance for which this verb is being loaded.</param>
/// <param name="data">The data that must be filled into.</param>
protected abstract void GetData(IEntity user, IComponent component, VerbData data);
/// <summary>
/// Invoked when this verb is activated from the right click menu.
/// </summary>
/// <param name="user">The entity of the user opening this menu.</param>
/// <param name="component">The component instance for which this verb is being loaded.</param>
public abstract void Activate(IEntity user, IComponent component);
public VerbData GetData(IEntity user, IComponent component)
{
var data = new VerbData();
GetData(user, component, data);
return data;
}
}
/// <inheritdoc />
/// <summary>
/// Sub class of <see cref="T:Content.Shared.Verbs.Verb" /> that works on a specific type of component,
/// to reduce casting boiler plate for implementations.
/// </summary>
/// <typeparam name="T">The type of component that this verb will run on.</typeparam>
public abstract class Verb<T> : Verb where T : IComponent
{
/// <summary>
/// Gets the visible verb data for the user.
/// </summary>
/// <remarks>
/// Implementations should write into <paramref name="data"/> to return their data.
/// </remarks>
/// <param name="user">The entity of the user opening this menu.</param>
/// <param name="component">The component instance for which this verb is being loaded.</param>
/// <param name="data">The data that must be filled into.</param>
protected abstract void GetData(IEntity user, T component, VerbData data);
/// <summary>
/// Invoked when this verb is activated from the right click menu.
/// </summary>
/// <param name="user">The entity of the user opening this menu.</param>
/// <param name="component">The component instance for which this verb is being loaded.</param>
protected abstract void Activate(IEntity user, T component);
protected sealed override void GetData(IEntity user, IComponent component, VerbData data)
{
GetData(user, (T) component, data);
}
/// <inheritdoc />
public sealed override void Activate(IEntity user, IComponent component)
{
Activate(user, (T) component);
}
Interaction = 1,
Activation = 2,
Alternative = 4,
Other = 8,
All = 1+2+4+8
}
/// <summary>
/// This attribute should be used on <see cref="Verb"/> implementations nested inside component classes,
/// so that they're automatically detected.
/// Verb objects describe actions that a user can take. The actions can be specified via an Action, local
/// events, or networked events. Verbs also provide text, icons, and categories for displaying in the
/// context-menu.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[MeansImplicitUse]
public sealed class VerbAttribute : Attribute
[Serializable, NetSerializable]
public sealed class Verb : IComparable
{
/// <summary>
/// This is an action that will be run when the verb is "acted" out.
/// </summary>
/// <remarks>
/// This delegate probably just points to some function in the system assembling this verb. This delegate
/// will be run regardless of whether <see cref="LocalVerbEventArgs"/> or <see cref="NetworkVerbEventArgs"/>
/// are defined.
/// </remarks>
[NonSerialized]
public Action? Act;
/// <summary>
/// This is local event that will be raised when the verb is executed.
/// </summary>
/// <remarks>
/// This event will be raised regardless of whether <see cref="NetworkVerbEventArgs"/> or <see cref="Act"/>
/// are defined.
/// </remarks>
[NonSerialized]
public object? LocalVerbEventArgs;
/// <summary>
/// Where do direct the local event.
/// </summary>
[NonSerialized]
public EntityUid LocalEventTarget = EntityUid.Invalid;
/// <summary>
/// This is networked event that will be raised when the verb is executed.
/// </summary>
/// <remarks>
/// This event will be raised regardless of whether <see cref="LocalVerbEventArgs"/> or <see cref="Act"/>
/// are defined.
/// </remarks>
[NonSerialized]
public EntityEventArgs? NetworkVerbEventArgs;
/// <summary>
/// The text that the user sees on the verb button.
/// </summary>
public string Text = string.Empty;
/// <summary>
/// Sprite of the icon that the user sees on the verb button.
/// </summary>
public SpriteSpecifier? Icon
{
get => _icon ??=
IconTexture == null ? null : new SpriteSpecifier.Texture(new ResourcePath(IconTexture));
set => _icon = value;
}
private SpriteSpecifier? _icon;
/// <summary>
/// Name of the category this button is under. Used to group verbs in the context menu.
/// </summary>
public VerbCategory? Category;
/// <summary>
/// Whether this verb is disabled.
/// </summary>
/// <remarks>
/// Disabled verbs are shown in the context menu with a slightly darker background color, and cannot be
/// executed. It is recommended that a <see cref="Tooltip"/> message be provided outlining why this verb is
/// disabled.
/// </remarks>
public bool Disabled;
/// <summary>
/// Optional tooltip to show when hovering over this verb.
/// </summary>
/// <remarks>
/// Useful for disabled verbs as a replacement for informative pop-up messages.
/// </remarks>
public string? Tooltip;
/// <summary>
/// Determines the priority of the verb. This affects both how the verb is displayed in the context menu
/// GUI, and which verb is actually executed when left/alt clicking.
/// </summary>
/// <remarks>
/// Bigger is higher priority (appears first, gets executed preferentially).
/// </remarks>
public int Priority;
/// <summary>
/// Raw texture path used to load the <see cref="Icon"/>.
/// </summary>
public string? IconTexture;
/// <summary>
/// Whether or not to close the context menu after using it to run this verb.
/// </summary>
/// <remarks>
/// Setting this to false may be useful for repeatable actions, like rotating an object or maybe knocking on
/// a window.
/// </remarks>
public bool CloseMenu = true;
/// <summary>
/// Compares two verbs based on their <see cref="Priority"/>, <see cref="Category"/>, <see cref="Text"/>,
/// and <see cref="IconTexture"/>.
/// </summary>
/// <remarks>
/// <para>
/// This is comparison is used when storing verbs in a SortedSet. The ordering of verbs determines both how
/// the verbs are displayed in the context menu, and the order in which alternative action verbs are
/// executed when alt-clicking.
/// </para>
/// <para>
/// If two verbs are equal according to this comparison, they cannot both be added to the same sorted set of
/// verbs. This is desirable, given that these verbs would also appear identical in the context menu.
/// Distinct verbs should always have a unique and descriptive combination of text, icon, and category.
/// </para>
/// </remarks>
public int CompareTo(object? obj)
{
if (obj is not Verb otherVerb)
return -1;
// Sort first by priority
if (Priority != otherVerb.Priority)
return otherVerb.Priority - Priority;
// Then try use alphabetical verb categories. Uncategorized verbs always appear first.
if (Category?.Text != otherVerb.Category?.Text)
{
return string.Compare(Category?.Text, otherVerb.Category?.Text, StringComparison.CurrentCulture);
}
// Then try use alphabetical verb text.
if (Text != otherVerb.Text)
{
return string.Compare(Text, otherVerb.Text, StringComparison.CurrentCulture);
}
// Finally, compare icon texture paths. Note that this matters for verbs that don't have any text (e.g., the rotate-verbs)
return string.Compare(IconTexture, otherVerb.IconTexture, StringComparison.CurrentCulture);
}
}
}

View File

@@ -1,24 +0,0 @@
namespace Content.Shared.Verbs
{
public abstract class VerbBase
{
/// <summary>
/// If true, this verb requires the user to be inside within
/// <see cref="VerbUtility.InteractionRange"/> meters from the entity on which this verb resides.
/// </summary>
public virtual bool RequireInteractionRange => true;
/// <summary>
/// If true, this verb requires both the user and the entity on which
/// this verb resides to be in the same container or no container.
/// OR the user can be the entity's container
/// </summary>
public virtual bool BlockedByContainers => true;
/// <summary>
/// If true, this verb can be activated by alt-clicking on the entity.
/// </summary>
public virtual bool AlternativeInteraction => false;
}
}

View File

@@ -1,18 +0,0 @@
namespace Content.Shared.Verbs
{
/// <summary>
/// Standard verb categories.
/// </summary>
public static class VerbCategories
{
public static readonly VerbCategoryData Debug =
("Debug", "/Textures/Interface/VerbIcons/debug.svg.192dpi.png");
public static readonly VerbCategoryData Rotate = ("Rotate", null);
public static readonly VerbCategoryData Construction =
("Construction", "/Textures/Interface/hammer_scaled.svg.192dpi.png");
public static readonly VerbCategoryData SetTransferAmount =
("Set Transfer Amount", "/Textures/Interface/VerbIcons/spill.svg.192dpi.png");
}
}

View File

@@ -0,0 +1,54 @@
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using System;
namespace Content.Shared.Verbs
{
/// <summary>
/// Contains combined name and icon information for a verb category.
/// </summary>
[Serializable, NetSerializable]
public class VerbCategory
{
public readonly string Text;
public readonly SpriteSpecifier? Icon;
/// <summary>
/// If true, this verb category is shown in the context menu as a row of icons without any text.
/// </summary>
/// <remarks>
/// For example, the 'Rotate' category simply shows two icons for rotating left and right.
/// </remarks>
public readonly bool IconsOnly;
public VerbCategory(string text, string? icon, bool iconsOnly = false)
{
Text = Loc.GetString(text);
Icon = icon == null ? null : new SpriteSpecifier.Texture(new ResourcePath(icon));
IconsOnly = iconsOnly;
}
public static readonly VerbCategory Debug =
new("verb-categories-debug", "/Textures/Interface/VerbIcons/debug.svg.192dpi.png");
public static readonly VerbCategory Eject =
new("verb-categories-eject", "/Textures/Interface/VerbIcons/eject.svg.192dpi.png");
public static readonly VerbCategory Insert =
new("verb-categories-insert", "/Textures/Interface/VerbIcons/insert.svg.192dpi.png");
public static readonly VerbCategory Buckle =
new("verb-categories-buckle", "/Textures/Interface/VerbIcons/buckle.svg.192dpi.png");
public static readonly VerbCategory Unbuckle =
new("verb-categories-unbuckle", "/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png");
public static readonly VerbCategory Rotate =
new("verb-categories-rotate", "/Textures/Interface/VerbIcons/refresh.svg.192dpi.png", iconsOnly: true);
public static readonly VerbCategory SetTransferAmount =
new("verb-categories-transfer", "/Textures/Interface/VerbIcons/spill.svg.192dpi.png");
}
}

View File

@@ -1,31 +0,0 @@
using Robust.Shared.Utility;
namespace Content.Shared.Verbs
{
/// <summary>
/// Contains combined name and icon information for a verb category.
/// </summary>
public readonly struct VerbCategoryData
{
public VerbCategoryData(string name, SpriteSpecifier? icon)
{
Name = name;
Icon = icon;
}
public string Name { get; }
public SpriteSpecifier? Icon { get; }
public static implicit operator VerbCategoryData((string name, string? icon) tuple)
{
var (name, icon) = tuple;
if (icon == null)
{
return new VerbCategoryData(name, null);
}
return new VerbCategoryData(name, new SpriteSpecifier.Texture(new ResourcePath(icon)));
}
}
}

View File

@@ -1,68 +0,0 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;
namespace Content.Shared.Verbs
{
/// <summary>
/// Stores visual data for a verb.
/// </summary>
/// <remarks>
/// An instance of this class gets instantiated by the verb system and should be filled in by implementations of
/// <see cref="Verb.GetData(IEntity, IComponent, VerbData)"/>.
/// </remarks>
public sealed class VerbData
{
/// <summary>
/// The text that the user sees on the verb button.
/// </summary>
/// <remarks>
/// This string is automatically passed through Loc.GetString().
/// </remarks>
public string Text { get; set; } = string.Empty;
/// <summary>
/// Sprite of the icon that the user sees on the verb button.
/// </summary>
public SpriteSpecifier? Icon { get; set; }
/// <summary>
/// Name of the category this button is under.
/// </summary>
public string Category { get; set; } = "";
/// <summary>
/// Sprite of the icon that the user sees on the verb button.
/// </summary>
public SpriteSpecifier? CategoryIcon { get; set; }
/// <summary>
/// Whether this verb is visible, disabled (greyed out) or hidden.
/// </summary>
public VerbVisibility Visibility { get; set; } = VerbVisibility.Visible;
public bool IsInvisible => Visibility == VerbVisibility.Invisible;
public bool IsDisabled => Visibility == VerbVisibility.Disabled;
/// <summary>
/// Convenience property to set verb category and icon at once.
/// </summary>
[ValueProvider("Content.Shared.GameObjects.VerbCategories")]
public VerbCategoryData CategoryData
{
set
{
Category = value.Name;
CategoryIcon = value.Icon;
}
}
/// <summary>
/// Convenience property to set <see cref="Icon"/> to a raw texture path.
/// </summary>
public string IconTexture
{
set => Icon = new SpriteSpecifier.Texture(new ResourcePath(value));
}
}
}

View File

@@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using Content.Shared.ActionBlocker;
using Content.Shared.Hands.Components;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.IoC;
using Content.Shared.Interaction;
namespace Content.Shared.Verbs
{
[Serializable, NetSerializable]
public class RequestServerVerbsEvent : EntityEventArgs
{
public readonly EntityUid EntityUid;
public readonly VerbType Type;
public RequestServerVerbsEvent(EntityUid entityUid, VerbType type)
{
EntityUid = entityUid;
Type = type;
}
}
[Serializable, NetSerializable]
public class VerbsResponseEvent : EntityEventArgs
{
public readonly Dictionary<VerbType, List<Verb>>? Verbs;
public readonly EntityUid Entity;
public VerbsResponseEvent(EntityUid entity, Dictionary<VerbType, SortedSet<Verb>>? verbs)
{
Entity = entity;
if (verbs == null)
return;
// Apparently SortedSet is not serlializable. Cast to List<Verb>.
Verbs = new();
foreach (var entry in verbs)
{
Verbs.Add(entry.Key, new List<Verb>(entry.Value));
}
}
}
[Serializable, NetSerializable]
public class TryExecuteVerbEvent : EntityEventArgs
{
public readonly EntityUid Target;
public readonly Verb RequestedVerb;
/// <summary>
/// The type of verb to try execute. Avoids having to get a list of all verbs on the receiving end.
/// </summary>
public readonly VerbType Type;
public TryExecuteVerbEvent(EntityUid target, Verb requestedVerb, VerbType type)
{
Target = target;
RequestedVerb = requestedVerb;
Type = type;
}
}
/// <summary>
/// Event used to toggle visibility of all context menu entities.
/// </summary>
[Serializable, NetSerializable]
public class SetSeeAllContextEvent : EntityEventArgs
{
public bool CanSeeAllContext = false;
}
/// <summary>
/// Request primary interaction verbs. This includes both use-in-hand and interacting with external entities.
/// </summary>
/// <remarks>
/// These verbs those that involve using the hands or the currently held item on some entity. These verbs usually
/// correspond to interactions that can be triggered by left-clicking or using 'Z', and often depend on the
/// currently held item. These verbs are collectively shown first in the context menu.
/// </remarks>
public class GetInteractionVerbsEvent : GetVerbsEvent
{
public GetInteractionVerbsEvent(IEntity user, IEntity target) : base(user, target) { }
}
/// <summary>
/// Request activation verbs.
/// </summary>
/// <remarks>
/// These are verbs that activate an item in the world but are independent of the currently held items. For
/// example, opening a door or a GUI. These verbs should correspond to interactions that can be triggered by
/// using 'E', though many of those can also be triggered by left-mouse or 'Z' if there is no other interaction.
/// These verbs are collectively shown second in the context menu.
/// </remarks>
public class GetActivationVerbsEvent : GetVerbsEvent
{
public GetActivationVerbsEvent(IEntity user, IEntity target) : base(user, target) { }
}
/// <summary>
/// Request alternative-interaction verbs.
/// </summary>
/// <remarks>
/// When interacting with an entity via alt + left-click/E/Z the highest priority alt-interact verb is executed.
/// These verbs are collectively shown second-to-last in the context menu.
/// </remarks>
public class GetAlternativeVerbsEvent : GetVerbsEvent
{
public GetAlternativeVerbsEvent(IEntity user, IEntity target) : base(user, target) { }
}
/// <summary>
/// Request Miscellaneous verbs.
/// </summary>
/// <remarks>
/// Includes (nearly) global interactions like "examine", "pull", or "debug". These verbs are collectively shown
/// last in the context menu.
/// </remarks>
public class GetOtherVerbsEvent : GetVerbsEvent
{
public GetOtherVerbsEvent(IEntity user, IEntity target) : base(user, target) { }
}
/// <summary>
/// Directed event that requests verbs from any systems/components on a target entity.
/// </summary>
public class GetVerbsEvent : EntityEventArgs
{
/// <summary>
/// Event output. Set of verbs that can be executed.
/// </summary>
public SortedSet<Verb> Verbs = new();
/// <summary>
/// Can the user physically access the target?
/// </summary>
/// <remarks>
/// This is a combination of <see cref="ContainerHelpers.IsInSameOrParentContainer"/> and
/// <see cref="SharedInteractionSystem.InRangeUnobstructed"/>.
/// </remarks>
public bool CanAccess;
/// <summary>
/// The entity being targeted for the verb.
/// </summary>
public IEntity Target;
/// <summary>
/// The entity that will be "performing" the verb.
/// </summary>
public IEntity User;
/// <summary>
/// Can the user physically interact?
/// </summary>
/// <remarks>
/// This is a just a cached <see cref="ActionBlockerSystem.CanInteract"/> result. Given that many verbs need
/// to check this, it prevents it from having to be repeatedly called by each individual system that might
/// contribute a verb.
/// </remarks>
public bool CanInteract;
/// <summary>
/// The User's hand component.
/// </summary>
/// <remarks>
/// This may be null if the user has no hands.
/// </remarks>
public SharedHandsComponent? Hands;
/// <summary>
/// The entity currently being held by the active hand.
/// </summary>
/// <remarks>
/// This is only ever not null when <see cref="ActionBlockerSystem.CanUse(IEntity)"/> is true and the user
/// has hands.
/// </remarks>
public IEntity? Using;
public GetVerbsEvent(IEntity user, IEntity target)
{
User = user;
Target = target;
CanAccess = (Target == User) || user.IsInSameOrParentContainer(target) &&
EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(user, target);
// A large number of verbs need to check action blockers. Instead of repeatedly having each system individually
// call ActionBlocker checks, just cache it for the verb request.
var actionBlockerSystem = EntitySystem.Get<ActionBlockerSystem>();
CanInteract = actionBlockerSystem.CanInteract(user);
if (!user.TryGetComponent(out Hands) ||
!actionBlockerSystem.CanUse(user))
return;
Hands.TryGetActiveHeldEntity(out Using);
// Check whether the "Held" entity is a virtual pull entity. If yes, set that as the entity being "Used".
// This allows you to do things like buckle a dragged person onto a surgery table, without click-dragging
// their sprite.
if (Using != null && Using.TryGetComponent<HandVirtualItemComponent>(out var pull))
{
Using = IoCManager.Resolve<IEntityManager>().GetEntity(pull.BlockingEntity);
}
}
}
}

View File

@@ -1,69 +0,0 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Verbs
{
public static class VerbSystemMessages
{
[Serializable, NetSerializable]
public class RequestVerbsMessage : EntityEventArgs
{
public readonly EntityUid EntityUid;
public RequestVerbsMessage(EntityUid entityUid)
{
EntityUid = entityUid;
}
}
[Serializable, NetSerializable]
public class VerbsResponseMessage : EntityEventArgs
{
public readonly NetVerbData[] Verbs;
public readonly EntityUid Entity;
public VerbsResponseMessage(NetVerbData[] verbs, EntityUid entity)
{
Verbs = verbs;
Entity = entity;
}
[Serializable, NetSerializable]
public readonly struct NetVerbData
{
public readonly string Text;
public readonly string Key;
public readonly string Category;
public readonly SpriteSpecifier? Icon;
public readonly SpriteSpecifier? CategoryIcon;
public readonly bool Available;
public NetVerbData(VerbData data, string key)
{
Text = data.Text;
Key = key;
Category = data.Category;
CategoryIcon = data.CategoryIcon;
Icon = data.Icon;
Available = data.Visibility == VerbVisibility.Visible;
}
}
}
[Serializable, NetSerializable]
public class UseVerbMessage : EntityEventArgs
{
public readonly EntityUid EntityUid;
public readonly string VerbKey;
public UseVerbMessage(EntityUid entityUid, string verbKey)
{
EntityUid = entityUid;
VerbKey = verbKey;
}
}
}
}

View File

@@ -1,99 +0,0 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
namespace Content.Shared.Verbs
{
public static class VerbUtility
{
public const float InteractionRange = 2;
public const float InteractionRangeSquared = InteractionRange * InteractionRange;
// TODO: This is a quick hack. Verb objects should absolutely be cached properly.
// This works for now though.
public static IEnumerable<(IComponent, Verb)> GetVerbs(IEntity entity)
{
var typeFactory = IoCManager.Resolve<IDynamicTypeFactory>();
foreach (var component in entity.GetAllComponents())
{
var type = component.GetType();
foreach (var nestedType in type.GetAllNestedTypes())
{
if (!typeof(Verb).IsAssignableFrom(nestedType) || nestedType.IsAbstract)
{
continue;
}
var verb = typeFactory.CreateInstance<Verb>(nestedType);
yield return (component, verb);
}
}
}
/// <summary>
/// Returns an IEnumerable of all classes inheriting <see cref="GlobalVerb"/> with the <see cref="GlobalVerbAttribute"/> attribute.
/// </summary>
/// <param name="assembly">The assembly to search for global verbs in.</param>
public static IEnumerable<GlobalVerb> GetGlobalVerbs(Assembly assembly)
{
var typeFactory = IoCManager.Resolve<IDynamicTypeFactory>();
foreach (Type type in assembly.GetTypes())
{
if (Attribute.IsDefined(type, typeof(GlobalVerbAttribute)))
{
if (!typeof(GlobalVerb).IsAssignableFrom(type) || type.IsAbstract)
{
continue;
}
yield return typeFactory.CreateInstance<GlobalVerb>(type);
}
}
}
public static bool VerbAccessChecks(IEntity user, IEntity target, VerbBase verb)
{
if (verb.RequireInteractionRange && !InVerbUseRange(user, target))
{
return false;
}
if (verb.BlockedByContainers && !VerbContainerCheck(user, target))
{
return false;
}
return true;
}
public static bool InVerbUseRange(IEntity user, IEntity target)
{
var distanceSquared = (user.Transform.WorldPosition - target.Transform.WorldPosition)
.LengthSquared;
if (distanceSquared > InteractionRangeSquared)
{
return false;
}
return true;
}
public static bool VerbContainerCheck(IEntity user, IEntity target)
{
if (!user.IsInSameOrNoContainer(target))
{
if (!target.TryGetContainer(out var container) ||
container.Owner != user)
{
return false;
}
}
return true;
}
}
}

View File

@@ -1,24 +0,0 @@
namespace Content.Shared.Verbs
{
/// <summary>
/// Possible states of visibility for the verb in the right click menu.
/// </summary>
public enum VerbVisibility
{
/// <summary>
/// The verb will be listed in the right click menu.
/// </summary>
Visible,
/// <summary>
/// The verb will be listed, but it will be grayed out and unable to be clicked on.
/// </summary>
Disabled,
/// <summary>
/// The verb will not be listed in the right click menu.
/// </summary>
Invisible
}
}