Equipment verbs & admin inventory access. (#14315)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<RefreshMovementSpeedModifiersEvent>
|
||||
/// </remarks>
|
||||
public sealed class InventoryRelayedEvent<TEvent> : EntityEventArgs where TEvent : EntityEventArgs, IInventoryRelayEvent
|
||||
public sealed class InventoryRelayedEvent<TEvent> : EntityEventArgs where TEvent : EntityEventArgs
|
||||
{
|
||||
public readonly TEvent Args;
|
||||
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user