Add Alt-click functionality (#4497)

* Fix ItemSlot Bug

* Add Alt-use Key

* Fix TransferAmount window bug

* Alt-click functionality

* Added AltInteract verbs

* Add new verbs

* verb icons

* Changed Comments

* Change Comments

* Fix disposal verbs

* Changed Get...() to Get...OrNull()

* Changed alt-interact combat behaviour

* Update verb icons

* Inventory interact event

* Add Alt+E secondary binding

* Add alt-z keybinding

* Rename AltUse -> AltActivateItemInWorld
This commit is contained in:
Leon Friedrich
2021-08-22 03:20:18 +10:00
committed by GitHub
parent ad5f7bb71b
commit 486dc6ca62
51 changed files with 748 additions and 53 deletions

View File

@@ -256,6 +256,8 @@ namespace Content.Server.Access.Components
[Verb]
public sealed class EjectPrivilegedIDVerb : Verb<IdCardConsoleComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, IdCardConsoleComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
@@ -281,6 +283,8 @@ namespace Content.Server.Access.Components
public sealed class EjectTargetIDVerb : Verb<IdCardConsoleComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, IdCardConsoleComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))

View File

@@ -43,6 +43,7 @@ namespace Content.Server.Administration.Verbs
}
data.Text = Loc.GetString("admin-add-reagent-verb-get-data-text");
data.IconTexture = "/Textures/Interface/VerbIcons/spill.svg.192dpi.png";
data.CategoryData = VerbCategories.Debug;
data.Visibility = VerbVisibility.Invisible;

View File

@@ -75,6 +75,11 @@ namespace Content.Server.Cabinet
[Verb]
public sealed class ToggleItemCabinetVerb : Verb<ItemCabinetComponent>
{
// Unlike lockers, you cannot open/close cabinets by clicking on them, as this usually removes their item
// instead. So open/close is the alt-interact verb
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, ItemCabinetComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))

View File

@@ -426,6 +426,8 @@ namespace Content.Server.Chemistry.Components
[Verb]
public sealed class EjectBeakerVerb : Verb<ChemMasterComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, ChemMasterComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
@@ -436,6 +438,7 @@ namespace Content.Server.Chemistry.Components
data.Text = Loc.GetString("eject-beaker-verb-get-data-text");
data.Visibility = component.HasBeaker ? VerbVisibility.Visible : VerbVisibility.Invisible;
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
}
protected override void Activate(IEntity user, ChemMasterComponent component)

View File

@@ -368,6 +368,8 @@ namespace Content.Server.Chemistry.Components
[Verb]
public sealed class EjectBeakerVerb : Verb<ReagentDispenserComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, ReagentDispenserComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
@@ -378,6 +380,7 @@ namespace Content.Server.Chemistry.Components
data.Text = Loc.GetString("eject-beaker-verb-get-data-text");
data.Visibility = component.HasBeaker ? VerbVisibility.Visible : VerbVisibility.Invisible;
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
}
protected override void Activate(IEntity user, ReagentDispenserComponent component)

View File

@@ -282,6 +282,8 @@ namespace Content.Server.Chemistry.Components
[Verb]
public sealed class CustomTransferVerb : Verb<SolutionTransferComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, SolutionTransferComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user) || !component.CanChangeTransferAmount)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Content.Server.DoAfter;
using Content.Server.Notification;
using Content.Shared.ActionBlocker;
@@ -242,6 +242,8 @@ namespace Content.Server.Climbing.Components
[Verb]
private sealed class ClimbVerb : Verb<ClimbableComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, ClimbableComponent component, VerbData data)
{
if (!component.CanVault(user, component.Owner, out var _))

View File

@@ -207,7 +207,7 @@ namespace Content.Server.Disposal.Unit.Components
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("self-insert-verb-get-data-text");
data.Text = Loc.GetString("disposal-self-insert-verb-get-data-text");
}
protected override void Activate(IEntity user, DisposalUnitComponent component)
@@ -230,8 +230,8 @@ namespace Content.Server.Disposal.Unit.Components
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("flush-verb-get-data-text");
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
data.Text = Loc.GetString("disposal-flush-verb-get-data-text");
data.IconTexture = "/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png";
}
protected override void Activate(IEntity user, DisposalUnitComponent component)
@@ -240,6 +240,37 @@ namespace Content.Server.Disposal.Unit.Components
}
}
[Verb]
private sealed class EjectVerb : Verb<DisposalUnitComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, DisposalUnitComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user) ||
component.ContainedEntities.Contains(user))
{
return;
}
// Only show verb if actually containing any entities.
if (component.ContainedEntities.Count > 0)
data.Visibility = VerbVisibility.Visible;
else
data.Visibility = VerbVisibility.Invisible;
data.Text = Loc.GetString("disposal-eject-verb-get-data-text");
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
}
protected override void Activate(IEntity user, DisposalUnitComponent component)
{
EntitySystem.Get<DisposalUnitSystem>().TryEjectContents(component);
}
}
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
{
EntitySystem.Get<DisposalUnitSystem>().TryEjectContents(this);

View File

@@ -82,10 +82,10 @@ namespace Content.Server.Hands.Components
.TryInteractionActivate(Owner, heldEntity);
}
protected override void DoUse(IEntity heldEntity)
protected override void DoUse(IEntity heldEntity, bool altInteract = false)
{
_entitySystemManager.GetEntitySystem<InteractionSystem>()
.TryUseInteraction(Owner, heldEntity);
.TryUseInteraction(Owner, heldEntity, altInteract);
}
protected override void HandlePickupAnimation(IEntity entity)

