]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Equipment verbs & admin inventory access. (#14315)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Sun, 5 Mar 2023 17:12:08 +0000 (06:12 +1300)
committerGitHub <noreply@github.com>
Sun, 5 Mar 2023 17:12:08 +0000 (04:12 +1100)
29 files changed:
Content.Client/Administration/Managers/ClientAdminManager.cs
Content.Client/Inventory/StrippableBoundUserInterface.cs
Content.Client/IoC/ClientContentIoC.cs
Content.Client/Verbs/UI/VerbMenuUIController.cs
Content.Client/Verbs/VerbSystem.cs
Content.Server/Administration/Managers/IAdminManager.cs
Content.Server/Hands/Systems/HandsSystem.cs
Content.Server/IoC/ServerContentIoC.cs
Content.Server/Kitchen/EntitySystems/SharpSystem.cs
Content.Server/Storage/EntitySystems/StorageSystem.cs
Content.Server/Strip/StrippableSystem.cs
Content.Shared/Administration/Managers/ISharedAdminManager.cs [new file with mode: 0644]
Content.Shared/Clothing/Components/ToggleableClothingComponent.cs
Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs
Content.Shared/DoAfter/DoAfterComponent.cs
Content.Shared/DoAfter/SharedDoAfterSystem.cs
Content.Shared/Examine/ExamineSystemShared.Group.cs
Content.Shared/Examine/GroupExamineComponent.cs
Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs
Content.Shared/Interaction/SharedInteractionSystem.cs
Content.Shared/Inventory/InventorySystem.Helpers.cs
Content.Shared/Inventory/InventorySystem.Relay.cs
Content.Shared/Strip/Components/StrippableComponent.cs
Content.Shared/Strip/SharedStrippableSystem.cs
Content.Shared/Verbs/SharedVerbSystem.cs
Content.Shared/Verbs/Verb.cs
Content.Shared/Verbs/VerbEvents.cs
Resources/Locale/en-US/strip/strippable-component.ftl
Resources/Locale/en-US/verbs/verbs.ftl [new file with mode: 0644]

index b5c80e6b80635c3240e4ef209307835c8adb8706..2242ef8cdad6d6baa9badf0a8fdf2ff8443d7af8 100644 (file)
@@ -1,13 +1,16 @@
 using Content.Shared.Administration;
+using Content.Shared.Administration.Managers;
 using Robust.Client.Console;
+using Robust.Client.Player;
 using Robust.Shared.ContentPack;
 using Robust.Shared.Network;
 using Robust.Shared.Utility;
 
 namespace Content.Client.Administration.Managers
 {
-    public sealed class ClientAdminManager : IClientAdminManager, IClientConGroupImplementation, IPostInjectInit
+    public sealed class ClientAdminManager : IClientAdminManager, IClientConGroupImplementation, IPostInjectInit, ISharedAdminManager
     {
+        [Dependency] private readonly IPlayerManager _player = default!;
         [Dependency] private readonly IClientNetManager _netMgr = default!;
         [Dependency] private readonly IClientConGroupController _conGroup = default!;
         [Dependency] private readonly IResourceManager _res = default!;
@@ -111,5 +114,12 @@ namespace Content.Client.Administration.Managers
         {
             _conGroup.Implementation = this;
         }
+
+        public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false)
+        {
+            return uid == _player.LocalPlayer?.ControlledEntity
+                ? _adminData
+                : null;
+        }
     }
 }
index 2d0f0ee5544f289a795ce4a2faf74306eb28661e..83f624c12b2158a4cdbd7cddcb514aff47e5e405 100644 (file)
@@ -5,12 +5,15 @@ using Content.Client.Strip;
 using Content.Client.Stylesheets;
 using Content.Client.UserInterface.Controls;
 using Content.Client.UserInterface.Systems.Hands.Controls;
+using Content.Client.Verbs;
+using Content.Client.Verbs.UI;
 using Content.Shared.Ensnaring.Components;
 using Content.Shared.Hands.Components;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Input;
 using Content.Shared.Inventory;
 using Content.Shared.Strip.Components;
+using Content.Shared.Verbs;
 using JetBrains.Annotations;
 using Robust.Client.GameObjects;
 using Robust.Client.ResourceManagement;
@@ -19,6 +22,7 @@ using Robust.Client.UserInterface.Controls;
 using Robust.Shared.Input;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
 using static Content.Client.Inventory.ClientInventorySystem;
 using static Robust.Client.UserInterface.Control;
 
@@ -31,6 +35,7 @@ namespace Content.Client.Inventory
         
         [Dependency] private readonly IPrototypeManager _protoMan = default!;
         [Dependency] private readonly IEntityManager _entMan = default!;
+        [Dependency] private readonly IUserInterfaceManager _ui = default!;
         private ExamineSystem _examine = default!;
         private InventorySystem _inv = default!;
 
@@ -170,15 +175,16 @@ namespace Content.Client.Inventory
             if (ev.Function == EngineKeyFunctions.Use)
             {
                 SendMessage(new StrippingSlotButtonPressed(slot.SlotName, slot is HandButton));
-            }
-            else if (ev.Function == ContentKeyFunctions.ExamineEntity && slot.Entity != null)
-            {
-                _examine.DoExamine(slot.Entity.Value);
                 return;
             }
 
-            if (ev.Function != EngineKeyFunctions.Use)
+            if (slot.Entity == null)
                 return;
+
+            if (ev.Function == ContentKeyFunctions.ExamineEntity)
+                _examine.DoExamine(slot.Entity.Value);
+            else if (ev.Function == EngineKeyFunctions.UseSecondary)
+                _ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(slot.Entity.Value);
         }
 
         private void AddInventoryButton(string slotId, InventoryTemplatePrototype template, InventoryComponent inv)
index 7eb709c4944b74a696fbb47486163c45c697b25f..bea60fc3f30a4850c9324687964e8c5f03313410 100644 (file)
@@ -18,6 +18,7 @@ using Content.Shared.Administration;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Module;
 using Content.Client.Guidebook;
