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!;
{
_conGroup.Implementation = this;
}
+
+ public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false)
+ {
+ return uid == _player.LocalPlayer?.ControlledEntity
+ ? _adminData
+ : null;
+ }
}
}
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;
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;
[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!;
if (ev.Function == EngineKeyFunctions.Use)
{
SendMessage(new StrippingSlotButtonPressed(slot.SlotName, slot is HandButton));
- }
- else if (ev.Function == ContentKeyFunctions.ExamineEntity && slot.Entity != null)
- {
- _examine.DoExamine(slot.Entity.Value);
return;
}
- if (ev.Function != EngineKeyFunctions.Use)
+ if (slot.Entity == null)
return;
+
+ if (ev.Function == ContentKeyFunctions.ExamineEntity)
+ _examine.DoExamine(slot.Entity.Value);
+ else if (ev.Function == EngineKeyFunctions.UseSecondary)
+ _ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(slot.Entity.Value);
}
private void AddInventoryButton(string slotId, InventoryTemplatePrototype template, InventoryComponent inv)
using Content.Shared.Administration.Logs;
using Content.Shared.Module;
using Content.Client.Guidebook;
+using Content.Shared.Administration.Managers;
namespace Content.Client.IoC
{
IoCManager.Register<IScreenshotHook, ScreenshotHook>();
IoCManager.Register<IClickMapManager, ClickMapManager>();
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
+ IoCManager.Register<ISharedAdminManager, ClientAdminManager>();
IoCManager.Register<EuiManager, EuiManager>();
IoCManager.Register<IVoteManager, VoteManager>();
IoCManager.Register<ChangelogManager, ChangelogManager>();
-using System;
-using System.Collections.Generic;
using System.Linq;
using Content.Client.CombatMode;
using Content.Client.ContextMenu.UI;
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
{
-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;
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!;
using Content.Shared.Administration;
+using Content.Shared.Administration.Managers;
using Robust.Server.Player;
/// <summary>
/// Manages server administrators and their permission flags.
/// </summary>
- public interface IAdminManager
+ public interface IAdminManager : ISharedAdminManager
{
/// <summary>
/// Fired when the permissions of an admin on the server changed.
/// <returns><see langword="null" /> if the player is not an admin.</returns>
AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false);
- /// <summary>
- /// Gets the admin data for a player, if they are an admin.
- /// </summary>
- /// <param name="uid">The entity being controlled by the player.</param>
- /// <param name="includeDeAdmin">
- /// Whether to return admin data for admins that are current de-adminned.
- /// </param>
- /// <returns><see langword="null" /> if the player is not an admin.</returns>
- AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false);
-
- /// <summary>
- /// See if a player has an admin flag.
- /// </summary>
- /// <returns>True if the player is and admin and has the specified flags.</returns>
- bool HasAdminFlag(EntityUid player, AdminFlags flag)
- {
- var data = GetAdminData(player);
- return data != null && data.HasFlag(flag);
- }
-
/// <summary>
/// See if a player has an admin flag.
/// </summary>
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;
[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()
{
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);
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;
IoCManager.Register<ServerUpdateManager>();
IoCManager.Register<IObjectivesManager, ObjectivesManager>();
IoCManager.Register<IAdminManager, AdminManager>();
+ IoCManager.Register<ISharedAdminManager, AdminManager>();
IoCManager.Register<EuiManager, EuiManager>();
IoCManager.Register<IVoteManager, VoteManager>();
IoCManager.Register<IPlayerLocator, PlayerLocator>();
private void OnGetInteractionVerbs(EntityUid uid, ButcherableComponent component, GetVerbsEvent<InteractionVerb> args)
{
- if (component.Type != ButcheringType.Knife || args.Hands == null)
+ if (component.Type != ButcheringType.Knife || args.Hands == null || !args.CanAccess || !args.CanInteract)
return;
bool disabled = false;
string? message = null;
- if (args.Using is null || !HasComp<SharpComponent>(args.Using))
+ if (!HasComp<SharpComponent>(args.Using))
{
disabled = true;
message = Loc.GetString("butcherable-need-knife",
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
{
{
[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!;
private void AddOpenUiVerb(EntityUid uid, ServerStorageComponent component, GetVerbsEvent<ActivationVerb> args)
{
+ bool silent = false;
if (!args.CanAccess || !args.CanInteract || TryComp<LockComponent>(uid, out var lockComponent) && lockComponent.Locked)
- return;
+ {
+ // we allow admins to open the storage anyways
+ if (!_admin.HasAdminFlag(args.User, AdminFlags.Admin))
+ return;
+
+ silent = true;
+ }
+
+ silent |= HasComp<GhostComponent>(args.User);
// Get the session for the user
if (!TryComp<ActorComponent>(args.User, out var actor))
ActivationVerb verb = new()
{
- Act = () => OpenStorageUI(uid, args.User, component)
+ Act = () => OpenStorageUI(uid, args.User, component, silent)
};
if (uiOpen)
{
/// Opens the storage UI for an entity
/// </summary>
/// <param name="entity">The entity to open the UI for</param>
- public void OpenStorageUI(EntityUid uid, EntityUid entity, ServerStorageComponent? storageComp = null)
+ public void OpenStorageUI(EntityUid uid, EntityUid entity, ServerStorageComponent? storageComp = null, bool silent = false)
{
if (!Resolve(uid, ref storageComp) || !TryComp(entity, out ActorComponent? player))
return;
- if (storageComp.StorageOpenSound is not null)
- _audio.Play(storageComp.StorageOpenSound, Filter.Pvs(uid, entityManager: EntityManager), uid, true, storageComp.StorageOpenSound.Params);
+ if (!silent)
+ _audio.PlayPvs(storageComp.StorageOpenSound, uid);
Logger.DebugS(storageComp.LoggerName, $"Storage (UID {uid}) \"used\" by player session (UID {player.PlayerSession.AttachedEntity}).");
if (args.Target == args.User)
return;
- if (!TryComp<ActorComponent>(args.User, out var actor))
+ if (!HasComp<ActorComponent>(args.User))
return;
+ args.Handled = true;
StartOpeningStripper(args.User, component);
}
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,
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));
_adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has placed the item {ToPrettyString(held):item} in {ToPrettyString(component.Owner):target}'s {slot} slot");
}
-
-
}
/// <summary>
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,
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);
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
}
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,
BreakOnUserMove = true,
};
- if (!ev.Stealth && Check())
+ if (!stealth && Check())
{
if (slotDef.StripHidden)
{
// 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}");
}
}
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,
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);
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}");
}
--- /dev/null
+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;
+ }
+}
/// </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;
}
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;
[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();
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)
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;
}
}
{
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;
}
}
-using System.Linq;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Shared.Damage;
using Content.Shared.Stunnable;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Shared.DoAfter;
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;
/// </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>
/// 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)
/// </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);
}
/// <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);
+ }
}
}
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);
[DataField("group")]
public List<ExamineGroup> ExamineGroups = new()
{
+ // TODO Remove hardcoded component names.
new ExamineGroup()
{
Components = new()
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)]
[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>
/// 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.
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;
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>
/// 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;
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;
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);
/// <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.
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;
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;
{
[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!;
[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
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;
/// </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)
{
-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))
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;
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
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>
/// 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;
/// <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) { }
/// <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) { }
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.
}
}
+ // 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);
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;
}
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
/// </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)
};
}
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;
+ }
}
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;
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
--- /dev/null
+# 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