View File

@@ -50,6 +50,7 @@ namespace Content.Server.Hands
CommandBinds.Builder
.Bind(ContentKeyFunctions.ActivateItemInHand, InputCmdHandler.FromDelegate(HandleActivateItem))
.Bind(ContentKeyFunctions.AltActivateItemInHand, InputCmdHandler.FromDelegate(HandleAltActivateItem))
.Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem))
.Bind(ContentKeyFunctions.SmartEquipBackpack, InputCmdHandler.FromDelegate(HandleSmartEquipBackpack))
.Bind(ContentKeyFunctions.SmartEquipBelt, InputCmdHandler.FromDelegate(HandleSmartEquipBelt))
@@ -220,6 +221,14 @@ namespace Content.Server.Hands
hands.UseActiveHeldEntity();
}
private void HandleAltActivateItem(ICommonSession? session)
{
if (!TryGetHandsComp(session, out var hands))
return;
hands.UseActiveHeldEntity(altInteract: true);
}
private bool HandleThrowItem(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (session is not IPlayerSession playerSession)

View File

@@ -32,6 +32,7 @@ namespace Content.Server.Interaction
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("in-range-unoccluded-verb-get-data-text");
data.IconTexture = "/Textures/Interface/VerbIcons/information.svg.192dpi.png";
data.CategoryData = VerbCategories.Debug;
}

View File