+using Content.Shared.Administration.Managers;
 
 namespace Content.Client.IoC
 {
@@ -32,6 +33,7 @@ namespace Content.Client.IoC
             IoCManager.Register<IScreenshotHook, ScreenshotHook>();
             IoCManager.Register<IClickMapManager, ClickMapManager>();
             IoCManager.Register<IClientAdminManager, ClientAdminManager>();
+            IoCManager.Register<ISharedAdminManager, ClientAdminManager>();
             IoCManager.Register<EuiManager, EuiManager>();
             IoCManager.Register<IVoteManager, VoteManager>();
             IoCManager.Register<ChangelogManager, ChangelogManager>();
index 9dff8560965a8f49bf70fdeff854b554c824b0e5..62d986c658b3a368a63fb29ffd026a2d5507f599 100644 (file)
@@ -1,5 +1,3 @@
-using System;
-using System.Collections.Generic;
 using System.Linq;
 using Content.Client.CombatMode;
 using Content.Client.ContextMenu.UI;
@@ -9,12 +7,7 @@ using Content.Shared.Verbs;
 using Robust.Client.Player;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controllers;
-using Robust.Shared.GameObjects;
 using Robust.Shared.Input;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
-using Robust.Shared.Maths;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
 
 namespace Content.Client.Verbs.UI
 {
index c73a319571e0c20c5519dc48a5741d29ac49d2b6..c55c3df841261052c76d8410a5dbc2892aec3eca 100644 (file)
@@ -1,11 +1,9 @@
-using Content.Client.CombatMode;
-using Content.Client.ContextMenu.UI;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
 using Content.Client.Examine;
 using Content.Client.Gameplay;
 using Content.Client.Popups;
-using Content.Client.Verbs.UI;
 using Content.Shared.Examine;
-using Content.Shared.GameTicking;
 using Content.Shared.Tag;
 using Content.Shared.Verbs;
 using JetBrains.Annotations;
@@ -15,16 +13,12 @@ using Robust.Client.Player;
 using Robust.Client.State;
 using Robust.Shared.Map;
 using Robust.Shared.Utility;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using Robust.Client.UserInterface;
 
 namespace Content.Client.Verbs
 {
     [UsedImplicitly]
     public sealed class VerbSystem : SharedVerbSystem
     {
-        [Dependency] private readonly CombatModeSystem _combatMode = default!;
         [Dependency] private readonly PopupSystem _popupSystem = default!;
         [Dependency] private readonly ExamineSystem _examineSystem = default!;
         [Dependency] private readonly TagSystem _tagSystem = default!;
index 5b67362721d6db5eac44e54371a3dd2999b037f4..c5cf126f3392bc7721c82d6b6dad72a5ead2d3f9 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Administration;
+using Content.Shared.Administration.Managers;
 using Robust.Server.Player;
 
 
@@ -7,7 +8,7 @@ namespace Content.Server.Administration.Managers
     /// <summary>
     ///     Manages server administrators and their permission flags.
     /// </summary>
-    public interface IAdminManager
+    public interface IAdminManager : ISharedAdminManager
     {
         /// <summary>
         ///     Fired when the permissions of an admin on the server changed.
@@ -47,26 +48,6 @@ namespace Content.Server.Administration.Managers
         /// <returns><see langword="null" /> if the player is not an admin.</returns>
         AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false);
 
-        /// <summary>
-        ///     Gets the admin data for a player, if they are an admin.
-        /// </summary>
-        /// <param name="uid">The entity being controlled by the player.</param>
-        /// <param name="includeDeAdmin">
-        ///     Whether to return admin data for admins that are current de-adminned.
-        /// </param>
-        /// <returns><see langword="null" /> if the player is not an admin.</returns>
-        AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false);
-
-        /// <summary>
-        ///     See if a player has an admin flag.
-        /// </summary>
-        /// <returns>True if the player is and admin and has the specified flags.</returns>
-        bool HasAdminFlag(EntityUid player, AdminFlags flag)
-        {
-            var data = GetAdminData(player);
-            return data != null && data.HasFlag(flag);
-        }
-
         /// <summary>
         ///     See if a player has an admin flag.
         /// </summary>
index dcc9d47289b6ca0ef1fd4d8cbb87f7c22d0d503c..59a653b2aecf7a0259dbf9b28edd9be1df5de52f 100644 (file)
@@ -19,6 +19,7 @@ using Content.Shared.Stacks;
 using Content.Shared.Throwing;
 using JetBrains.Annotations;
 using Robust.Server.Player;
+using Robust.Shared.Configuration;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 using Robust.Shared.Input.Binding;
@@ -42,6 +43,8 @@ namespace Content.Server.Hands.Systems
         [Dependency] private readonly PullingSystem _pullingSystem = default!;
         [Dependency] private readonly ThrowingSystem _throwingSystem = default!;
         [Dependency] private readonly StorageSystem _storageSystem = default!;
+        [Dependency] private readonly ISharedPlayerManager _player = default!;
+        [Dependency] private readonly IConfigurationManager _configuration = default!;
 
         public override void Initialize()
         {
@@ -99,7 +102,7 @@ namespace Content.Server.Hands.Systems
             if (finalPosition.EqualsApprox(initialPosition.Position, tolerance: 0.1f))
                 return;
 
-            var filter = Filter.Pvs(item);
+            var filter = Filter.Pvs(item, entityManager: EntityManager, playerManager: _player, cfgManager: _configuration);
 
             if (exclude != null)
                 filter = filter.RemoveWhereAttachedEntity(entity => entity == exclude);
index 3ab0052b8903b465f881e17dafb7359121d04d89..642b5d88d44528f641507a12a8ed35c937f6354d 100644 (file)
@@ -21,6 +21,7 @@ using Content.Server.ServerUpdates;
 using Content.Server.Voting.Managers;
 using Content.Shared.Administration;
 using Content.Shared.Administration.Logs;
+using Content.Shared.Administration.Managers;
 using Content.Shared.Kitchen;
 using Content.Shared.Module;
 
@@ -41,6 +42,7 @@ namespace Content.Server.IoC
             IoCManager.Register<ServerUpdateManager>();
             IoCManager.Register<IObjectivesManager, ObjectivesManager>();
             IoCManager.Register<IAdminManager, AdminManager>();
+            IoCManager.Register<ISharedAdminManager, AdminManager>();
             IoCManager.Register<EuiManager, EuiManager>();
             IoCManager.Register<IVoteManager, VoteManager>();
             IoCManager.Register<IPlayerLocator, PlayerLocator>();
index 41426d7215a7360a69583c68a2ceada85e8b86c8..4a9e259e2973a4acfe0d6c2f55276ad5e55a4d15 100644 (file)
@@ -123,13 +123,13 @@ public sealed class SharpSystem : EntitySystem
 
     private void OnGetInteractionVerbs(EntityUid uid, ButcherableComponent component, GetVerbsEvent<InteractionVerb> args)
     {
-        if (component.Type != ButcheringType.Knife || args.Hands == null)
+        if (component.Type != ButcheringType.Knife || args.Hands == null || !args.CanAccess || !args.CanInteract)
             return;
 
         bool disabled = false;
         string? message = null;
 
-        if (args.Using is null || !HasComp<SharpComponent>(args.Using))
+        if (!HasComp<SharpComponent>(args.Using))
         {
             disabled = true;
             message = Loc.GetString("butcherable-need-knife",
index 91510e339a3f8fc51ad6f3a4c4243860e4998256..965f928f4c620d60987db19423b03fc484132a56 100644 (file)
@@ -33,6 +33,9 @@ using Content.Shared.DoAfter;
 using Content.Shared.Implants.Components;
 using Content.Shared.Lock;
 using Content.Shared.Movement.Events;
+using Content.Server.Ghost.Components;
+using Content.Server.Administration.Managers;
+using Content.Shared.Administration;
 
 namespace Content.Server.Storage.EntitySystems
 {
@@ -41,6 +44,7 @@ namespace Content.Server.Storage.EntitySystems
     {
         [Dependency] private readonly IGameTiming _gameTiming = default!;
         [Dependency] private readonly IRobustRandom _random = default!;
+        [Dependency] private readonly IAdminManager _admin = default!;
         [Dependency] private readonly ContainerSystem _containerSystem = default!;
         [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
         [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
@@ -94,8 +98,17 @@ namespace Content.Server.Storage.EntitySystems
 
         private void AddOpenUiVerb(EntityUid uid, ServerStorageComponent component, GetVerbsEvent<ActivationVerb> args)
         {
+            bool silent = false;
             if (!args.CanAccess || !args.CanInteract || TryComp<LockComponent>(uid, out var lockComponent) && lockComponent.Locked)
-                return;
+            {
+                // we allow admins to open the storage anyways
+                if (!_admin.HasAdminFlag(args.User, AdminFlags.Admin))
+                    return;
+
+                silent = true;
+            }
+
+            silent |= HasComp<GhostComponent>(args.User);
 
             // Get the session for the user
             if (!TryComp<ActorComponent>(args.User, out var actor))
@@ -106,7 +119,7 @@ namespace Content.Server.Storage.EntitySystems
 
             ActivationVerb verb = new()
             {
-                Act = () => OpenStorageUI(uid, args.User, component)
+                Act = () => OpenStorageUI(uid, args.User, component, silent)
             };
             if (uiOpen)
             {
@@ -583,13 +596,13 @@ namespace Content.Server.Storage.EntitySystems
         ///     Opens the storage UI for an entity
         /// </summary>
         /// <param name="entity">The entity to open the UI for</param>
-        public void OpenStorageUI(EntityUid uid, EntityUid entity, ServerStorageComponent? storageComp = null)
+        public void OpenStorageUI(EntityUid uid, EntityUid entity, ServerStorageComponent? storageComp = null, bool silent = false)
         {
             if (!Resolve(uid, ref storageComp) || !TryComp(entity, out ActorComponent? player))
                 return;
 
-            if (storageComp.StorageOpenSound is not null)
-                _audio.Play(storageComp.StorageOpenSound, Filter.Pvs(uid, entityManager: EntityManager), uid, true, storageComp.StorageOpenSound.Params);
+            if (!silent)
+                _audio.PlayPvs(storageComp.StorageOpenSound, uid);
 
             Logger.DebugS(storageComp.LoggerName, $"Storage (UID {uid}) \"used\" by player session (UID {player.PlayerSession.AttachedEntity}).");
 
index 6c8edee5803e4f73611af3ccf1c62af7dd37576c..3289629055e50e70b9085adfc1204d09655c6581 100644 (file)
@@ -163,9 +163,10 @@ namespace Content.Server.Strip
             if (args.Target == args.User)
                 return;
 
-            if (!TryComp<ActorComponent>(args.User, out var actor))
+            if (!HasComp<ActorComponent>(args.User))
                 return;
 
+            args.Handled = true;
             StartOpeningStripper(args.User, component);
         }
 
@@ -214,12 +215,9 @@ namespace Content.Server.Strip
                 return;
             }
 
-            var userEv = new BeforeStripEvent(slotDef.StripTime);
-            RaiseLocalEvent(user, userEv);
-            var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
-            RaiseLocalEvent(component.Owner, ev);
+            var (time, stealth) = GetStripTimeModifiers(user, component.Owner, slotDef.StripTime);
 
-            var doAfterArgs = new DoAfterEventArgs(user, ev.Time, CancellationToken.None, component.Owner)
+            var doAfterArgs = new DoAfterEventArgs(user, time, CancellationToken.None, component.Owner)
             {
                 ExtraCheck = Check,
                 BreakOnStun = true,
@@ -229,7 +227,7 @@ namespace Content.Server.Strip
                 NeedHand = true,
             };
 
-            if (!ev.Stealth && Check() && userHands.ActiveHandEntity != null)
+            if (!stealth && Check() && userHands.ActiveHandEntity != null)
             {
                 var message = Loc.GetString("strippable-component-alert-owner-insert",
                     ("user", Identity.Entity(user, EntityManager)), ("item", userHands.ActiveHandEntity));
@@ -246,8 +244,6 @@ namespace Content.Server.Strip
 
                 _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has placed the item {ToPrettyString(held):item} in {ToPrettyString(component.Owner):target}'s {slot} slot");
             }
-
-
         }
 
         /// <summary>
@@ -282,12 +278,9 @@ namespace Content.Server.Strip
                 return true;
             }
 
-            var userEv = new BeforeStripEvent(component.HandStripDelay);
-            RaiseLocalEvent(user, userEv);
-            var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
-            RaiseLocalEvent(component.Owner, ev);
+            var (time, stealth) = GetStripTimeModifiers(user, component.Owner, component.HandStripDelay);
 
-            var doAfterArgs = new DoAfterEventArgs(user, ev.Time, CancellationToken.None, component.Owner)
+            var doAfterArgs = new DoAfterEventArgs(user, time, CancellationToken.None, component.Owner)
             {
                 ExtraCheck = Check,
                 BreakOnStun = true,
@@ -297,13 +290,16 @@ namespace Content.Server.Strip
                 NeedHand = true,
             };
 
-            if (Check() && userHands.Hands.TryGetValue(handName, out var handSlot))
+            if (!stealth
+                && Check()
+                && userHands.Hands.TryGetValue(handName, out var handSlot)
+                && handSlot.HeldEntity != null)
             {
-                if (handSlot.HeldEntity != null)
-                {
-                    _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", handSlot.HeldEntity)), component.Owner,
-                        component.Owner, PopupType.Large);
-                }
+                    _popupSystem.PopupEntity(
+                        Loc.GetString("strippable-component-alert-owner-insert",
+                        ("user", Identity.Entity(user, EntityManager)),
+                        ("item", handSlot.HeldEntity)),
+                        component.Owner, component.Owner, PopupType.Large);
             }
 
             var result = await _doAfterSystem.WaitDoAfter(doAfterArgs);
@@ -313,7 +309,7 @@ namespace Content.Server.Strip
                 return;
 
             _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: userHands);
-            _handsSystem.TryPickup(component.Owner, held, handName, checkActionBlocker: false, animateUser: true, handsComp: hands);
+            _handsSystem.TryPickup(component.Owner, held, handName, checkActionBlocker: false, animateUser: true, animate: !stealth, handsComp: hands);
             _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has placed the item {ToPrettyString(held):item} in {ToPrettyString(component.Owner):target}'s hands");
             // hand update will trigger strippable update
         }
@@ -349,12 +345,9 @@ namespace Content.Server.Strip
                 return;
             }
 
-            var userEv = new BeforeStripEvent(slotDef.StripTime);
-            RaiseLocalEvent(user, userEv);
-            var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
-            RaiseLocalEvent(component.Owner, ev);
+            var (time, stealth) = GetStripTimeModifiers(user, component.Owner, slotDef.StripTime);
 
-            var doAfterArgs = new DoAfterEventArgs(user, ev.Time, CancellationToken.None, component.Owner)
+            var doAfterArgs = new DoAfterEventArgs(user, time, CancellationToken.None, component.Owner)
             {
                 ExtraCheck = Check,
                 BreakOnStun = true,
@@ -363,7 +356,7 @@ namespace Content.Server.Strip
                 BreakOnUserMove = true,
             };
 
-            if (!ev.Stealth && Check())
+            if (!stealth && Check())
             {
                 if (slotDef.StripHidden)
                 {
@@ -385,7 +378,7 @@ namespace Content.Server.Strip
                 // Raise a dropped event, so that things like gas tank internals properly deactivate when stripping
                 RaiseLocalEvent(item.Value, new DroppedEvent(user), true);
 
-                _handsSystem.PickupOrDrop(user, item.Value);
+                _handsSystem.PickupOrDrop(user, item.Value, animate: !stealth);
                 _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has stripped the item {ToPrettyString(item.Value):item} from {ToPrettyString(component.Owner):target}");
             }
         }
@@ -418,12 +411,9 @@ namespace Content.Server.Strip
                 return true;
             }
 
-            var userEv = new BeforeStripEvent(component.HandStripDelay);
-            RaiseLocalEvent(user, userEv);
-            var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
-            RaiseLocalEvent(component.Owner, ev);
+            var (time, stealth) = GetStripTimeModifiers(user, component.Owner, component.HandStripDelay);
 
-            var doAfterArgs = new DoAfterEventArgs(user, ev.Time, CancellationToken.None, component.Owner)
+            var doAfterArgs = new DoAfterEventArgs(user, time, CancellationToken.None, component.Owner)
             {
                 ExtraCheck = Check,
                 BreakOnStun = true,
@@ -432,12 +422,16 @@ namespace Content.Server.Strip
                 BreakOnUserMove = true,
             };
 
-            if (Check() && hands.Hands.TryGetValue(handName, out var handSlot))
+            if (!stealth
+                && Check()
+                && hands.Hands.TryGetValue(handName, out var handSlot)
+                && handSlot.HeldEntity != null)
             {
-                if (handSlot.HeldEntity != null)
-                {
-                    _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", handSlot.HeldEntity)), component.Owner, component.Owner);
-                }
+                _popupSystem.PopupEntity(
+                    Loc.GetString("strippable-component-alert-owner",
+                    ("user", Identity.Entity(user, EntityManager)),
+                    ("item", handSlot.HeldEntity)),
+                    component.Owner, component.Owner);
             }
 
             var result = await _doAfterSystem.WaitDoAfter(doAfterArgs);
@@ -447,7 +441,7 @@ namespace Content.Server.Strip
                 return;
 
             _handsSystem.TryDrop(component.Owner, hand, checkActionBlocker: false, handsComp: hands);
-            _handsSystem.PickupOrDrop(user, held, handsComp: userHands);
+            _handsSystem.PickupOrDrop(user, held, handsComp: userHands, animate: !stealth);
             // hand update will trigger strippable update
             _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has stripped the item {ToPrettyString(held):item} from {ToPrettyString(component.Owner):target}");
         }
diff --git a/Content.Shared/Administration/Managers/ISharedAdminManager.cs b/Content.Shared/Administration/Managers/ISharedAdminManager.cs
new file mode 100644 (file)
index 0000000..34a1a41
--- /dev/null
@@ -0,0 +1,47 @@
+namespace Content.Shared.Administration.Managers;
+
+/// <summary>
+///     Manages server administrators and their permission flags.
+/// </summary>
+public interface ISharedAdminManager
+{
+    /// <summary>
+    ///     Gets the admin data for a player, if they are an admin.
+    /// </summary>
+    /// <remarks>
+    ///     When used by the client, this only returns accurate results for the player's own entity.
+    /// </remarks>
+    /// <param name="includeDeAdmin">
+    ///     Whether to return admin data for admins that are current de-adminned.
+    /// </param>
+    /// <returns><see langword="null" /> if the player is not an admin.</returns>
+    AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false);
+
+    /// <summary>
+    ///     See if a player has an admin flag.
+    /// </summary>
+    /// <remarks>
+    ///     When used by the client, this only returns accurate results for the player's own entity.
+    /// </remarks>
+    /// <returns>True if the player is and admin and has the specified flags.</returns>
+    bool HasAdminFlag(EntityUid player, AdminFlags flag)
+    {
+        var data = GetAdminData(player);
+        return data != null && data.HasFlag(flag);
+    }
+
+    /// <summary>
+    ///     Checks if a player is an admin.
+    /// </summary>
+    /// <remarks>
+    ///     When used by the client, this only returns accurate results for the player's own entity.
+    /// </remarks>
+    /// <param name="includeDeAdmin">
+    ///     Whether to return admin data for admins that are current de-adminned.
+    /// </param>
+    /// <returns>true if the player is an admin, false otherwise.</returns>
+    bool IsAdmin(EntityUid uid, bool includeDeAdmin = false)
+    {
+        return GetAdminData(uid, includeDeAdmin) != null;
+    }
+}
index 96536f731954101e98e0b8d8292a6f510349baea..a029ad833c292bcb0f5a77245027ab39c5737d6a 100644 (file)
@@ -57,4 +57,19 @@ public sealed class ToggleableClothingComponent : Component
     /// </summary>
     [DataField("clothingUid")]
     public EntityUid? ClothingUid;
+
+    /// <summary>
+    ///     Time it takes for this clothing to be toggled via the stripping menu verbs. Null prevents the verb from even showing up.
+    /// </summary>
+    [DataField("stripDelay")]
+    public TimeSpan? StripDelay = TimeSpan.FromSeconds(3);
+
+    /// <summary>
+    ///     Text shown in the toggle-clothing verb. Defaults to using the name of the <see cref="ToggleAction"/> action.
+    /// </summary>
+    [DataField("verbText")]
+    public string? VerbText;
+
+    // prevent duplicate doafters
+    public byte? DoAfterId;
 }
index 7d7a65294336e88ed16e723b41593466fe1d1af8..b7ea47dc7b6efdf95d180265878931f4541361f2 100644 (file)
@@ -1,12 +1,16 @@
 using Content.Shared.Actions;
 using Content.Shared.Actions.ActionTypes;
 using Content.Shared.Clothing.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
 using Content.Shared.Popups;
+using Content.Shared.Strip;
+using Content.Shared.Verbs;
 using Robust.Shared.Containers;
-using Robust.Shared.Player;
+using Robust.Shared.Network;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
 
@@ -18,7 +22,10 @@ public sealed class ToggleableClothingSystem : EntitySystem
     [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
     [Dependency] private readonly InventorySystem _inventorySystem = default!;
     [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly SharedStrippableSystem _strippable = default!;
     [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly INetManager _net = default!;
 
     private Queue<EntityUid> _toInsert = new();
 
@@ -36,6 +43,103 @@ public sealed class ToggleableClothingSystem : EntitySystem
         SubscribeLocalEvent<AttachedClothingComponent, InteractHandEvent>(OnInteractHand);
         SubscribeLocalEvent<AttachedClothingComponent, GotUnequippedEvent>(OnAttachedUnequip);
         SubscribeLocalEvent<AttachedClothingComponent, ComponentRemove>(OnRemoveAttached);
+
+        SubscribeLocalEvent<ToggleableClothingComponent, InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>>>(GetRelayedVerbs);
+        SubscribeLocalEvent<ToggleableClothingComponent, GetVerbsEvent<EquipmentVerb>>(OnGetVerbs);
+        SubscribeLocalEvent<AttachedClothingComponent, GetVerbsEvent<EquipmentVerb>>(OnGetAttachedStripVerbsEvent);
+        SubscribeLocalEvent<ToggleableClothingComponent, DoAfterEvent<ToggleClothingEvent>>(OnDoAfterComplete);
+    }
+
+    private void GetRelayedVerbs(EntityUid uid, ToggleableClothingComponent component, InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>> args)
+    {
+        OnGetVerbs(uid, component, args.Args);
+    }
+
+    private void OnGetVerbs(EntityUid uid, ToggleableClothingComponent component, GetVerbsEvent<EquipmentVerb> args)
+    {
+        if (!args.CanAccess || !args.CanInteract || component.ClothingUid == null || component.Container == null)
+            return;
+
+        var text = component.VerbText ?? component.ToggleAction?.DisplayName;
+        if (text == null)
+            return;
+
+        if (!_inventorySystem.InSlotWithFlags(uid, component.RequiredFlags))
+            return;
+
+        var wearer = Transform(uid).ParentUid;
+        if (args.User != wearer && component.StripDelay == null)
+            return;
+
+        var verb = new EquipmentVerb()
+        {
+            Icon = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
+            Text = Loc.GetString(text),
+        };
+
+        if (args.User == wearer)
+        {
+            verb.EventTarget = uid;
+            verb.ExecutionEventArgs = new ToggleClothingEvent() { Performer = args.User };
+        }
+        else
+        {
+            verb.Act = () => StartDoAfter(args.User, uid, Transform(uid).ParentUid, component);
+        }
+
+        args.Verbs.Add(verb);
+    }
+
+    private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, ToggleableClothingComponent component)
+    {
+        // TODO predict do afters & networked clothing toggle.
+        if (_net.IsClient)
+            return;
+
+        if (component.DoAfterId != null || component.StripDelay == null)
+            return;
+
+        var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, (float) component.StripDelay.Value.TotalSeconds);
+
+        if (!stealth)
+        {
+            var popup = Loc.GetString("strippable-component-alert-owner-interact", ("user", Identity.Entity(user, EntityManager)), ("item", item));
+            _popupSystem.PopupEntity(popup, wearer, wearer, PopupType.Large);
+        }
+
+        var args = new DoAfterEventArgs(user, time, default, wearer, item)
+        {
+            BreakOnDamage = true,
+            BreakOnStun = true,
+            BreakOnTargetMove = true,
+            RaiseOnTarget = false,
+            RaiseOnUsed = true,
+            RaiseOnUser = false,
+            // This should just re-use the BUI range checks & cancel the do after if the BUI closes. But that is all
+            // server-side at the moment.
+            // TODO BUI REFACTOR.
+            DistanceThreshold = 2,
+        };
+
+        var doAfter = _doAfter.DoAfter(args, new ToggleClothingEvent() { Performer = user });
+        component.DoAfterId = doAfter.ID;
+    }
+
+    private void OnGetAttachedStripVerbsEvent(EntityUid uid, AttachedClothingComponent component, GetVerbsEvent<EquipmentVerb> args)
+    {
+        // redirect to the attached entity.
+        OnGetVerbs(component.AttachedUid, Comp<ToggleableClothingComponent>(component.AttachedUid), args);
+    }
+
+    private void OnDoAfterComplete(EntityUid uid, ToggleableClothingComponent component, DoAfterEvent<ToggleClothingEvent> args)
+    {
+        DebugTools.Assert(component.DoAfterId == args.Id);
+        component.DoAfterId = null;
+
+        if (args.Cancelled)
+            return;
+
+        OnToggleClothing(uid, component, args.AdditionalData);
     }
 
     public override void Update(float frameTime)
index 38d4622e43aa1a087029bc883005a3f26464991a..b8dc23d3786829c24f13ea0b6e3e295fd1da23b5 100644 (file)
@@ -38,12 +38,14 @@ public sealed class DoAfterComponentState : ComponentState
 public sealed class DoAfterEvent : HandledEntityEventArgs
 {
     public bool Cancelled;
+    public byte Id;
     public readonly DoAfterEventArgs Args;
 
-    public DoAfterEvent(bool cancelled, DoAfterEventArgs args)
+    public DoAfterEvent(bool cancelled, DoAfterEventArgs args, byte id)
     {
         Cancelled = cancelled;
         Args = args;
+        Id = id;
     }
 }
 
@@ -57,13 +59,15 @@ public sealed class DoAfterEvent<T> : HandledEntityEventArgs
 {
     public T AdditionalData;
     public bool Cancelled;
+    public byte Id;
     public readonly DoAfterEventArgs Args;
 
-    public DoAfterEvent(T additionalData, bool cancelled, DoAfterEventArgs args)
+    public DoAfterEvent(T additionalData, bool cancelled, DoAfterEventArgs args, byte id)
     {
         AdditionalData = additionalData;
         Cancelled = cancelled;
         Args = args;
+        Id = id;
     }
 }
 
index 3f73e27c02e79be34cdc0b80ef2ec5eac8b4138e..15cddd29c58a39fd9f1d84c955b5d2a4b0a0498c 100644 (file)
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Content.Shared.Damage;
@@ -7,6 +7,7 @@ using Content.Shared.Mobs;
 using Content.Shared.Stunnable;
 using Robust.Shared.GameStates;
 using Robust.Shared.Timing;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.DoAfter;
 
@@ -25,6 +26,17 @@ public abstract class SharedDoAfterSystem : EntitySystem
         SubscribeLocalEvent<DoAfterComponent, ComponentGetState>(OnDoAfterGetState);
     }
 
+    public bool DoAfterExists(EntityUid uid, DoAfter doAFter, DoAfterComponent? component = null)
+        => DoAfterExists(uid, doAFter.ID, component);
+
+    public bool DoAfterExists(EntityUid uid, byte id, DoAfterComponent? component = null)
+    {
+        if (!Resolve(uid, ref component))
+            return false;
+
+        return component.DoAfters.ContainsKey(id);
+    }
+
     private void Add(EntityUid entity, DoAfterComponent component, DoAfter doAfter)
     {
         doAfter.ID = component.RunningIndex;
@@ -170,11 +182,11 @@ public abstract class SharedDoAfterSystem : EntitySystem
     /// </summary>
     /// <param name="eventArgs">The DoAfterEventArgs</param>
     /// <param name="data">The extra data sent over </param>
-    public void DoAfter<T>(DoAfterEventArgs eventArgs, T data)
+    public DoAfter DoAfter<T>(DoAfterEventArgs eventArgs, T data)
     {
         var doAfter = CreateDoAfter(eventArgs);
-
-        doAfter.Done = cancelled => { Send(data, cancelled, eventArgs); };
+        doAfter.Done = cancelled => { Send(data, cancelled, eventArgs, doAfter.ID); };
+        return doAfter;
     }
 
     /// <summary>
@@ -183,11 +195,11 @@ public abstract class SharedDoAfterSystem : EntitySystem
     ///     Use this if you don't have any extra data to send with the DoAfter
     /// </summary>
     /// <param name="eventArgs">The DoAfterEventArgs</param>
-    public void DoAfter(DoAfterEventArgs eventArgs)
+    public DoAfter DoAfter(DoAfterEventArgs eventArgs)
     {
         var doAfter = CreateDoAfter(eventArgs);
-
-        doAfter.Done = cancelled => { Send(cancelled, eventArgs); };
+        doAfter.Done = cancelled => { Send(cancelled, eventArgs, doAfter.ID); };
+        return doAfter;
     }
 
     private DoAfter CreateDoAfter(DoAfterEventArgs eventArgs)
@@ -351,9 +363,9 @@ public abstract class SharedDoAfterSystem : EntitySystem
     /// </summary>
     /// <param name="cancelled"></param>
     /// <param name="args"></param>
-    private void Send(bool cancelled, DoAfterEventArgs args)
+    private void Send(bool cancelled, DoAfterEventArgs args, byte Id)
     {
-        var ev = new DoAfterEvent(cancelled, args);
+        var ev = new DoAfterEvent(cancelled, args, Id);
 
         RaiseDoAfterEvent(ev, args);
     }
@@ -365,22 +377,29 @@ public abstract class SharedDoAfterSystem : EntitySystem
     /// <param name="cancelled"></param>
     /// <param name="args"></param>
     /// <typeparam name="T"></typeparam>
-    private void Send<T>(T data, bool cancelled, DoAfterEventArgs args)
+    private void Send<T>(T data, bool cancelled, DoAfterEventArgs args, byte id)
     {
-        var ev = new DoAfterEvent<T>(data, cancelled, args);
+        var ev = new DoAfterEvent<T>(data, cancelled, args, id);
 
         RaiseDoAfterEvent(ev, args);
     }
 
     private void RaiseDoAfterEvent<TEvent>(TEvent ev, DoAfterEventArgs args) where TEvent : notnull
     {
-        if (EntityManager.EntityExists(args.User) && args.RaiseOnUser)
+        if (args.RaiseOnUser && Exists(args.User))
             RaiseLocalEvent(args.User, ev, args.Broadcast);
 
-        if (args.Target is { } target && EntityManager.EntityExists(target) && args.RaiseOnTarget)
+        if (args.RaiseOnTarget && args.Target is { } target && Exists(target))
+        {
+            DebugTools.Assert(!args.RaiseOnUser || args.Target != args.User);
+            DebugTools.Assert(!args.RaiseOnUsed || args.Target != args.Used);
             RaiseLocalEvent(target, ev, args.Broadcast);
+        }
 
-        if (args.Used is { } used && EntityManager.EntityExists(used) && args.RaiseOnUsed)
+        if (args.RaiseOnUsed && args.Used is { } used && Exists(used))
+        {
+            DebugTools.Assert(!args.RaiseOnUser || args.Used != args.User);
             RaiseLocalEvent(used, ev, args.Broadcast);
+        }
     }
 }
index 364284e45a83bc1fd938b9c7462cf6ae5a1905b4..78e5ce0d96496339809364c50880e77705749207 100644 (file)
@@ -35,10 +35,10 @@ namespace Content.Shared.Examine
                         SendExamineGroup(args.User, args.Target, group);
                         group.Entries.Clear();
                     },
