Equipment verbs & admin inventory access. (#14315)
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Managers;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Administration.Managers
|
||||
{
|
||||
public sealed class ClientAdminManager : IClientAdminManager, IClientConGroupImplementation, IPostInjectInit
|
||||
public sealed class ClientAdminManager : IClientAdminManager, IClientConGroupImplementation, IPostInjectInit, ISharedAdminManager
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IClientNetManager _netMgr = default!;
|
||||
[Dependency] private readonly IClientConGroupController _conGroup = default!;
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
@@ -111,5 +114,12 @@ namespace Content.Client.Administration.Managers
|
||||
{
|
||||
_conGroup.Implementation = this;
|
||||
}
|
||||
|
||||
public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false)
|
||||
{
|
||||
return uid == _player.LocalPlayer?.ControlledEntity
|
||||
? _adminData
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@ using Content.Client.Strip;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Hands.Controls;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Client.Verbs.UI;
|
||||
using Content.Shared.Ensnaring.Components;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Strip.Components;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
@@ -19,6 +22,7 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Client.Inventory.ClientInventorySystem;
|
||||
using static Robust.Client.UserInterface.Control;
|
||||
|
||||
@@ -31,6 +35,7 @@ namespace Content.Client.Inventory
|
||||
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
private ExamineSystem _examine = default!;
|
||||
private InventorySystem _inv = default!;
|
||||
|
||||
@@ -170,15 +175,16 @@ namespace Content.Client.Inventory
|
||||
if (ev.Function == EngineKeyFunctions.Use)
|
||||
{
|
||||
SendMessage(new StrippingSlotButtonPressed(slot.SlotName, slot is HandButton));
|
||||
}
|
||||
else if (ev.Function == ContentKeyFunctions.ExamineEntity && slot.Entity != null)
|
||||
{
|
||||
_examine.DoExamine(slot.Entity.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.Function != EngineKeyFunctions.Use)
|
||||
if (slot.Entity == null)
|
||||
return;
|
||||
|
||||
if (ev.Function == ContentKeyFunctions.ExamineEntity)
|
||||
_examine.DoExamine(slot.Entity.Value);
|
||||
else if (ev.Function == EngineKeyFunctions.UseSecondary)
|
||||
_ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(slot.Entity.Value);
|
||||
}
|
||||
|
||||
private void AddInventoryButton(string slotId, InventoryTemplatePrototype template, InventoryComponent inv)
|
||||
|
||||
@@ -18,6 +18,7 @@ using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Module;
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Shared.Administration.Managers;
|
||||
|
||||
namespace Content.Client.IoC
|
||||
{
|
||||
@@ -32,6 +33,7 @@ namespace Content.Client.IoC
|
||||
IoCManager.Register<IScreenshotHook, ScreenshotHook>();
|
||||
IoCManager.Register<IClickMapManager, ClickMapManager>();
|
||||
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
|
||||
IoCManager.Register<ISharedAdminManager, ClientAdminManager>();
|
||||
IoCManager.Register<EuiManager, EuiManager>();
|
||||
IoCManager.Register<IVoteManager, VoteManager>();
|
||||
IoCManager.Register<ChangelogManager, ChangelogManager>();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Client.CombatMode;
|
||||
using Content.Client.ContextMenu.UI;
|
||||
@@ -9,12 +7,7 @@ using Content.Shared.Verbs;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Verbs.UI
|
||||
{
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Content.Client.CombatMode;
|
||||
using Content.Client.ContextMenu.UI;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Popups;
|
||||
using Content.Client.Verbs.UI;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
@@ -15,16 +13,12 @@ using Robust.Client.Player;
|
||||
using Robust.Client.State;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Verbs
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class VerbSystem : SharedVerbSystem
|
||||
{
|
||||
[Dependency] private readonly CombatModeSystem _combatMode = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly ExamineSystem _examineSystem = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Managers;
|
||||
using Robust.Server.Player;
|
||||
|
||||
|
||||
@@ -7,7 +8,7 @@ namespace Content.Server.Administration.Managers
|
||||
/// <summary>
|
||||
/// Manages server administrators and their permission flags.
|
||||
/// </summary>
|
||||
public interface IAdminManager
|
||||
public interface IAdminManager : ISharedAdminManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when the permissions of an admin on the server changed.
|
||||
@@ -47,26 +48,6 @@ namespace Content.Server.Administration.Managers
|
||||
/// <returns><see langword="null" /> if the player is not an admin.</returns>
|
||||
AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the admin data for a player, if they are an admin.
|
||||
/// </summary>
|
||||
/// <param name="uid">The entity being controlled by the player.</param>
|
||||
/// <param name="includeDeAdmin">
|
||||
/// Whether to return admin data for admins that are current de-adminned.
|
||||
/// </param>
|
||||
/// <returns><see langword="null" /> if the player is not an admin.</returns>
|
||||
AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false);
|
||||
|
||||
/// <summary>
|
||||
/// See if a player has an admin flag.
|
||||
/// </summary>
|
||||
/// <returns>True if the player is and admin and has the specified flags.</returns>
|
||||
bool HasAdminFlag(EntityUid player, AdminFlags flag)
|
||||
{
|
||||
var data = GetAdminData(player);
|
||||
return data != null && data.HasFlag(flag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See if a player has an admin flag.
|
||||
/// </summary>
|
||||
|
||||
@@ -19,6 +19,7 @@ using Content.Shared.Stacks;
|
||||
using Content.Shared.Throwing;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Input.Binding;
|
||||
@@ -42,6 +43,8 @@ namespace Content.Server.Hands.Systems
|
||||
[Dependency] private readonly PullingSystem _pullingSystem = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
|
||||
[Dependency] private readonly StorageSystem _storageSystem = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _player = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configuration = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -99,7 +102,7 @@ namespace Content.Server.Hands.Systems
|
||||
if (finalPosition.EqualsApprox(initialPosition.Position, tolerance: 0.1f))
|
||||
return;
|
||||
|
||||
var filter = Filter.Pvs(item);
|
||||
var filter = Filter.Pvs(item, entityManager: EntityManager, playerManager: _player, cfgManager: _configuration);
|
||||
|
||||
if (exclude != null)
|
||||
filter = filter.RemoveWhereAttachedEntity(entity => entity == exclude);
|
||||
|
||||
@@ -21,6 +21,7 @@ using Content.Server.ServerUpdates;
|
||||
using Content.Server.Voting.Managers;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Administration.Managers;
|
||||
using Content.Shared.Kitchen;
|
||||
using Content.Shared.Module;
|
||||
|
||||
@@ -41,6 +42,7 @@ namespace Content.Server.IoC
|
||||
IoCManager.Register<ServerUpdateManager>();
|
||||
IoCManager.Register<IObjectivesManager, ObjectivesManager>();
|
||||
IoCManager.Register<IAdminManager, AdminManager>();
|
||||
IoCManager.Register<ISharedAdminManager, AdminManager>();
|
||||
IoCManager.Register<EuiManager, EuiManager>();
|
||||
IoCManager.Register<IVoteManager, VoteManager>();
|
||||
IoCManager.Register<IPlayerLocator, PlayerLocator>();
|
||||
|
||||
@@ -123,13 +123,13 @@ public sealed class SharpSystem : EntitySystem
|
||||
|
||||
private void OnGetInteractionVerbs(EntityUid uid, ButcherableComponent component, GetVerbsEvent<InteractionVerb> args)
|
||||
{
|
||||
if (component.Type != ButcheringType.Knife || args.Hands == null)
|
||||
if (component.Type != ButcheringType.Knife || args.Hands == null || !args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
|
||||
bool disabled = false;
|
||||
string? message = null;
|
||||
|
||||
if (args.Using is null || !HasComp<SharpComponent>(args.Using))
|
||||
if (!HasComp<SharpComponent>(args.Using))
|
||||
{
|
||||
disabled = true;
|
||||
message = Loc.GetString("butcherable-need-knife",
|
||||
|
||||
@@ -33,6 +33,9 @@ using Content.Shared.DoAfter;
|
||||
using Content.Shared.Implants.Components;
|
||||
using Content.Shared.Lock;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Shared.Administration;
|
||||
|
||||
namespace Content.Server.Storage.EntitySystems
|
||||
{
|
||||
@@ -41,6 +44,7 @@ namespace Content.Server.Storage.EntitySystems
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IAdminManager _admin = default!;
|
||||
[Dependency] private readonly ContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
|
||||
@@ -94,9 +98,18 @@ namespace Content.Server.Storage.EntitySystems
|
||||
|
||||
private void AddOpenUiVerb(EntityUid uid, ServerStorageComponent component, GetVerbsEvent<ActivationVerb> args)
|
||||
{
|
||||
bool silent = false;
|
||||
if (!args.CanAccess || !args.CanInteract || TryComp<LockComponent>(uid, out var lockComponent) && lockComponent.Locked)
|
||||
{
|
||||
// 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))
|
||||
return;
|
||||
@@ -106,7 +119,7 @@ namespace Content.Server.Storage.EntitySystems
|
||||
|
||||
ActivationVerb verb = new()
|
||||
{
|
||||
Act = () => OpenStorageUI(uid, args.User, component)
|
||||
Act = () => OpenStorageUI(uid, args.User, component, silent)
|
||||
};
|
||||
if (uiOpen)
|
||||
{
|
||||
@@ -583,13 +596,13 @@ namespace Content.Server.Storage.EntitySystems
|
||||
/// Opens the storage UI for an entity
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to open the UI for</param>
|
||||
public void OpenStorageUI(EntityUid uid, EntityUid entity, ServerStorageComponent? storageComp = null)
|
||||
public void OpenStorageUI(EntityUid uid, EntityUid entity, ServerStorageComponent? storageComp = null, bool silent = false)
|
||||
{
|
||||
if (!Resolve(uid, ref storageComp) || !TryComp(entity, out ActorComponent? player))
|
||||
return;
|
||||
|
||||
if (storageComp.StorageOpenSound is not null)
|
||||
_audio.Play(storageComp.StorageOpenSound, Filter.Pvs(uid, entityManager: EntityManager), uid, true, storageComp.StorageOpenSound.Params);
|
||||
if (!silent)
|
||||
_audio.PlayPvs(storageComp.StorageOpenSound, uid);
|
||||
|
||||
Logger.DebugS(storageComp.LoggerName, $"Storage (UID {uid}) \"used\" by player session (UID {player.PlayerSession.AttachedEntity}).");
|
||||
|
||||
|
||||
@@ -163,9 +163,10 @@ namespace Content.Server.Strip
|
||||
if (args.Target == args.User)
|
||||
return;
|
||||
|
||||
if (!TryComp<ActorComponent>(args.User, out var actor))
|
||||
if (!HasComp<ActorComponent>(args.User))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
StartOpeningStripper(args.User, component);
|
||||
}
|
||||
|
||||
@@ -214,12 +215,9 @@ namespace Content.Server.Strip
|
||||
return;
|
||||
}
|
||||
|
||||
var userEv = new BeforeStripEvent(slotDef.StripTime);
|
||||
RaiseLocalEvent(user, userEv);
|
||||
var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
|
||||
RaiseLocalEvent(component.Owner, ev);
|
||||
var (time, stealth) = GetStripTimeModifiers(user, component.Owner, slotDef.StripTime);
|
||||
|
||||
var doAfterArgs = new DoAfterEventArgs(user, ev.Time, CancellationToken.None, component.Owner)
|
||||
var doAfterArgs = new DoAfterEventArgs(user, time, CancellationToken.None, component.Owner)
|
||||
{
|
||||
ExtraCheck = Check,
|
||||
BreakOnStun = true,
|
||||
@@ -229,7 +227,7 @@ namespace Content.Server.Strip
|
||||
NeedHand = true,
|
||||
};
|
||||
|
||||
if (!ev.Stealth && Check() && userHands.ActiveHandEntity != null)
|
||||
if (!stealth && Check() && userHands.ActiveHandEntity != null)
|
||||
{
|
||||
var message = Loc.GetString("strippable-component-alert-owner-insert",
|
||||
("user", Identity.Entity(user, EntityManager)), ("item", userHands.ActiveHandEntity));
|
||||
@@ -246,8 +244,6 @@ namespace Content.Server.Strip
|
||||
|
||||
_adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has placed the item {ToPrettyString(held):item} in {ToPrettyString(component.Owner):target}'s {slot} slot");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -282,12 +278,9 @@ namespace Content.Server.Strip
|
||||
return true;
|
||||
}
|
||||
|
||||
var userEv = new BeforeStripEvent(component.HandStripDelay);
|
||||
RaiseLocalEvent(user, userEv);
|
||||
var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
|
||||
RaiseLocalEvent(component.Owner, ev);
|
||||
var (time, stealth) = GetStripTimeModifiers(user, component.Owner, component.HandStripDelay);
|
||||
|
||||
var doAfterArgs = new DoAfterEventArgs(user, ev.Time, CancellationToken.None, component.Owner)
|
||||
var doAfterArgs = new DoAfterEventArgs(user, time, CancellationToken.None, component.Owner)
|
||||
{
|
||||
ExtraCheck = Check,
|
||||
BreakOnStun = true,
|
||||
@@ -297,13 +290,16 @@ namespace Content.Server.Strip
|
||||
NeedHand = true,
|
||||
};
|
||||
|
||||
if (Check() && userHands.Hands.TryGetValue(handName, out var handSlot))
|
||||
if (!stealth
|
||||
&& Check()
|
||||
&& userHands.Hands.TryGetValue(handName, out var handSlot)
|
||||
&& handSlot.HeldEntity != null)
|
||||
{
|
||||
if (handSlot.HeldEntity != null)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", handSlot.HeldEntity)), component.Owner,
|
||||
component.Owner, PopupType.Large);
|
||||
}
|
||||
_popupSystem.PopupEntity(
|
||||
Loc.GetString("strippable-component-alert-owner-insert",
|
||||
("user", Identity.Entity(user, EntityManager)),
|
||||
("item", handSlot.HeldEntity)),
|
||||
component.Owner, component.Owner, PopupType.Large);
|
||||
}
|
||||
|
||||
var result = await _doAfterSystem.WaitDoAfter(doAfterArgs);
|
||||
@@ -313,7 +309,7 @@ namespace Content.Server.Strip
|
||||
return;
|
||||
|
||||
_handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: userHands);
|
||||
_handsSystem.TryPickup(component.Owner, held, handName, checkActionBlocker: false, animateUser: true, handsComp: hands);
|
||||
_handsSystem.TryPickup(component.Owner, held, handName, checkActionBlocker: false, animateUser: true, animate: !stealth, handsComp: hands);
|
||||
_adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has placed the item {ToPrettyString(held):item} in {ToPrettyString(component.Owner):target}'s hands");
|
||||
// hand update will trigger strippable update
|
||||
}
|
||||
@@ -349,12 +345,9 @@ namespace Content.Server.Strip
|
||||
return;
|
||||
}
|
||||
|
||||
var userEv = new BeforeStripEvent(slotDef.StripTime);
|
||||
RaiseLocalEvent(user, userEv);
|
||||
var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
|
||||
RaiseLocalEvent(component.Owner, ev);
|
||||
var (time, stealth) = GetStripTimeModifiers(user, component.Owner, slotDef.StripTime);
|
||||
|
||||
var doAfterArgs = new DoAfterEventArgs(user, ev.Time, CancellationToken.None, component.Owner)
|
||||
var doAfterArgs = new DoAfterEventArgs(user, time, CancellationToken.None, component.Owner)
|
||||
{
|
||||
ExtraCheck = Check,
|
||||
BreakOnStun = true,
|
||||
@@ -363,7 +356,7 @@ namespace Content.Server.Strip
|
||||
BreakOnUserMove = true,
|
||||
};
|
||||
|
||||
if (!ev.Stealth && Check())
|
||||
if (!stealth && Check())
|
||||
{
|
||||
if (slotDef.StripHidden)
|
||||
{
|
||||
@@ -385,7 +378,7 @@ namespace Content.Server.Strip
|
||||
// Raise a dropped event, so that things like gas tank internals properly deactivate when stripping
|
||||
RaiseLocalEvent(item.Value, new DroppedEvent(user), true);
|
||||
|
||||
_handsSystem.PickupOrDrop(user, item.Value);
|
||||
_handsSystem.PickupOrDrop(user, item.Value, animate: !stealth);
|
||||
_adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has stripped the item {ToPrettyString(item.Value):item} from {ToPrettyString(component.Owner):target}");
|
||||
}
|
||||
}
|
||||
@@ -418,12 +411,9 @@ namespace Content.Server.Strip
|
||||
return true;
|
||||
}
|
||||
|
||||
var userEv = new BeforeStripEvent(component.HandStripDelay);
|
||||
RaiseLocalEvent(user, userEv);
|
||||
var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
|
||||
RaiseLocalEvent(component.Owner, ev);
|
||||
var (time, stealth) = GetStripTimeModifiers(user, component.Owner, component.HandStripDelay);
|
||||
|
||||
var doAfterArgs = new DoAfterEventArgs(user, ev.Time, CancellationToken.None, component.Owner)
|
||||
var doAfterArgs = new DoAfterEventArgs(user, time, CancellationToken.None, component.Owner)
|
||||
{
|
||||
ExtraCheck = Check,
|
||||
BreakOnStun = true,
|
||||
@@ -432,12 +422,16 @@ namespace Content.Server.Strip
|
||||
BreakOnUserMove = true,
|
||||
};
|
||||
|
||||
if (Check() && hands.Hands.TryGetValue(handName, out var handSlot))
|
||||
if (!stealth
|
||||
&& Check()
|
||||
&& hands.Hands.TryGetValue(handName, out var handSlot)
|
||||
&& handSlot.HeldEntity != null)
|
||||
{
|
||||
if (handSlot.HeldEntity != null)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", handSlot.HeldEntity)), component.Owner, component.Owner);
|
||||
}
|
||||
_popupSystem.PopupEntity(
|
||||
Loc.GetString("strippable-component-alert-owner",
|
||||
("user", Identity.Entity(user, EntityManager)),
|
||||
("item", handSlot.HeldEntity)),
|
||||
component.Owner, component.Owner);
|
||||
}
|
||||
|
||||
var result = await _doAfterSystem.WaitDoAfter(doAfterArgs);
|
||||
@@ -447,7 +441,7 @@ namespace Content.Server.Strip
|
||||
return;
|
||||
|
||||
_handsSystem.TryDrop(component.Owner, hand, checkActionBlocker: false, handsComp: hands);
|
||||
_handsSystem.PickupOrDrop(user, held, handsComp: userHands);
|
||||
_handsSystem.PickupOrDrop(user, held, handsComp: userHands, animate: !stealth);
|
||||
// hand update will trigger strippable update
|
||||
_adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has stripped the item {ToPrettyString(held):item} from {ToPrettyString(component.Owner):target}");
|
||||
}
|
||||
|
||||
@@ -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,17 +85,20 @@ public abstract partial class SharedHandsSystem : EntitySystem
|
||||
if (!CanPickupToHand(uid, entity, hand, checkActionBlocker, handsComp, item))
|
||||
return false;
|
||||
|
||||
// animation
|
||||
if (animate)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var coordinateEntity = xform.ParentUid.IsValid() ? xform.ParentUid : uid;
|
||||
|
||||
var itemPos = Transform(entity).MapPosition;
|
||||
if (itemPos.MapId == xform.MapID)
|
||||
|
||||
if (itemPos.MapId == xform.MapID
|
||||
&& (itemPos.Position - xform.MapPosition.Position).Length <= MaxAnimationRange
|
||||
&& MetaData(entity).VisibilityMask == MetaData(uid).VisibilityMask) // Don't animate aghost pickups.
|
||||
{
|
||||
// TODO max range for animation?
|
||||
var initialPosition = EntityCoordinates.FromMap(coordinateEntity, itemPos, EntityManager);
|
||||
PickupAnimation(entity, initialPosition, xform.LocalPosition, animateUser ? null : uid);
|
||||
}
|
||||
}
|
||||
DoPickup(uid, hand, entity, handsComp);
|
||||
|
||||
return true;
|
||||
@@ -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;
|
||||
|
||||
@@ -10,6 +10,9 @@ strippable-component-alert-owner = {$user} is removing your {$item}!
|
||||
strippable-component-alert-owner-hidden = You feel someone fumbling in your {$slot}!
|
||||
strippable-component-alert-owner-insert = {$user} is putting {$item} on you!
|
||||
|
||||
# generic warning for when a user interacts with your equipped items.
|
||||
strippable-component-alert-owner-interact = {$user} is fumbling around with your {$item}!
|
||||
|
||||
# StripVerb
|
||||
strip-verb-get-data-text = Strip
|
||||
|
||||
|
||||
2
Resources/Locale/en-US/verbs/verbs.ftl
Normal file
2
Resources/Locale/en-US/verbs/verbs.ftl
Normal file
@@ -0,0 +1,2 @@
|
||||
# Default text that gets shown in the context menu for examining something with a GroupExamineComponent
|
||||
verb-examine-group-other = Other
|
||||
Reference in New Issue
Block a user