@@ -21,6 +21,7 @@ using Content.Shared.Inventory;
using Content.Shared.Notification.Managers;
using Content.Shared.Rotatable;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
@@ -52,10 +53,13 @@ namespace Content.Server.Interaction
public override void Initialize()
{
SubscribeNetworkEvent<DragDropRequestEvent>(HandleDragDropRequestEvent);
SubscribeNetworkEvent<InteractInventorySlotEvent>(HandleInteractInventorySlotEvent);
CommandBinds.Builder
.Bind(EngineKeyFunctions.Use,
new PointerInputCmdHandler(HandleUseInteraction))
.Bind(ContentKeyFunctions.AltActivateItemInWorld,
new PointerInputCmdHandler(HandleAltUseInteraction))
.Bind(ContentKeyFunctions.WideAttack,
new PointerInputCmdHandler(HandleWideAttack))
.Bind(ContentKeyFunctions.ActivateItemInWorld,
@@ -102,6 +106,34 @@ namespace Content.Server.Interaction
}
#endregion
/// <summary>
/// Handles the event were a client uses an item in their inventory or in their hands, either by
/// alt-clicking it or pressing 'E' while hovering over it.
/// </summary>
private void HandleInteractInventorySlotEvent(InteractInventorySlotEvent msg, EntitySessionEventArgs args)
{
if (!EntityManager.TryGetEntity(msg.ItemUid, out var item))
{
Logger.WarningS("system.interaction",
$"Client sent inventory interaction with an invalid target item. Session={args.SenderSession}");
return;
}
// client sanitization
if (!ValidateClientInput(args.SenderSession, item.Transform.Coordinates, msg.ItemUid, out var userEntity))
{
Logger.InfoS("system.interaction", $"Inventory interaction validation failed. Session={args.SenderSession}");
return;
}
if (msg.AltInteract)
// Use 'UserInteraction' function - behaves as if the user alt-clicked the item in the world.
UserInteraction(userEntity, item.Transform.Coordinates, msg.ItemUid, msg.AltInteract);
else
// User used 'E'. We want to activate it, not simulate clicking on the item
InteractionActivate(userEntity, item);
}
#region Drag drop
private void HandleDragDropRequestEvent(DragDropRequestEvent msg, EntitySessionEventArgs args)
{
@@ -241,6 +273,20 @@ namespace Content.Server.Interaction
return true;
}
public bool HandleAltUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
// client sanitization
if (!ValidateClientInput(session, coords, uid, out var userEntity))
{
Logger.InfoS("system.interaction", $"Alt-use input validation failed");
return true;
}
UserInteraction(userEntity, coords, uid, altInteract : true );
return true;
}
private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (!ValidateClientInput(session, coords, uid, out var userEntity))
@@ -264,12 +310,24 @@ namespace Content.Server.Interaction
return pull.TogglePull(userEntity);
}
public async void UserInteraction(IEntity user, EntityCoordinates coordinates, EntityUid clickedUid)
/// <summary>
/// Resolves user interactions with objects.
/// </summary>
/// <remarks>
/// Checks Whether combat mode is enabled and whether the user can actually interact with the given entity.
/// </remarks>
/// <param name="altInteract">Whether to use default or alternative interactions (usually as a result of
/// alt+clicking). If combat mode is enabled, the alternative action is to perform the default non-combat
/// interaction. Having an item in the active hand also disables alternative interactions.</param>
public async void UserInteraction(IEntity user, EntityCoordinates coordinates, EntityUid clickedUid, bool altInteract = false )
{
if (user.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode)
// TODO COMBAT Consider using alt-interact for advanced combat? maybe alt-interact disarms?
if (!altInteract && user.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode)
{
DoAttack(user, coordinates, false, clickedUid);
return;
}
if (!ValidateInteractAndFace(user, coordinates))
@@ -313,12 +371,22 @@ namespace Content.Server.Interaction
}
else
{
// We are close to the nearby object and the object isn't contained in our active hand
// InteractUsing/AfterInteract: We will either use the item on the nearby object
if (item != null)
// We are close to the nearby object.
if (altInteract)
// We are trying to use alternative interactions. Perform alternative interactions, using context
// menu verbs.
// Verbs can be triggered with an item in the hand, but currently there are no verbs that depend on
// the currently held item. Maybe this if statement should be changed to
// (altInteract && (item == null || item == target)).
// Note that item == target will happen when alt-clicking the item currently in your hands.
AltInteract(user, target);
else if (item != null && item != target)
// We are performing a standard interaction with an item, and the target isn't the same as the item
// currently in our hand. We will use the item in our hand on the nearby object via InteractUsing
await InteractUsing(user, item, target, coordinates);
// InteractHand/Activate: Since our hand is empty we will use InteractHand/Activate
else
else if (item == null)
// Since our hand is empty we will use InteractHand/Activate
InteractHand(user, target);
}
}
@@ -432,6 +500,44 @@ namespace Content.Server.Interaction
await InteractDoAfter(user, used, target, clickLocation, true);
}
/// <summary>
/// Alternative interactions on an entity.
/// </summary>
/// <remarks>
/// Uses the context menu verb list, and acts out the first verb marked as an alternative interaction. Note
/// that this does not have any checks to see whether this interaction is valid, as these are all done in <see
/// cref="UserInteraction(IEntity, EntityCoordinates, EntityUid, bool)"/>
/// </remarks>
public void AltInteract(IEntity user, IEntity target)
{
// TODO VERB SYSTEM when ECS-ing verbs and re-writing VerbUtility.GetVerbs, maybe sort verbs by some
// priority property, such that which verbs appear first is more predictable?.
// Iterate through list of verbs that apply to target. We do not include global verbs here. If in the future
// alt click should also support global verbs, this needs to be changed.
foreach (var (component, verb) in VerbUtility.GetVerbs(target))
{
// Check that the verb marked as an alternative interaction?
if (!verb.AlternativeInteraction)
continue;
// Can the verb be acted out?
if (!VerbUtility.VerbAccessChecks(user, target, verb))
continue;
// Is the verb currently enabled?
var verbData = verb.GetData(user, component);
if (verbData.IsInvisible || verbData.IsDisabled)
continue;
// Act out the verb. Note that, if there is more than one AlternativeInteraction verb, only the first
// one is activated. The priority is effectively determined by the order in which VerbUtility.GetVerbs()
// returns the verbs.
verb.Activate(user, component);
break;
}
}
/// <summary>
/// Uses an empty hand on an entity
/// Finds components with the InteractHand interface and calls their function
@@ -470,11 +576,14 @@ namespace Content.Server.Interaction
/// </summary>
/// <param name="user"></param>
/// <param name="used"></param>
public void TryUseInteraction(IEntity user, IEntity used)
public void TryUseInteraction(IEntity user, IEntity used, bool altInteract = false)
{
if (user != null && used != null && _actionBlockerSystem.CanUse(user))
{
UseInteraction(user, used);
if (altInteract)
AltInteract(user, used);
else
UseInteraction(user, used);
}
}