-                    Text = group.ContextText,
-                    Message = group.HoverMessage,
+                    Text = Loc.GetString(group.ContextText),
+                    Message = Loc.GetString(group.HoverMessage),
                     Category = VerbCategory.Examine,
-                    Icon = new SpriteSpecifier.Texture(new ResourcePath(group.Icon)),
+                    Icon = group.Icon,
                 };
 
                 args.Verbs.Add(examineVerb);
index 626619343d4752145a9e0d66da803a712072f334..91be372bee0e8ce6fcc676d089150d744b79fd72 100644 (file)
@@ -15,6 +15,7 @@ namespace Content.Shared.Examine
         [DataField("group")]
         public List<ExamineGroup> ExamineGroups = new()
         {
+            // TODO Remove hardcoded component names.
             new ExamineGroup()
             {
                 Components = new()
@@ -30,7 +31,7 @@ namespace Content.Shared.Examine
     public sealed class ExamineGroup
     {
         /// <summary>
-        ///     The title of the Examine Group, the .
+        ///     The title of the Examine Group. Localized string that gets added to the examine tooltip.
         /// </summary>
         [DataField("title")]
         [ViewVariables(VVAccess.ReadWrite)]
@@ -42,6 +43,8 @@ namespace Content.Shared.Examine
         [DataField("entries")]
         public List<ExamineEntry> Entries = new();
 
+        // TODO custom type serializer, or just make this work via some other automatic grouping process that doesn't
+        // rely on manually specifying component names in yaml.
         /// <summary>
         ///     A list of all components this ExamineGroup encompasses.
         /// </summary>
@@ -52,13 +55,13 @@ namespace Content.Shared.Examine
         ///     The icon path for the Examine Group.
         /// </summary>
         [DataField("icon")]
-        public string Icon = "/Textures/Interface/examine-star.png";
+        public SpriteSpecifier Icon = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/examine-star.png"));
 
         /// <summary>
         ///     The text shown in the context verb menu.
         /// </summary>
         [DataField("contextText")]
-        public string ContextText = string.Empty;
+        public string ContextText = "verb-examine-group-other";
 
         /// <summary>
         ///     Details shown when hovering over the button.
index 7aa4d1428319adbf341890551b3351ce4adf7756..00bbe11f63f2ebbad5316045dcf95aea9ded2ceb 100644 (file)
@@ -10,10 +10,23 @@ namespace Content.Shared.Hands.EntitySystems;
 
 public abstract partial class SharedHandsSystem : EntitySystem
 {
+    /// <summary>
+    ///     Maximum pickup distance for which the pickup animation plays.
+    /// </summary>
+    public const float MaxAnimationRange = 10;
+
     /// <summary>
     ///     Tries to pick up an entity to a specific hand. If no explicit hand is specified, defaults to using the currently active hand.
     /// </summary>
-    public bool TryPickup(EntityUid uid, EntityUid entity, string? handName = null, bool checkActionBlocker = true, bool animateUser = false, SharedHandsComponent? handsComp = null, ItemComponent? item = null)
+    public bool TryPickup(
+        EntityUid uid,
+        EntityUid entity,
+        string? handName = null,
+        bool checkActionBlocker = true,
+        bool animateUser = false,
+        bool animate = true,
+        SharedHandsComponent? handsComp = null,
+        ItemComponent? item = null)
     {
         if (!Resolve(uid, ref handsComp, false))
             return false;
@@ -25,7 +38,7 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (hand == null)
             return false;
 
-        return TryPickup(uid, entity, hand, checkActionBlocker, animateUser, handsComp, item);
+        return TryPickup(uid, entity, hand, checkActionBlocker, animateUser, animate, handsComp, item);
     }
 
     /// <summary>
@@ -35,7 +48,14 @@ public abstract partial class SharedHandsSystem : EntitySystem
     ///     If one empty hand fails to pick up the item, this will NOT check other hands. If ever hand-specific item
     ///     restrictions are added, there a might need to be a TryPickupAllHands or something like that.
     /// </remarks>
-    public bool TryPickupAnyHand(EntityUid uid, EntityUid entity, bool checkActionBlocker = true, bool animateUser = false, SharedHandsComponent? handsComp = null, ItemComponent? item = null)
+    public bool TryPickupAnyHand(
+        EntityUid uid,
+        EntityUid entity,
+        bool checkActionBlocker = true,
+        bool animateUser = false,
+        bool animate = true,
+        SharedHandsComponent? handsComp = null,
+        ItemComponent? item = null)
     {
         if (!Resolve(uid, ref handsComp, false))
             return false;
@@ -43,10 +63,18 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!TryGetEmptyHand(uid, out var hand, handsComp))
             return false;
 
-        return TryPickup(uid, entity, hand, checkActionBlocker, animateUser, handsComp, item);
+        return TryPickup(uid, entity, hand, checkActionBlocker, animateUser, animate, handsComp, item);
     }
 
-    public bool TryPickup(EntityUid uid, EntityUid entity, Hand hand, bool checkActionBlocker = true, bool animateUser = false, SharedHandsComponent? handsComp = null, ItemComponent? item = null)
+    public bool TryPickup(
+        EntityUid uid,
+        EntityUid entity,
+        Hand hand,
+        bool checkActionBlocker = true,
+        bool animateUser = false,
+        bool animate = true,
+        SharedHandsComponent? handsComp = null,
+        ItemComponent? item = null)
     {
         if (!Resolve(uid, ref handsComp, false))
             return false;
@@ -57,16 +85,19 @@ public abstract partial class SharedHandsSystem : EntitySystem
         if (!CanPickupToHand(uid, entity, hand, checkActionBlocker, handsComp, item))
             return false;
 
-        // animation
-        var xform = Transform(uid);
-        var coordinateEntity = xform.ParentUid.IsValid() ? xform.ParentUid : uid;
-
-        var itemPos = Transform(entity).MapPosition;
-        if (itemPos.MapId == xform.MapID)
+        if (animate)
         {
-            // TODO max range for animation?
-            var initialPosition = EntityCoordinates.FromMap(coordinateEntity, itemPos, EntityManager);
-            PickupAnimation(entity, initialPosition, xform.LocalPosition, animateUser ? null : uid);
+            var xform = Transform(uid);
+            var coordinateEntity = xform.ParentUid.IsValid() ? xform.ParentUid : uid;
+            var itemPos = Transform(entity).MapPosition;
+
+            if (itemPos.MapId == xform.MapID
+                && (itemPos.Position - xform.MapPosition.Position).Length <= MaxAnimationRange
+                && MetaData(entity).VisibilityMask == MetaData(uid).VisibilityMask) // Don't animate aghost pickups.
+            {
+                var initialPosition = EntityCoordinates.FromMap(coordinateEntity, itemPos, EntityManager);
+                PickupAnimation(entity, initialPosition, xform.LocalPosition, animateUser ? null : uid);
+            }
         }
         DoPickup(uid, hand, entity, handsComp);
 
@@ -112,12 +143,19 @@ public abstract partial class SharedHandsSystem : EntitySystem
     /// <summary>
     ///     Puts an item into any hand, preferring the active hand, or puts it on the floor.
     /// </summary>
-    public void PickupOrDrop(EntityUid? uid, EntityUid entity, bool checkActionBlocker = true, bool animateUser = false, SharedHandsComponent? handsComp = null, ItemComponent? item = null)
+    public void PickupOrDrop(
+        EntityUid? uid,
+        EntityUid entity,
+        bool checkActionBlocker = true,
+        bool animateUser = false,
+        bool animate = true,
+        SharedHandsComponent? handsComp = null,
+        ItemComponent? item = null)
     {
         if (uid == null
             || !Resolve(uid.Value, ref handsComp, false)
             || !TryGetEmptyHand(uid.Value, out var hand, handsComp)
-            || !TryPickup(uid.Value, entity, hand, checkActionBlocker, animateUser, handsComp, item))
+            || !TryPickup(uid.Value, entity, hand, checkActionBlocker, animateUser, animate, handsComp, item))
         {
             // TODO make this check upwards for any container, and parent to that.
             // Currently this just checks the direct parent, so items can still teleport through containers.
index a2396ce2c7d1ed3ac9581158bbd85b4d974d6b60..8314d36b125e8cccae7e5be54a42a0299c20d6e4 100644 (file)
@@ -1,7 +1,9 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Content.Shared.ActionBlocker;
+using Content.Shared.Administration;
 using Content.Shared.Administration.Logs;
+using Content.Shared.Administration.Managers;
 using Content.Shared.CombatMode;
 using Content.Shared.Database;
 using Content.Shared.Ghost;
@@ -9,6 +11,7 @@ using Content.Shared.Hands.Components;
 using Content.Shared.Input;
 using Content.Shared.Interaction.Components;
 using Content.Shared.Interaction.Events;
+using Content.Shared.Inventory;
 using Content.Shared.Item;
 using Content.Shared.Movement.Components;
 using Content.Shared.Physics;
@@ -45,6 +48,7 @@ namespace Content.Shared.Interaction
     {
         [Dependency] private readonly IGameTiming _gameTiming = default!;
         [Dependency] private readonly IMapManager _mapManager = default!;
+        [Dependency] private readonly ISharedAdminManager _adminManager = default!;
         [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
         [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
         [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
@@ -55,6 +59,7 @@ namespace Content.Shared.Interaction
         [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
         [Dependency] private readonly UseDelaySystem _useDelay = default!;
         [Dependency] private readonly SharedPullingSystem _pullSystem = default!;
+        [Dependency] private readonly InventorySystem _inventory = default!;
         [Dependency] private readonly IRobustRandom _random = default!;
 
         private const CollisionGroup InRangeUnobstructedMask
@@ -104,7 +109,12 @@ namespace Content.Shared.Interaction
                 return;
             }
 
-            if (!_containerSystem.IsInSameOrParentContainer(user, ev.Target) && !CanAccessViaStorage(user, ev.Target))
+            // Check if the bound entity is accessible. Note that we allow admins to ignore this restriction, so that
+            // they can fiddle with UI's that people can't normally interact with (e.g., placing things directly into
+            // other people's backpacks).
+            if (!_containerSystem.IsInSameOrParentContainer(user, ev.Target)
+                && !CanAccessViaStorage(user, ev.Target)
+                && !_adminManager.HasAdminFlag(user, AdminFlags.Admin))
             {
                 ev.Cancel();
                 return;
@@ -983,6 +993,32 @@ namespace Content.Shared.Interaction
         /// </summary>
         public abstract bool CanAccessViaStorage(EntityUid user, EntityUid target);
 
+        /// <summary>
+        ///     Checks whether an entity currently equipped by another player is accessible to some user. This shouldn't
+        ///     be used as a general interaction check, as these kinda of interactions should generally trigger a
+        ///     do-after and a warning for the other player.
+        /// </summary>
+        public bool CanAccessEquipment(EntityUid user, EntityUid target)
+        {
+            if (Deleted(target))
+                return false;
+
+            if (!_containerSystem.TryGetContainingContainer(target, out var container))
+                return false;
+
+            var wearer = container.Owner;
+            if (!_inventory.TryGetSlot(wearer, container.ID, out var slotDef))
+                return false;
+
+            if (wearer == user)
+                return true;
+
+            if (slotDef.StripHidden)
+                return false;
+
+            return InRangeUnobstructed(user, wearer) && _containerSystem.IsInSameOrParentContainer(user, wearer);
+        }
+
         protected bool ValidateClientInput(ICommonSession? session, EntityCoordinates coords,
             EntityUid uid, [NotNullWhen(true)] out EntityUid? userEntity)
         {
index c2f48939a46f6d126459f5d5839bc41af8529377..eec4e0e0fd4e24ef491dec30546e85e685730c18 100644 (file)
@@ -1,11 +1,32 @@
-using Content.Shared.Clothing.Components;
-using Content.Shared.Item;
+using System.Diagnostics.CodeAnalysis;
 using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Inventory;
 
 public partial class InventorySystem
 {
+    /// <summary>
+    ///     Returns the definition of the inventory slot that the given entity is currently in..
+    /// </summary>
+    public bool TryGetContainingSlot(EntityUid uid, [NotNullWhen(true)] out SlotDefinition? slot)
+    {
+        if (!_containerSystem.TryGetContainingContainer(uid, out var container))
+        {
+            slot = null;
+            return false;
+        }
+
+        return TryGetSlot(container.Owner, container.ID, out slot);
+    }
+
+    /// <summary>
+    ///     Returns true if the given entity is equipped to an inventory slot with the given inventory slot flags.
+    /// </summary>
+    public bool InSlotWithFlags(EntityUid uid, SlotFlags flags)
+    {
+        return TryGetContainingSlot(uid, out var slot) && ((slot.SlotFlags & flags) == flags);
+    }
+
     public bool SpawnItemInSlot(EntityUid uid, string slot, string prototype, bool silent = false, bool force = false, InventoryComponent? inventory = null)
     {
         if (!Resolve(uid, ref inventory, false))
index 6b2c4c4257f13bde7e52b3c29ffcefb493f2b481..a46399797c186c1b38770f3b87131bea4cb442a0 100644 (file)
@@ -7,6 +7,8 @@ using Content.Shared.Radio;
 using Content.Shared.Slippery;
 using Content.Shared.Strip.Components;
 using Content.Shared.Temperature;
+using Content.Shared.Verbs;
+using Robust.Shared.Containers;
 
 namespace Content.Shared.Inventory;
 
@@ -23,6 +25,8 @@ public partial class InventorySystem
         SubscribeLocalEvent<InventoryComponent, SeeIdentityAttemptEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, ModifyChangedTemperatureEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent);
+
+        SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetStrippingVerbs);
     }
 
     protected void RelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, T args) where T : EntityEventArgs, IInventoryRelayEvent
@@ -38,6 +42,33 @@ public partial class InventorySystem
             RaiseLocalEvent(container.ContainedEntity.Value, ev, false);
         }
     }
+
+    private void OnGetStrippingVerbs(EntityUid uid, InventoryComponent component, GetVerbsEvent<EquipmentVerb> args)
+    {
+        // Automatically relay stripping related verbs to all equipped clothing.
+
+        if (!_prototypeManager.TryIndex(component.TemplateId, out InventoryTemplatePrototype? proto))
+            return;
+
+        if (!TryComp(uid, out ContainerManagerComponent? containers))
+            return;
+
+        var ev = new InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>>(args);
+        foreach (var slotDef in proto.Slots)
+        {
+            if (slotDef.StripHidden && args.User != uid)
+                continue;
+
+            if (!containers.TryGetContainer(slotDef.Name, out var container))
+                continue;
+
+            if (container is not ContainerSlot slot || slot.ContainedEntity is not { } ent)
+                continue;
+
+            RaiseLocalEvent(ent, ev);
+        }
+    }
+
 }
 
 /// <summary>
@@ -49,7 +80,7 @@ public partial class InventorySystem
 ///      happens to be a dead mouse. Clothing that wishes to modify movement speed must subscribe to
 ///      InventoryRelayedEvent&lt;RefreshMovementSpeedModifiersEvent&gt;
 /// </remarks>
-public sealed class InventoryRelayedEvent<TEvent> : EntityEventArgs where TEvent : EntityEventArgs, IInventoryRelayEvent
+public sealed class InventoryRelayedEvent<TEvent> : EntityEventArgs where TEvent : EntityEventArgs
 {
     public readonly TEvent Args;
 
index 597795473e5b6cbd04388fb2e0cd72e07416bd31..ba5c50fc0f9c9fb1f311a3c23370cfd5b6a62c29 100644 (file)
@@ -62,6 +62,9 @@ namespace Content.Shared.Strip.Components
     /// <summary>
     /// Used to modify strip times. Raised directed at the user.
     /// </summary>
+    /// <remarks>
+    /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player.
+    /// </remarks>
     public sealed class BeforeStripEvent : BaseBeforeStripEvent
     {
         public BeforeStripEvent(float initialTime, bool stealth = false) : base(initialTime, stealth) { }
@@ -70,6 +73,9 @@ namespace Content.Shared.Strip.Components
     /// <summary>
     /// Used to modify strip times. Raised directed at the target.
     /// </summary>
+    /// <remarks>
+    /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player.
+    /// </remarks>
     public sealed class BeforeGettingStrippedEvent : BaseBeforeStripEvent
     {
         public BeforeGettingStrippedEvent(float initialTime, bool stealth = false) : base(initialTime, stealth) { }
index ec707b51fcf7b2fe37c84e9298c3e727cdab80ef..55aa1febfa859988e501a40ef8a0aabd2af401bf 100644 (file)
@@ -14,6 +14,15 @@ public abstract class SharedStrippableSystem : EntitySystem
         SubscribeLocalEvent<StrippableComponent, DragDropDraggedEvent>(OnDragDrop);
     }
 
+    public (float Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, float initialTime)
+    {
+        var userEv = new BeforeStripEvent(initialTime);
+        RaiseLocalEvent(user, userEv);
+        var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
+        RaiseLocalEvent(target, ev);
+        return (ev.Time, ev.Stealth);
+    }
+
     private void OnDragDrop(EntityUid uid, StrippableComponent component, ref DragDropDraggedEvent args)
     {
         // If the user drags a strippable thing onto themselves.
index 0bfa176b69e4dad575110f36fb0f4d7eb1e68e70..2c38afcb03b50b6148347a5cf1ab5755d3792b6c 100644 (file)
@@ -93,6 +93,7 @@ namespace Content.Shared.Verbs
                 }
             }
 
+            // TODO: fix this garbage and use proper generics or reflection or something else, not this.
             if (types.Contains(typeof(InteractionVerb)))
             {
                 var verbEvent = new GetVerbsEvent<InteractionVerb>(user, target, @using, hands, canInteract, canAccess);
@@ -145,6 +146,14 @@ namespace Content.Shared.Verbs
                 verbs.UnionWith(verbEvent.Verbs);
             }
 
+            if (types.Contains(typeof(EquipmentVerb)))
+            {
+                var access = canAccess || _interactionSystem.CanAccessEquipment(user, target);
+                var verbEvent = new GetVerbsEvent<EquipmentVerb>(user, target, @using, hands, canInteract, access);
+                RaiseLocalEvent(target, verbEvent);
+                verbs.UnionWith(verbEvent.Verbs);
+            }
+
             return verbs;
         }
 
index c55e1c31fc5302e02174bbf49cc355d7ac7f78d9..047dfa5db98d0e607a1861b2b5d06475b48c2c37 100644 (file)
@@ -202,8 +202,9 @@ namespace Content.Shared.Verbs
             return string.Compare(Icon?.ToString(), otherVerb.Icon?.ToString(), StringComparison.CurrentCulture);
         }
 
+        // I hate this. Please somebody allow generics to be networked.
         /// <summary>
-        ///     Collection of all verb types, along with string keys.
+        ///     Collection of all verb types,
         /// </summary>
         /// <remarks>
         ///     Useful when iterating over verb types, though maybe this should be obtained and stored via reflection or
@@ -212,13 +213,14 @@ namespace Content.Shared.Verbs
         /// </remarks>
         public static List<Type> VerbTypes = new()
         {
-            { typeof(Verb) },
-            { typeof(InteractionVerb) },
-            { typeof(UtilityVerb) },
-            { typeof(InnateVerb)},
-            { typeof(AlternativeVerb) },
-            { typeof(ActivationVerb) },
-            { typeof(ExamineVerb) }
+            typeof(Verb),
+            typeof(InteractionVerb),
+            typeof(UtilityVerb),
+            typeof(InnateVerb),
+            typeof(AlternativeVerb),
+            typeof(ActivationVerb),
+            typeof(ExamineVerb),
+            typeof(EquipmentVerb) 
         };
     }
 
@@ -333,4 +335,15 @@ namespace Content.Shared.Verbs
 
         public bool ShowOnExamineTooltip = true;
     }
+
+    /// <summary>
+    ///     Verbs specifically for interactions that occur with equipped entities. These verbs should be accessible via
+    ///     the stripping UI, and may optionally also be accessible via a verb on the equipee if the via inventory relay
+    ///     events.get-verbs event.
+    /// </summary>
+    [Serializable, NetSerializable]
+    public sealed class EquipmentVerb : Verb
+    {
+        public override int TypePriority => 5;
+    }
 }
index ed7de92fff517ebc59dfc1e40159f1559b3a919d..256b401592e94544e5aff40f0de42152f2f7fc5d 100644 (file)
@@ -22,7 +22,7 @@ namespace Content.Shared.Verbs
 
         public readonly bool AdminRequest;
 
-        public RequestServerVerbsEvent(EntityUid entityUid, List<Type> verbTypes, EntityUid? slotOwner = null, bool adminRequest = false)
+        public RequestServerVerbsEvent(EntityUid entityUid, IEnumerable<Type> verbTypes, EntityUid? slotOwner = null, bool adminRequest = false)
         {
             EntityUid = entityUid;
             SlotOwner = slotOwner;
index 0c7746cc38765c58cabc9a86c64d18f6ab61b327..7654b20b03f3ca06742b0408412daf6f55e9b05c 100644 (file)
@@ -10,6 +10,9 @@ strippable-component-alert-owner = {$user} is removing your {$item}!
 strippable-component-alert-owner-hidden = You feel someone fumbling in your {$slot}!
 strippable-component-alert-owner-insert = {$user} is putting {$item} on you!
 
+# generic warning for when a user interacts with your equipped items.
+strippable-component-alert-owner-interact = {$user} is fumbling around with your {$item}!
+
 # StripVerb
 strip-verb-get-data-text = Strip
 
diff --git a/Resources/Locale/en-US/verbs/verbs.ftl b/Resources/Locale/en-US/verbs/verbs.ftl
new file mode 100644 (file)
index 0000000..e39e072
--- /dev/null
@@ -0,0 +1,2 @@
+# Default text that gets shown in the context menu for examining something with a GroupExamineComponent
+verb-examine-group-other = Other
\ No newline at end of file