diff --git a/Content.Client/Changelog/ChangelogWindow.xaml.cs b/Content.Client/Changelog/ChangelogWindow.xaml.cs index c6117128f9..a1b2d65eac 100644 --- a/Content.Client/Changelog/ChangelogWindow.xaml.cs +++ b/Content.Client/Changelog/ChangelogWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Content.Client.Resources; using Content.Client.Stylesheets; diff --git a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs index 03eb454372..f2256d80f9 100644 --- a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs @@ -1,4 +1,4 @@ -using Content.Shared.Chemistry; +using Content.Shared.Chemistry; using Content.Shared.Chemistry.Reagent; using JetBrains.Annotations; using Robust.Client.GameObjects; @@ -24,7 +24,7 @@ namespace Content.Client.Chemistry.UI _window.Close(); } }; - + _window.OnClose += Close; _window.OpenCentered(); } diff --git a/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs b/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs index 1ad07c0e9d..1cc046b640 100644 --- a/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs +++ b/Content.Client/ContextMenu/UI/ContextMenuPresenter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading; using Content.Client.Examine; @@ -121,7 +121,7 @@ namespace Content.Client.ContextMenu.UI if (firstEntity == null) return; - if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject) + if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.AltActivateItemInWorld || args.Function == ContentKeyFunctions.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject) { var inputSys = _systemManager.GetEntitySystem(); @@ -221,7 +221,7 @@ namespace Content.Client.ContextMenu.UI return; } - if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.Point || + if (args.Function == EngineKeyFunctions.Use || args.Function == ContentKeyFunctions.AltActivateItemInWorld || args.Function == ContentKeyFunctions.Point || args.Function == ContentKeyFunctions.TryPullObject || args.Function == ContentKeyFunctions.MovePulledObject) { var inputSys = _systemManager.GetEntitySystem(); diff --git a/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs index bbd8fb0f6e..d8a018d1ea 100644 --- a/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Content.Client.Stylesheets; using Content.Shared.Input; @@ -85,7 +85,9 @@ namespace Content.Client.EscapeMenu.UI.Tabs AddButton(EngineKeyFunctions.Use); AddButton(ContentKeyFunctions.WideAttack); AddButton(ContentKeyFunctions.ActivateItemInHand); + AddButton(ContentKeyFunctions.AltActivateItemInHand); AddButton(ContentKeyFunctions.ActivateItemInWorld); + AddButton(ContentKeyFunctions.AltActivateItemInWorld); AddButton(ContentKeyFunctions.Drop); AddButton(ContentKeyFunctions.ExamineEntity); AddButton(ContentKeyFunctions.SwapHands); diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 494a85befd..c3fbb0cd71 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -31,9 +31,11 @@ namespace Content.Client.Input human.AddFunction(ContentKeyFunctions.SwapHands); human.AddFunction(ContentKeyFunctions.Drop); human.AddFunction(ContentKeyFunctions.ActivateItemInHand); + human.AddFunction(ContentKeyFunctions.AltActivateItemInHand); human.AddFunction(ContentKeyFunctions.OpenCharacterMenu); human.AddFunction(ContentKeyFunctions.ActivateItemInWorld); human.AddFunction(ContentKeyFunctions.ThrowItemInHand); + human.AddFunction(ContentKeyFunctions.AltActivateItemInWorld); human.AddFunction(ContentKeyFunctions.TryPullObject); human.AddFunction(ContentKeyFunctions.MovePulledObject); human.AddFunction(ContentKeyFunctions.ReleasePulledObject); diff --git a/Content.Client/Inventory/HumanInventoryInterfaceController.cs b/Content.Client/Inventory/HumanInventoryInterfaceController.cs index fa52c89b71..8df5647861 100644 --- a/Content.Client/Inventory/HumanInventoryInterfaceController.cs +++ b/Content.Client/Inventory/HumanInventoryInterfaceController.cs @@ -207,10 +207,9 @@ namespace Content.Client.Inventory return; if (!Owner.TryGetSlot(slot, out var item)) return; - if (_itemSlotManager.OnButtonPressed(args, item)) - return; - base.HandleInventoryKeybind(args, slot); + if (!_itemSlotManager.OnButtonPressed(args, item)) + base.HandleInventoryKeybind(args, slot); } private void ClearButton(ItemSlotButton button, Slots slot) diff --git a/Content.Client/Items/Managers/ItemSlotManager.cs b/Content.Client/Items/Managers/ItemSlotManager.cs index 3435913914..c274547cda 100644 --- a/Content.Client/Items/Managers/ItemSlotManager.cs +++ b/Content.Client/Items/Managers/ItemSlotManager.cs @@ -7,6 +7,7 @@ using Content.Client.Verbs; using Content.Shared.Cooldown; using Content.Shared.Hands.Components; using Content.Shared.Input; +using Content.Shared.Interaction; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Input; @@ -86,26 +87,11 @@ namespace Content.Client.Items.Managers } else if (args.Function == ContentKeyFunctions.ActivateItemInWorld) { - var inputSys = _entitySystemManager.GetEntitySystem(); - - var func = args.Function; - var funcId = _inputManager.NetworkBindMap.KeyFunctionID(args.Function); - - - var mousePosWorld = _eyeManager.ScreenToMap(args.PointerLocation); - - var coordinates = _mapManager.TryFindGridAt(mousePosWorld, out var grid) ? grid.MapToGrid(mousePosWorld) : - EntityCoordinates.FromMap(_mapManager, mousePosWorld); - - var message = new FullInputCmdMessage(_gameTiming.CurTick, _gameTiming.TickFraction, funcId, BoundKeyState.Down, - coordinates, args.PointerLocation, item.Uid); - - // client side command handlers will always be sent the local player session. - var session = _playerManager.LocalPlayer?.Session; - if (session == null) - return false; - - inputSys.HandleInputCommand(session, func, message); + _entityManager.EntityNetManager?.SendSystemNetworkMessage(new InteractInventorySlotEvent(item.Uid, altInteract: false)); + } + else if (args.Function == ContentKeyFunctions.AltActivateItemInWorld) + { + _entityManager.EntityNetManager?.SendSystemNetworkMessage(new InteractInventorySlotEvent(item.Uid, altInteract: true)); } else { diff --git a/Content.Server/Access/Components/IdCardConsoleComponent.cs b/Content.Server/Access/Components/IdCardConsoleComponent.cs index 87f0085931..4b9bb5c397 100644 --- a/Content.Server/Access/Components/IdCardConsoleComponent.cs +++ b/Content.Server/Access/Components/IdCardConsoleComponent.cs @@ -256,6 +256,8 @@ namespace Content.Server.Access.Components [Verb] public sealed class EjectPrivilegedIDVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, IdCardConsoleComponent component, VerbData data) { if (!EntitySystem.Get().CanInteract(user)) @@ -281,6 +283,8 @@ namespace Content.Server.Access.Components public sealed class EjectTargetIDVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, IdCardConsoleComponent component, VerbData data) { if (!EntitySystem.Get().CanInteract(user)) diff --git a/Content.Server/Administration/Verbs/AdminAddReagentVerb.cs b/Content.Server/Administration/Verbs/AdminAddReagentVerb.cs index dae951605b..cd1375a45c 100644 --- a/Content.Server/Administration/Verbs/AdminAddReagentVerb.cs +++ b/Content.Server/Administration/Verbs/AdminAddReagentVerb.cs @@ -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; diff --git a/Content.Server/Cabinet/ItemCabinetComponent.cs b/Content.Server/Cabinet/ItemCabinetComponent.cs index 89eb94363b..3490dcbca0 100644 --- a/Content.Server/Cabinet/ItemCabinetComponent.cs +++ b/Content.Server/Cabinet/ItemCabinetComponent.cs @@ -75,6 +75,11 @@ namespace Content.Server.Cabinet [Verb] public sealed class ToggleItemCabinetVerb : Verb { + // 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().CanInteract(user)) diff --git a/Content.Server/Chemistry/Components/ChemMasterComponent.cs b/Content.Server/Chemistry/Components/ChemMasterComponent.cs index 55d8a45600..940135fb21 100644 --- a/Content.Server/Chemistry/Components/ChemMasterComponent.cs +++ b/Content.Server/Chemistry/Components/ChemMasterComponent.cs @@ -426,6 +426,8 @@ namespace Content.Server.Chemistry.Components [Verb] public sealed class EjectBeakerVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, ChemMasterComponent component, VerbData data) { if (!EntitySystem.Get().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) diff --git a/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs b/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs index 03e239e5c9..3752c10a14 100644 --- a/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs +++ b/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs @@ -368,6 +368,8 @@ namespace Content.Server.Chemistry.Components [Verb] public sealed class EjectBeakerVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, ReagentDispenserComponent component, VerbData data) { if (!EntitySystem.Get().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) diff --git a/Content.Server/Chemistry/Components/SolutionTransferComponent.cs b/Content.Server/Chemistry/Components/SolutionTransferComponent.cs index 74fca84068..421841fa67 100644 --- a/Content.Server/Chemistry/Components/SolutionTransferComponent.cs +++ b/Content.Server/Chemistry/Components/SolutionTransferComponent.cs @@ -282,6 +282,8 @@ namespace Content.Server.Chemistry.Components [Verb] public sealed class CustomTransferVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, SolutionTransferComponent component, VerbData data) { if (!EntitySystem.Get().CanInteract(user) || !component.CanChangeTransferAmount) diff --git a/Content.Server/Climbing/Components/ClimbableComponent.cs b/Content.Server/Climbing/Components/ClimbableComponent.cs index e8033f269d..ba899056db 100644 --- a/Content.Server/Climbing/Components/ClimbableComponent.cs +++ b/Content.Server/Climbing/Components/ClimbableComponent.cs @@ -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 { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, ClimbableComponent component, VerbData data) { if (!component.CanVault(user, component.Owner, out var _)) diff --git a/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs b/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs index 80ccaf411c..f14f0999b7 100644 --- a/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs +++ b/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs @@ -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 + { + public override bool AlternativeInteraction => true; + + protected override void GetData(IEntity user, DisposalUnitComponent component, VerbData data) + { + data.Visibility = VerbVisibility.Invisible; + + if (!EntitySystem.Get().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().TryEjectContents(component); + } + } + void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs) { EntitySystem.Get().TryEjectContents(this); diff --git a/Content.Server/Hands/Components/HandsComponent.cs b/Content.Server/Hands/Components/HandsComponent.cs index 4fdb02afc2..a044676de3 100644 --- a/Content.Server/Hands/Components/HandsComponent.cs +++ b/Content.Server/Hands/Components/HandsComponent.cs @@ -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() - .TryUseInteraction(Owner, heldEntity); + .TryUseInteraction(Owner, heldEntity, altInteract); } protected override void HandlePickupAnimation(IEntity entity) diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index 7e6adca66c..07fcd9712c 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -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) diff --git a/Content.Server/Interaction/InRangeUnoccludedVerb.cs b/Content.Server/Interaction/InRangeUnoccludedVerb.cs index c61815f64f..dd59bd9956 100644 --- a/Content.Server/Interaction/InRangeUnoccludedVerb.cs +++ b/Content.Server/Interaction/InRangeUnoccludedVerb.cs @@ -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; } diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index 6908b146b5..a38fd26192 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -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(HandleDragDropRequestEvent); + SubscribeNetworkEvent(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 + /// + /// 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. + /// + 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) + /// + /// Resolves user interactions with objects. + /// + /// + /// Checks Whether combat mode is enabled and whether the user can actually interact with the given entity. + /// + /// 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. + 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); } + /// + /// Alternative interactions on an entity. + /// + /// + /// 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 + /// + 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; + } + } + /// /// 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 /// /// /// - 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); } } diff --git a/Content.Server/Items/ItemComponent.cs b/Content.Server/Items/ItemComponent.cs index 3b8a17bfb9..02d5f8eeca 100644 --- a/Content.Server/Items/ItemComponent.cs +++ b/Content.Server/Items/ItemComponent.cs @@ -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) diff --git a/Content.Server/Lock/LockComponent.cs b/Content.Server/Lock/LockComponent.cs index ad9f25afe8..8d1d94c7e0 100644 --- a/Content.Server/Lock/LockComponent.cs +++ b/Content.Server/Lock/LockComponent.cs @@ -26,6 +26,8 @@ namespace Content.Server.Storage.Components [Verb] private sealed class ToggleLockVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, LockComponent component, VerbData data) { if (!EntitySystem.Get().CanInteract(user) || diff --git a/Content.Server/Medical/Components/MedicalScannerComponent.cs b/Content.Server/Medical/Components/MedicalScannerComponent.cs index dc1ca68355..3df1cc4d6d 100644 --- a/Content.Server/Medical/Components/MedicalScannerComponent.cs +++ b/Content.Server/Medical/Components/MedicalScannerComponent.cs @@ -228,6 +228,8 @@ namespace Content.Server.Medical.Components [Verb] public sealed class EjectVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, MedicalScannerComponent component, VerbData data) { if (!EntitySystem.Get().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) diff --git a/Content.Server/PDA/PDAComponent.cs b/Content.Server/PDA/PDAComponent.cs index 6f528af652..a4bbfe07c1 100644 --- a/Content.Server/PDA/PDAComponent.cs +++ b/Content.Server/PDA/PDAComponent.cs @@ -375,6 +375,8 @@ namespace Content.Server.PDA [Verb] public sealed class EjectIDVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, PDAComponent component, VerbData data) { if (!EntitySystem.Get().CanInteract(user)) diff --git a/Content.Server/Power/Components/BaseCharger.cs b/Content.Server/Power/Components/BaseCharger.cs index 7177f9663b..2b5544581d 100644 --- a/Content.Server/Power/Components/BaseCharger.cs +++ b/Content.Server/Power/Components/BaseCharger.cs @@ -162,6 +162,8 @@ namespace Content.Server.Power.Components [Verb] private sealed class EjectVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, BaseCharger component, VerbData data) { if (!EntitySystem.Get().CanInteract(user)) diff --git a/Content.Server/PowerCell/Components/PowerCellSlotComponent.cs b/Content.Server/PowerCell/Components/PowerCellSlotComponent.cs index 97decb4d4f..f5b503ce91 100644 --- a/Content.Server/PowerCell/Components/PowerCellSlotComponent.cs +++ b/Content.Server/PowerCell/Components/PowerCellSlotComponent.cs @@ -179,6 +179,8 @@ namespace Content.Server.PowerCell.Components [Verb] public sealed class EjectCellVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, PowerCellSlotComponent component, VerbData data) { if (!component.ShowVerb || !EntitySystem.Get().CanInteract(user)) diff --git a/Content.Server/Storage/Components/ServerStorageComponent.cs b/Content.Server/Storage/Components/ServerStorageComponent.cs index a7ecb44342..a95546a55d 100644 --- a/Content.Server/Storage/Components/ServerStorageComponent.cs +++ b/Content.Server/Storage/Components/ServerStorageComponent.cs @@ -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 + { + public override bool AlternativeInteraction => true; + + protected override void GetData(IEntity user, ServerStorageComponent component, VerbData data) + { + if (!EntitySystem.Get().CanInteract(user)) + { + data.Visibility = VerbVisibility.Invisible; + return; + } + + // Get the session for the user + var session = user.GetComponentOrNull()?.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"; + } + } + + /// + protected override void Activate(IEntity user, ServerStorageComponent component) + { + // "Open" actually closes the UI if it is already open. + component.OpenStorageUI(user); + } + } } } diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs index 7da24a07df..bc47b11bb9 100644 --- a/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs +++ b/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs @@ -290,6 +290,8 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components [Verb] public sealed class EjectCellVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, ServerBatteryBarrelComponent component, VerbData data) { if (!EntitySystem.Get().CanInteract(user) || !component._powerCellRemovable) diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/ServerMagazineBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/ServerMagazineBarrelComponent.cs index e176a03e24..3629789f7a 100644 --- a/Content.Server/Weapon/Ranged/Barrels/Components/ServerMagazineBarrelComponent.cs +++ b/Content.Server/Weapon/Ranged/Barrels/Components/ServerMagazineBarrelComponent.cs @@ -429,6 +429,8 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components [Verb] private sealed class EjectMagazineVerb : Verb { + public override bool AlternativeInteraction => true; + protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data) { if (!EntitySystem.Get().CanInteract(user)) diff --git a/Content.Shared/Hands/Components/SharedHandsComponent.cs b/Content.Shared/Hands/Components/SharedHandsComponent.cs index e98459a464..d698336476 100644 --- a/Content.Shared/Hands/Components/SharedHandsComponent.cs +++ b/Content.Shared/Hands/Components/SharedHandsComponent.cs @@ -674,12 +674,12 @@ namespace Content.Shared.Hands.Components DoInteraction(activeHeldEntity, heldEntity); } - public void UseActiveHeldEntity() + public void UseActiveHeldEntity(bool altInteract = false) { if (!TryGetActiveHeldEntity(out var heldEntity)) return; - DoUse(heldEntity); + DoUse(heldEntity, altInteract); } public void ActivateHeldEntity(string handName) @@ -783,7 +783,7 @@ namespace Content.Shared.Hands.Components protected virtual void DoInteraction(IEntity activeHeldEntity, IEntity heldEntity) { } - protected virtual void DoUse(IEntity heldEntity) { } + protected virtual void DoUse(IEntity heldEntity, bool altInteract = false) { } protected virtual void DoActivate(IEntity heldEntity) { } diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index c5327ba431..8876a92b5d 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -7,7 +7,9 @@ namespace Content.Shared.Input { public static readonly BoundKeyFunction WideAttack = "WideAttack"; public static readonly BoundKeyFunction ActivateItemInHand = "ActivateItemInHand"; - public static readonly BoundKeyFunction ActivateItemInWorld = "ActivateItemInWorld"; // default action on world entity + public static readonly BoundKeyFunction AltActivateItemInHand = "AltActivateItemInHand"; + public static readonly BoundKeyFunction ActivateItemInWorld = "ActivateItemInWorld"; + public static readonly BoundKeyFunction AltActivateItemInWorld = "AltActivateItemInWorld"; public static readonly BoundKeyFunction Drop = "Drop"; public static readonly BoundKeyFunction ExamineEntity = "ExamineEntity"; public static readonly BoundKeyFunction FocusChat = "FocusChatInputWindow"; diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index 0d02792593..37721e7791 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Content.Shared.Notification; using Content.Shared.Notification.Managers; @@ -9,6 +10,7 @@ using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Physics.Broadphase; +using Robust.Shared.Serialization; namespace Content.Shared.Interaction { @@ -338,4 +340,27 @@ namespace Content.Shared.Interaction return inRange; } } + + /// + /// Raised when a player attempts to activate an item in an inventory slot or hand slot + /// + [Serializable, NetSerializable] + public class InteractInventorySlotEvent : EntityEventArgs + { + /// + /// Entity that was interacted with. + /// + public EntityUid ItemUid { get; } + + /// + /// Whether the interaction used the alt-modifier to trigger alternative interactions. + /// + public bool AltInteract { get; } + + public InteractInventorySlotEvent(EntityUid itemUid, bool altInteract = false) + { + ItemUid = itemUid; + AltInteract = altInteract; + } + } } diff --git a/Content.Shared/Verbs/VerbBase.cs b/Content.Shared/Verbs/VerbBase.cs index 40d0c0f0b4..830b349da6 100644 --- a/Content.Shared/Verbs/VerbBase.cs +++ b/Content.Shared/Verbs/VerbBase.cs @@ -1,4 +1,4 @@ - + namespace Content.Shared.Verbs { public abstract class VerbBase @@ -15,5 +15,10 @@ namespace Content.Shared.Verbs /// OR the user can be the entity's container /// public virtual bool BlockedByContainers => true; + + /// + /// If true, this verb can be activated by alt-clicking on the entity. + /// + public virtual bool AlternativeInteraction => false; } } diff --git a/Content.Shared/Verbs/VerbCategories.cs b/Content.Shared/Verbs/VerbCategories.cs index 89c268f793..8a0cbed568 100644 --- a/Content.Shared/Verbs/VerbCategories.cs +++ b/Content.Shared/Verbs/VerbCategories.cs @@ -10,7 +10,8 @@ namespace Content.Shared.Verbs ("Debug", "/Textures/Interface/VerbIcons/debug.svg.192dpi.png"); public static readonly VerbCategoryData Rotate = ("Rotate", null); - public static readonly VerbCategoryData Construction = ("Construction", 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"); } diff --git a/Content.Shared/Verbs/VerbData.cs b/Content.Shared/Verbs/VerbData.cs index 20a8edbdee..b76c278a2e 100644 --- a/Content.Shared/Verbs/VerbData.cs +++ b/Content.Shared/Verbs/VerbData.cs @@ -16,6 +16,9 @@ namespace Content.Shared.Verbs /// /// The text that the user sees on the verb button. /// + /// + /// This string is automatically passed through Loc.GetString(). + /// public string Text { get; set; } = string.Empty; /// diff --git a/Resources/Locale/en-US/disposal/mailing/components/disposal-mailing-unit-component.ftl b/Resources/Locale/en-US/disposal/mailing/components/disposal-mailing-unit-component.ftl index 0fcf911167..c47c55725e 100644 --- a/Resources/Locale/en-US/disposal/mailing/components/disposal-mailing-unit-component.ftl +++ b/Resources/Locale/en-US/disposal/mailing/components/disposal-mailing-unit-component.ftl @@ -16,8 +16,12 @@ disposal-mailing-unit-is-valid-interaction-no-hands = You have no hands. ## SelfInsertVerb -self-insert-verb-get-data-text = Jump inside +disposal-self-insert-verb-get-data-text = Jump inside ## FlushVerb -flush-verb-get-data-text = Flush \ No newline at end of file +disposal-flush-verb-get-data-text = Flush + +## EjectVerb + +disposal-eject-verb-get-data-text = Eject diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index 6ee69cb997..1b7328b712 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -72,7 +72,9 @@ ui-options-function-walk = Walk ui-options-function-use = Use ui-options-function-wide-attack = Wide attack ui-options-function-activate-item-in-hand = Activate item in hand +ui-options-function-alt-activate-item-in-hand = Alternative activate item in hand ui-options-function-activate-item-in-world = Activate item in world +ui-options-function-alt-activate-item-in-world = Alternative activate item in world ui-options-function-drop = Drop item ui-options-function-examine-entity = Examine ui-options-function-swap-hands = Swap hands diff --git a/Resources/Locale/en-US/storage/components/storage-component.ftl b/Resources/Locale/en-US/storage/components/storage-component.ftl new file mode 100644 index 0000000000..e93e55363c --- /dev/null +++ b/Resources/Locale/en-US/storage/components/storage-component.ftl @@ -0,0 +1,5 @@ + +## ToggleOpenVerb + +toggle-open-verb-open = Open +toggle-open-verb-close = Close \ No newline at end of file diff --git a/Resources/Textures/Interface/VerbIcons/climb.svg b/Resources/Textures/Interface/VerbIcons/climb.svg new file mode 100644 index 0000000000..f976cb240d --- /dev/null +++ b/Resources/Textures/Interface/VerbIcons/climb.svg @@ -0,0 +1,74 @@ + + + + + + image/svg+xml + + + + + + + Up Arrow + + + + + + diff --git a/Resources/Textures/Interface/VerbIcons/delete_transparent.svg b/Resources/Textures/Interface/VerbIcons/delete_transparent.svg new file mode 100644 index 0000000000..7270336b85 --- /dev/null +++ b/Resources/Textures/Interface/VerbIcons/delete_transparent.svg @@ -0,0 +1,89 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/Resources/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png b/Resources/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png new file mode 100644 index 0000000000..cf6a5bd005 Binary files /dev/null and b/Resources/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png differ diff --git a/Resources/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png.yml b/Resources/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png.yml new file mode 100644 index 0000000000..efbfe16f3a --- /dev/null +++ b/Resources/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png.yml @@ -0,0 +1,3 @@ +#variant of delete.svg, with transparent background +sample: + filter: true diff --git a/Resources/Textures/Interface/VerbIcons/information.svg b/Resources/Textures/Interface/VerbIcons/information.svg new file mode 100644 index 0000000000..b78b5e7b16 --- /dev/null +++ b/Resources/Textures/Interface/VerbIcons/information.svg @@ -0,0 +1,90 @@ + + + + + + image/svg+xml + + + + + + + Information + + + + + + + + diff --git a/Resources/Textures/Interface/VerbIcons/information.svg.192dpi.png b/Resources/Textures/Interface/VerbIcons/information.svg.192dpi.png new file mode 100644 index 0000000000..d4db0e8083 Binary files /dev/null and b/Resources/Textures/Interface/VerbIcons/information.svg.192dpi.png differ diff --git a/Resources/Textures/Interface/VerbIcons/information.svg.192dpi.png.yml b/Resources/Textures/Interface/VerbIcons/information.svg.192dpi.png.yml new file mode 100644 index 0000000000..5c43e23305 --- /dev/null +++ b/Resources/Textures/Interface/VerbIcons/information.svg.192dpi.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/VerbIcons/pickup.svg b/Resources/Textures/Interface/VerbIcons/pickup.svg new file mode 100644 index 0000000000..5a1032dcc9 --- /dev/null +++ b/Resources/Textures/Interface/VerbIcons/pickup.svg @@ -0,0 +1,77 @@ + + + + + + image/svg+xml + + + + + + + Up Arrow + + + + + + diff --git a/Resources/Textures/Interface/VerbIcons/pickup.svg.192dpi.png b/Resources/Textures/Interface/VerbIcons/pickup.svg.192dpi.png new file mode 100644 index 0000000000..be09ad1d1f Binary files /dev/null and b/Resources/Textures/Interface/VerbIcons/pickup.svg.192dpi.png differ diff --git a/Resources/Textures/Interface/VerbIcons/pickup.svg.192dpi.png.yml b/Resources/Textures/Interface/VerbIcons/pickup.svg.192dpi.png.yml new file mode 100644 index 0000000000..5c43e23305 --- /dev/null +++ b/Resources/Textures/Interface/VerbIcons/pickup.svg.192dpi.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Interface/hammer_sca;ed.svg.192dpi.png.yml b/Resources/Textures/Interface/hammer_sca;ed.svg.192dpi.png.yml new file mode 100644 index 0000000000..e1f62434bc --- /dev/null +++ b/Resources/Textures/Interface/hammer_sca;ed.svg.192dpi.png.yml @@ -0,0 +1,3 @@ +#this is just a copy of hammer.svg, but scaled and adjusted slightly so that it can be used as a right-click context menu icon. +sample: + filter: true diff --git a/Resources/Textures/Interface/hammer_scaled.svg b/Resources/Textures/Interface/hammer_scaled.svg new file mode 100644 index 0000000000..16f249d61e --- /dev/null +++ b/Resources/Textures/Interface/hammer_scaled.svg @@ -0,0 +1,76 @@ + + + + + + image/svg+xml + + + + + + + Up Arrow + + + + + + + + + diff --git a/Resources/Textures/Interface/hammer_scaled.svg.192dpi.png b/Resources/Textures/Interface/hammer_scaled.svg.192dpi.png new file mode 100644 index 0000000000..8e24be30e7 Binary files /dev/null and b/Resources/Textures/Interface/hammer_scaled.svg.192dpi.png differ diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 6a91958376..640238fa82 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -109,6 +109,10 @@ binds: - function: ActivateItemInHand type: State key: Z +- function: AltActivateItemInHand + type: State + key: Z + mod1: Alt - function: OpenCharacterMenu type: State key: C @@ -128,6 +132,15 @@ binds: - function: ActivateItemInWorld type: State key: E +- function: AltActivateItemInWorld + type: State + key: MouseLeft + canFocus: true + mod1: Alt +- function: AltActivateItemInWorld # secondary binding + type: State + key: E + mod1: Alt - function: ThrowItemInHand type: State key: Q