View File

@@ -44,6 +44,7 @@ namespace Content.Server.Items
}
data.Text = Loc.GetString("pick-up-verb-get-data-text");
data.IconTexture = "/Textures/Interface/VerbIcons/pickup.svg.192dpi.png";
}
protected override void Activate(IEntity user, ItemComponent component)

View File

@@ -26,6 +26,8 @@ namespace Content.Server.Storage.Components
[Verb]
private sealed class ToggleLockVerb : Verb<LockComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, LockComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user) ||

View File

@@ -228,6 +228,8 @@ namespace Content.Server.Medical.Components
[Verb]
public sealed class EjectVerb : Verb<MedicalScannerComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, MedicalScannerComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
@@ -238,6 +240,7 @@ namespace Content.Server.Medical.Components
data.Text = Loc.GetString("medical-scanner-eject-verb-get-data-text");
data.Visibility = component.IsOccupied ? VerbVisibility.Visible : VerbVisibility.Invisible;
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
}
protected override void Activate(IEntity user, MedicalScannerComponent component)

View File

@@ -375,6 +375,8 @@ namespace Content.Server.PDA
[Verb]
public sealed class EjectIDVerb : Verb<PDAComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, PDAComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))

View File

@@ -162,6 +162,8 @@ namespace Content.Server.Power.Components
[Verb]
private sealed class EjectVerb : Verb<BaseCharger>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, BaseCharger component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))

View File

@@ -179,6 +179,8 @@ namespace Content.Server.PowerCell.Components
[Verb]
public sealed class EjectCellVerb : Verb<PowerCellSlotComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, PowerCellSlotComponent component, VerbData data)
{
if (!component.ShowVerb || !EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))

View File

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Content.Server.DoAfter;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Shared.ActionBlocker;
using Content.Shared.Acts;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
@@ -14,6 +15,7 @@ using Content.Shared.Notification.Managers;
using Content.Shared.Placeable;
using Content.Shared.Sound;
using Content.Shared.Storage;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Server.GameObjects;
using Robust.Server.Player;
@@ -22,6 +24,7 @@ using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
@@ -629,5 +632,46 @@ namespace Content.Server.Storage.Components
{
SoundSystem.Play(Filter.Pvs(Owner), StorageSoundCollection.GetSound(), Owner, AudioParams.Default);
}
[Verb]
private sealed class ToggleOpenVerb : Verb<ServerStorageComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, ServerStorageComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
// Get the session for the user
var session = user.GetComponentOrNull<ActorComponent>()?.PlayerSession;
if (session == null)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
// Does this player currently have the storage UI open?
if (component.SubscribedSessions.Contains(session))
{
data.Text = Loc.GetString("toggle-open-verb-close");
data.IconTexture = "/Textures/Interface/VerbIcons/close.svg.192dpi.png";
} else
{
data.Text = Loc.GetString("toggle-open-verb-open");
data.IconTexture = "/Textures/Interface/VerbIcons/open.svg.192dpi.png";
}
}
/// <inheritdoc />
protected override void Activate(IEntity user, ServerStorageComponent component)
{
// "Open" actually closes the UI if it is already open.
component.OpenStorageUI(user);
}
}
}
}

View File

@@ -290,6 +290,8 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
[Verb]
public sealed class EjectCellVerb : Verb<ServerBatteryBarrelComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, ServerBatteryBarrelComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user) || !component._powerCellRemovable)

View File

@@ -429,6 +429,8 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
[Verb]
private sealed class EjectMagazineVerb : Verb<ServerMagazineBarrelComponent>
{
public override bool AlternativeInteraction => true;
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
{
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))