From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sun, 5 Mar 2023 17:12:08 +0000 (+1300) Subject: Equipment verbs & admin inventory access. (#14315) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=b148bebd603400b02cab36c086183da32225163a;p=space-station-14.git Equipment verbs & admin inventory access. (#14315) --- diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs index b5c80e6b80..2242ef8cda 100644 --- a/Content.Client/Administration/Managers/ClientAdminManager.cs +++ b/Content.Client/Administration/Managers/ClientAdminManager.cs @@ -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; + } } } diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index 2d0f0ee554..83f624c12b 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -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().OpenVerbMenu(slot.Entity.Value); } private void AddInventoryButton(string slotId, InventoryTemplatePrototype template, InventoryComponent inv) diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index 7eb709c494..bea60fc3f3 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -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(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Content.Client/Verbs/UI/VerbMenuUIController.cs b/Content.Client/Verbs/UI/VerbMenuUIController.cs index 9dff856096..62d986c658 100644 --- a/Content.Client/Verbs/UI/VerbMenuUIController.cs +++ b/Content.Client/Verbs/UI/VerbMenuUIController.cs @@ -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 { diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs index c73a319571..c55c3df841 100644 --- a/Content.Client/Verbs/VerbSystem.cs +++ b/Content.Client/Verbs/VerbSystem.cs @@ -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!; diff --git a/Content.Server/Administration/Managers/IAdminManager.cs b/Content.Server/Administration/Managers/IAdminManager.cs index 5b67362721..c5cf126f33 100644 --- a/Content.Server/Administration/Managers/IAdminManager.cs +++ b/Content.Server/Administration/Managers/IAdminManager.cs @@ -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 /// /// Manages server administrators and their permission flags. /// - public interface IAdminManager + public interface IAdminManager : ISharedAdminManager { /// /// Fired when the permissions of an admin on the server changed. @@ -47,26 +48,6 @@ namespace Content.Server.Administration.Managers /// if the player is not an admin. AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false); - /// - /// Gets the admin data for a player, if they are an admin. - /// - /// The entity being controlled by the player. - /// - /// Whether to return admin data for admins that are current de-adminned. - /// - /// if the player is not an admin. - AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false); - - /// - /// See if a player has an admin flag. - /// - /// True if the player is and admin and has the specified flags. - bool HasAdminFlag(EntityUid player, AdminFlags flag) - { - var data = GetAdminData(player); - return data != null && data.HasFlag(flag); - } - /// /// See if a player has an admin flag. /// diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index dcc9d47289..59a653b2ae 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -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); diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 3ab0052b89..642b5d88d4 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -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(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs index 41426d7215..4a9e259e29 100644 --- a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs @@ -123,13 +123,13 @@ public sealed class SharpSystem : EntitySystem private void OnGetInteractionVerbs(EntityUid uid, ButcherableComponent component, GetVerbsEvent 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(args.Using)) + if (!HasComp(args.Using)) { disabled = true; message = Loc.GetString("butcherable-need-knife", diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index 91510e339a..965f928f4c 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -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 args) { + bool silent = false; if (!args.CanAccess || !args.CanInteract || TryComp(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(args.User); // Get the session for the user if (!TryComp(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 /// /// The entity to open the UI for - 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})."); diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 6c8edee580..3289629055 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -163,9 +163,10 @@ namespace Content.Server.Strip if (args.Target == args.User) return; - if (!TryComp(args.User, out var actor)) + if (!HasComp(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"); } - - } /// @@ -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 index 0000000000..34a1a41676 --- /dev/null +++ b/Content.Shared/Administration/Managers/ISharedAdminManager.cs @@ -0,0 +1,47 @@ +namespace Content.Shared.Administration.Managers; + +/// +/// Manages server administrators and their permission flags. +/// +public interface ISharedAdminManager +{ + /// + /// Gets the admin data for a player, if they are an admin. + /// + /// + /// When used by the client, this only returns accurate results for the player's own entity. + /// + /// + /// Whether to return admin data for admins that are current de-adminned. + /// + /// if the player is not an admin. + AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false); + + /// + /// See if a player has an admin flag. + /// + /// + /// When used by the client, this only returns accurate results for the player's own entity. + /// + /// True if the player is and admin and has the specified flags. + bool HasAdminFlag(EntityUid player, AdminFlags flag) + { + var data = GetAdminData(player); + return data != null && data.HasFlag(flag); + } + + /// + /// Checks if a player is an admin. + /// + /// + /// When used by the client, this only returns accurate results for the player's own entity. + /// + /// + /// Whether to return admin data for admins that are current de-adminned. + /// + /// true if the player is an admin, false otherwise. + bool IsAdmin(EntityUid uid, bool includeDeAdmin = false) + { + return GetAdminData(uid, includeDeAdmin) != null; + } +} diff --git a/Content.Shared/Clothing/Components/ToggleableClothingComponent.cs b/Content.Shared/Clothing/Components/ToggleableClothingComponent.cs index 96536f7319..a029ad833c 100644 --- a/Content.Shared/Clothing/Components/ToggleableClothingComponent.cs +++ b/Content.Shared/Clothing/Components/ToggleableClothingComponent.cs @@ -57,4 +57,19 @@ public sealed class ToggleableClothingComponent : Component /// [DataField("clothingUid")] public EntityUid? ClothingUid; + + /// + /// Time it takes for this clothing to be toggled via the stripping menu verbs. Null prevents the verb from even showing up. + /// + [DataField("stripDelay")] + public TimeSpan? StripDelay = TimeSpan.FromSeconds(3); + + /// + /// Text shown in the toggle-clothing verb. Defaults to using the name of the action. + /// + [DataField("verbText")] + public string? VerbText; + + // prevent duplicate doafters + public byte? DoAfterId; } diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index 7d7a652943..b7ea47dc7b 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -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 _toInsert = new(); @@ -36,6 +43,103 @@ public sealed class ToggleableClothingSystem : EntitySystem SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnAttachedUnequip); SubscribeLocalEvent(OnRemoveAttached); + + SubscribeLocalEvent>>(GetRelayedVerbs); + SubscribeLocalEvent>(OnGetVerbs); + SubscribeLocalEvent>(OnGetAttachedStripVerbsEvent); + SubscribeLocalEvent>(OnDoAfterComplete); + } + + private void GetRelayedVerbs(EntityUid uid, ToggleableClothingComponent component, InventoryRelayedEvent> args) + { + OnGetVerbs(uid, component, args.Args); + } + + private void OnGetVerbs(EntityUid uid, ToggleableClothingComponent component, GetVerbsEvent 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 args) + { + // redirect to the attached entity. + OnGetVerbs(component.AttachedUid, Comp(component.AttachedUid), args); + } + + private void OnDoAfterComplete(EntityUid uid, ToggleableClothingComponent component, DoAfterEvent 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) diff --git a/Content.Shared/DoAfter/DoAfterComponent.cs b/Content.Shared/DoAfter/DoAfterComponent.cs index 38d4622e43..b8dc23d378 100644 --- a/Content.Shared/DoAfter/DoAfterComponent.cs +++ b/Content.Shared/DoAfter/DoAfterComponent.cs @@ -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 : 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; } } diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs index 3f73e27c02..15cddd29c5 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs @@ -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(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 /// /// The DoAfterEventArgs /// The extra data sent over - public void DoAfter(DoAfterEventArgs eventArgs, T data) + public DoAfter DoAfter(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; } /// @@ -183,11 +195,11 @@ public abstract class SharedDoAfterSystem : EntitySystem /// Use this if you don't have any extra data to send with the DoAfter /// /// The DoAfterEventArgs - 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 /// /// /// - 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 /// /// /// - private void Send(T data, bool cancelled, DoAfterEventArgs args) + private void Send(T data, bool cancelled, DoAfterEventArgs args, byte id) { - var ev = new DoAfterEvent(data, cancelled, args); + var ev = new DoAfterEvent(data, cancelled, args, id); RaiseDoAfterEvent(ev, args); } private void RaiseDoAfterEvent(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); + } } } diff --git a/Content.Shared/Examine/ExamineSystemShared.Group.cs b/Content.Shared/Examine/ExamineSystemShared.Group.cs index 364284e45a..78e5ce0d96 100644 --- a/Content.Shared/Examine/ExamineSystemShared.Group.cs +++ b/Content.Shared/Examine/ExamineSystemShared.Group.cs @@ -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); diff --git a/Content.Shared/Examine/GroupExamineComponent.cs b/Content.Shared/Examine/GroupExamineComponent.cs index 626619343d..91be372bee 100644 --- a/Content.Shared/Examine/GroupExamineComponent.cs +++ b/Content.Shared/Examine/GroupExamineComponent.cs @@ -15,6 +15,7 @@ namespace Content.Shared.Examine [DataField("group")] public List ExamineGroups = new() { + // TODO Remove hardcoded component names. new ExamineGroup() { Components = new() @@ -30,7 +31,7 @@ namespace Content.Shared.Examine public sealed class ExamineGroup { /// - /// The title of the Examine Group, the . + /// The title of the Examine Group. Localized string that gets added to the examine tooltip. /// [DataField("title")] [ViewVariables(VVAccess.ReadWrite)] @@ -42,6 +43,8 @@ namespace Content.Shared.Examine [DataField("entries")] public List 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. /// /// A list of all components this ExamineGroup encompasses. /// @@ -52,13 +55,13 @@ namespace Content.Shared.Examine /// The icon path for the Examine Group. /// [DataField("icon")] - public string Icon = "/Textures/Interface/examine-star.png"; + public SpriteSpecifier Icon = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/examine-star.png")); /// /// The text shown in the context verb menu. /// [DataField("contextText")] - public string ContextText = string.Empty; + public string ContextText = "verb-examine-group-other"; /// /// Details shown when hovering over the button. diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs index 7aa4d14283..00bbe11f63 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs @@ -10,10 +10,23 @@ namespace Content.Shared.Hands.EntitySystems; public abstract partial class SharedHandsSystem : EntitySystem { + /// + /// Maximum pickup distance for which the pickup animation plays. + /// + public const float MaxAnimationRange = 10; + /// /// Tries to pick up an entity to a specific hand. If no explicit hand is specified, defaults to using the currently active hand. /// - 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); } /// @@ -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. /// - 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 /// /// Puts an item into any hand, preferring the active hand, or puts it on the floor. /// - 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. diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index a2396ce2c7..8314d36b12 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -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 /// public abstract bool CanAccessViaStorage(EntityUid user, EntityUid target); + /// + /// 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. + /// + 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) { diff --git a/Content.Shared/Inventory/InventorySystem.Helpers.cs b/Content.Shared/Inventory/InventorySystem.Helpers.cs index c2f48939a4..eec4e0e0fd 100644 --- a/Content.Shared/Inventory/InventorySystem.Helpers.cs +++ b/Content.Shared/Inventory/InventorySystem.Helpers.cs @@ -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 { + /// + /// Returns the definition of the inventory slot that the given entity is currently in.. + /// + 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); + } + + /// + /// Returns true if the given entity is equipped to an inventory slot with the given inventory slot flags. + /// + 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)) diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index 6b2c4c4257..a46399797c 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -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(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); + + SubscribeLocalEvent>(OnGetStrippingVerbs); } protected void RelayInventoryEvent(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 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>(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); + } + } + } /// @@ -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<RefreshMovementSpeedModifiersEvent> /// -public sealed class InventoryRelayedEvent : EntityEventArgs where TEvent : EntityEventArgs, IInventoryRelayEvent +public sealed class InventoryRelayedEvent : EntityEventArgs where TEvent : EntityEventArgs { public readonly TEvent Args; diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs index 597795473e..ba5c50fc0f 100644 --- a/Content.Shared/Strip/Components/StrippableComponent.cs +++ b/Content.Shared/Strip/Components/StrippableComponent.cs @@ -62,6 +62,9 @@ namespace Content.Shared.Strip.Components /// /// Used to modify strip times. Raised directed at the user. /// + /// + /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. + /// public sealed class BeforeStripEvent : BaseBeforeStripEvent { public BeforeStripEvent(float initialTime, bool stealth = false) : base(initialTime, stealth) { } @@ -70,6 +73,9 @@ namespace Content.Shared.Strip.Components /// /// Used to modify strip times. Raised directed at the target. /// + /// + /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. + /// public sealed class BeforeGettingStrippedEvent : BaseBeforeStripEvent { public BeforeGettingStrippedEvent(float initialTime, bool stealth = false) : base(initialTime, stealth) { } diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs index ec707b51fc..55aa1febfa 100644 --- a/Content.Shared/Strip/SharedStrippableSystem.cs +++ b/Content.Shared/Strip/SharedStrippableSystem.cs @@ -14,6 +14,15 @@ public abstract class SharedStrippableSystem : EntitySystem SubscribeLocalEvent(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. diff --git a/Content.Shared/Verbs/SharedVerbSystem.cs b/Content.Shared/Verbs/SharedVerbSystem.cs index 0bfa176b69..2c38afcb03 100644 --- a/Content.Shared/Verbs/SharedVerbSystem.cs +++ b/Content.Shared/Verbs/SharedVerbSystem.cs @@ -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(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(user, target, @using, hands, canInteract, access); + RaiseLocalEvent(target, verbEvent); + verbs.UnionWith(verbEvent.Verbs); + } + return verbs; } diff --git a/Content.Shared/Verbs/Verb.cs b/Content.Shared/Verbs/Verb.cs index c55e1c31fc..047dfa5db9 100644 --- a/Content.Shared/Verbs/Verb.cs +++ b/Content.Shared/Verbs/Verb.cs @@ -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. /// - /// Collection of all verb types, along with string keys. + /// Collection of all verb types, /// /// /// 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 /// public static List 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; } + + /// + /// 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. + /// + [Serializable, NetSerializable] + public sealed class EquipmentVerb : Verb + { + public override int TypePriority => 5; + } } diff --git a/Content.Shared/Verbs/VerbEvents.cs b/Content.Shared/Verbs/VerbEvents.cs index ed7de92fff..256b401592 100644 --- a/Content.Shared/Verbs/VerbEvents.cs +++ b/Content.Shared/Verbs/VerbEvents.cs @@ -22,7 +22,7 @@ namespace Content.Shared.Verbs public readonly bool AdminRequest; - public RequestServerVerbsEvent(EntityUid entityUid, List verbTypes, EntityUid? slotOwner = null, bool adminRequest = false) + public RequestServerVerbsEvent(EntityUid entityUid, IEnumerable verbTypes, EntityUid? slotOwner = null, bool adminRequest = false) { EntityUid = entityUid; SlotOwner = slotOwner; diff --git a/Resources/Locale/en-US/strip/strippable-component.ftl b/Resources/Locale/en-US/strip/strippable-component.ftl index 0c7746cc38..7654b20b03 100644 --- a/Resources/Locale/en-US/strip/strippable-component.ftl +++ b/Resources/Locale/en-US/strip/strippable-component.ftl @@ -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 index 0000000000..e39e0727f0 --- /dev/null +++ b/Resources/Locale/en-US/verbs/verbs.ftl @@ -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