Equipment verbs & admin inventory access. (#14315)

This commit is contained in:
Leon Friedrich
2023-03-06 06:12:08 +13:00
committed by GitHub
parent a9b268af49
commit b148bebd60
29 changed files with 499 additions and 141 deletions

View File

@@ -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;
}
}

View 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;
}

View 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)

View 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;
}
}

View 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);
}
}
}

View 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);

View 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.

View 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.

View 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)
{

View 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))

View 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;

View 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) { }

View 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.

View 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;
}

View 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;
}
}

View 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;