Cuffable/Handcuff ECS (#14382)
This commit is contained in:
@@ -1,67 +0,0 @@
|
|||||||
using Content.Shared.ActionBlocker;
|
|
||||||
using Content.Shared.Cuffs.Components;
|
|
||||||
using Content.Shared.Humanoid;
|
|
||||||
using Robust.Client.GameObjects;
|
|
||||||
using Robust.Client.Graphics;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.IoC;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
using Robust.Shared.ViewVariables;
|
|
||||||
|
|
||||||
namespace Content.Client.Cuffs.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
[ComponentReference(typeof(SharedCuffableComponent))]
|
|
||||||
public sealed class CuffableComponent : SharedCuffableComponent
|
|
||||||
{
|
|
||||||
[ViewVariables]
|
|
||||||
private string? _currentRSI;
|
|
||||||
|
|
||||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
|
||||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
|
||||||
|
|
||||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
|
||||||
{
|
|
||||||
if (curState is not CuffableComponentState cuffState)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CanStillInteract = cuffState.CanStillInteract;
|
|
||||||
_sysMan.GetEntitySystem<ActionBlockerSystem>().UpdateCanMove(Owner);
|
|
||||||
|
|
||||||
if (_entityManager.TryGetComponent<SpriteComponent>(Owner, out var spriteComponent))
|
|
||||||
{
|
|
||||||
spriteComponent.LayerSetVisible(HumanoidVisualLayers.Handcuffs, cuffState.NumHandsCuffed > 0);
|
|
||||||
spriteComponent.LayerSetColor(HumanoidVisualLayers.Handcuffs, cuffState.Color);
|
|
||||||
|
|
||||||
if (cuffState.NumHandsCuffed > 0)
|
|
||||||
{
|
|
||||||
if (_currentRSI != cuffState.RSI) // we don't want to keep loading the same RSI
|
|
||||||
{
|
|
||||||
_currentRSI = cuffState.RSI;
|
|
||||||
|
|
||||||
if (_currentRSI != null)
|
|
||||||
{
|
|
||||||
spriteComponent.LayerSetState(HumanoidVisualLayers.Handcuffs, new RSI.StateId(cuffState.IconState), new ResourcePath(_currentRSI));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
spriteComponent.LayerSetState(HumanoidVisualLayers.Handcuffs, new RSI.StateId(cuffState.IconState)); // TODO: safety check to see if RSI contains the state?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ev = new CuffedStateChangeEvent();
|
|
||||||
_entityManager.EventBus.RaiseLocalEvent(Owner, ref ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnRemove()
|
|
||||||
{
|
|
||||||
base.OnRemove();
|
|
||||||
if (_entityManager.TryGetComponent<SpriteComponent>(Owner, out var spriteComponent))
|
|
||||||
spriteComponent.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using Content.Shared.Cuffs.Components;
|
|
||||||
using Robust.Client.GameObjects;
|
|
||||||
using Robust.Client.Graphics;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.IoC;
|
|
||||||
|
|
||||||
namespace Content.Client.Cuffs.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
[ComponentReference(typeof(SharedHandcuffComponent))]
|
|
||||||
public sealed class HandcuffComponent : SharedHandcuffComponent
|
|
||||||
{
|
|
||||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
|
||||||
{
|
|
||||||
if (curState is not HandcuffedComponentState state)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.IconState == string.Empty)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<SpriteComponent?>(Owner, out var sprite))
|
|
||||||
{
|
|
||||||
sprite.LayerSetState(0, new RSI.StateId(state.IconState)); // TODO: safety check to see if RSI contains the state?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,79 @@
|
|||||||
|
using Content.Shared.ActionBlocker;
|
||||||
using Content.Shared.Cuffs;
|
using Content.Shared.Cuffs;
|
||||||
|
using Content.Shared.Cuffs.Components;
|
||||||
|
using Content.Shared.Humanoid;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
namespace Content.Client.Cuffs
|
namespace Content.Client.Cuffs;
|
||||||
|
|
||||||
|
public sealed class CuffableSystem : SharedCuffableSystem
|
||||||
{
|
{
|
||||||
public sealed class CuffableSystem : SharedCuffableSystem
|
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||||
{
|
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<CuffableComponent, ComponentShutdown>(OnShutdown);
|
||||||
|
SubscribeLocalEvent<CuffableComponent, ComponentHandleState>(OnCuffableHandleState);
|
||||||
|
SubscribeLocalEvent<HandcuffComponent, ComponentHandleState>(OnHandcuffHandleState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnShutdown(EntityUid uid, CuffableComponent component, ComponentShutdown args)
|
||||||
|
{
|
||||||
|
if (TryComp<SpriteComponent>(uid, out var sprite))
|
||||||
|
sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHandcuffHandleState(EntityUid uid, HandcuffComponent component, ref ComponentHandleState args)
|
||||||
|
{
|
||||||
|
if (args.Current is not HandcuffComponentState state)
|
||||||
|
return;
|
||||||
|
|
||||||
|
component.Cuffing = state.Cuffing;
|
||||||
|
|
||||||
|
if (state.IconState == string.Empty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (TryComp<SpriteComponent>(uid, out var sprite))
|
||||||
|
{
|
||||||
|
sprite.LayerSetState(HumanoidVisualLayers.Handcuffs, state.IconState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCuffableHandleState(EntityUid uid, CuffableComponent component, ref ComponentHandleState args)
|
||||||
|
{
|
||||||
|
if (args.Current is not CuffableComponentState cuffState)
|
||||||
|
return;
|
||||||
|
|
||||||
|
component.CanStillInteract = cuffState.CanStillInteract;
|
||||||
|
component.Uncuffing = cuffState.Uncuffing;
|
||||||
|
_actionBlocker.UpdateCanMove(uid);
|
||||||
|
|
||||||
|
var ev = new CuffedStateChangeEvent();
|
||||||
|
RaiseLocalEvent(uid, ref ev);
|
||||||
|
|
||||||
|
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||||
|
return;
|
||||||
|
var cuffed = cuffState.NumHandsCuffed > 0;
|
||||||
|
sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, cuffed);
|
||||||
|
|
||||||
|
// if they are not cuffed, that means that we didn't get a valid color,
|
||||||
|
// iconstate, or RSI. that also means we don't need to update the sprites.
|
||||||
|
if (!cuffed)
|
||||||
|
return;
|
||||||
|
sprite.LayerSetColor(HumanoidVisualLayers.Handcuffs, cuffState.Color!.Value);
|
||||||
|
|
||||||
|
if (!Equals(component.CurrentRSI, cuffState.RSI) && cuffState.RSI != null) // we don't want to keep loading the same RSI
|
||||||
|
{
|
||||||
|
component.CurrentRSI = cuffState.RSI;
|
||||||
|
sprite.LayerSetState(HumanoidVisualLayers.Handcuffs, cuffState.IconState, component.CurrentRSI);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sprite.LayerSetState(HumanoidVisualLayers.Handcuffs, cuffState.IconState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Client.Cuffs.Components;
|
using System.Linq;
|
||||||
|
using Content.Client.Cuffs;
|
||||||
using Content.Client.Examine;
|
using Content.Client.Examine;
|
||||||
using Content.Client.Hands;
|
using Content.Client.Hands;
|
||||||
using Content.Client.Strip;
|
using Content.Client.Strip;
|
||||||
@@ -7,6 +8,8 @@ using Content.Client.UserInterface.Controls;
|
|||||||
using Content.Client.UserInterface.Systems.Hands.Controls;
|
using Content.Client.UserInterface.Systems.Hands.Controls;
|
||||||
using Content.Client.Verbs;
|
using Content.Client.Verbs;
|
||||||
using Content.Client.Verbs.UI;
|
using Content.Client.Verbs.UI;
|
||||||
|
using Content.Shared.Cuffs;
|
||||||
|
using Content.Shared.Cuffs.Components;
|
||||||
using Content.Shared.Ensnaring.Components;
|
using Content.Shared.Ensnaring.Components;
|
||||||
using Content.Shared.Hands.Components;
|
using Content.Shared.Hands.Components;
|
||||||
using Content.Shared.IdentityManagement;
|
using Content.Shared.IdentityManagement;
|
||||||
@@ -16,7 +19,6 @@ using Content.Shared.Strip.Components;
|
|||||||
using Content.Shared.Verbs;
|
using Content.Shared.Verbs;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Client.GameObjects;
|
using Robust.Client.GameObjects;
|
||||||
using Robust.Client.ResourceManagement;
|
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
using Robust.Shared.Input;
|
using Robust.Shared.Input;
|
||||||
@@ -38,6 +40,7 @@ namespace Content.Client.Inventory
|
|||||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||||
private ExamineSystem _examine = default!;
|
private ExamineSystem _examine = default!;
|
||||||
private InventorySystem _inv = default!;
|
private InventorySystem _inv = default!;
|
||||||
|
private readonly SharedCuffableSystem _cuffable;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private StrippingMenu? _strippingMenu;
|
private StrippingMenu? _strippingMenu;
|
||||||
@@ -50,6 +53,7 @@ namespace Content.Client.Inventory
|
|||||||
IoCManager.InjectDependencies(this);
|
IoCManager.InjectDependencies(this);
|
||||||
_examine = _entMan.EntitySysManager.GetEntitySystem<ExamineSystem>();
|
_examine = _entMan.EntitySysManager.GetEntitySystem<ExamineSystem>();
|
||||||
_inv = _entMan.EntitySysManager.GetEntitySystem<InventorySystem>();
|
_inv = _entMan.EntitySysManager.GetEntitySystem<InventorySystem>();
|
||||||
|
_cuffable = _entMan.System<SharedCuffableSystem>();
|
||||||
var title = Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner.Owner, _entMan)));
|
var title = Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner.Owner, _entMan)));
|
||||||
_strippingMenu = new StrippingMenu(title, this);
|
_strippingMenu = new StrippingMenu(title, this);
|
||||||
_strippingMenu.OnClose += Close;
|
_strippingMenu.OnClose += Close;
|
||||||
@@ -159,7 +163,7 @@ namespace Content.Client.Inventory
|
|||||||
if (_entMan.TryGetComponent(hand.HeldEntity, out HandVirtualItemComponent? virt))
|
if (_entMan.TryGetComponent(hand.HeldEntity, out HandVirtualItemComponent? virt))
|
||||||
{
|
{
|
||||||
button.Blocked = true;
|
button.Blocked = true;
|
||||||
if (_entMan.TryGetComponent(Owner.Owner, out CuffableComponent? cuff) && cuff.Container.Contains(virt.BlockingEntity))
|
if (_entMan.TryGetComponent(Owner.Owner, out CuffableComponent? cuff) && _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity))
|
||||||
button.BlockedRect.MouseFilter = MouseFilterMode.Ignore;
|
button.BlockedRect.MouseFilter = MouseFilterMode.Ignore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Cuffs.Components;
|
using Content.Server.Cuffs;
|
||||||
using Content.Server.Hands.Components;
|
using Content.Server.Hands.Components;
|
||||||
using Content.Shared.Body.Components;
|
using Content.Shared.Body.Components;
|
||||||
|
using Content.Shared.Cuffs.Components;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using Robust.Server.Console;
|
using Robust.Server.Console;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
@@ -56,6 +56,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
|
|||||||
var coordinates = new MapCoordinates(Vector2.Zero, mapId);
|
var coordinates = new MapCoordinates(Vector2.Zero, mapId);
|
||||||
|
|
||||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||||
|
var cuffableSys = entityManager.System<CuffableSystem>();
|
||||||
|
|
||||||
// Spawn the entities
|
// Spawn the entities
|
||||||
human = entityManager.SpawnEntity("HumanDummy", coordinates);
|
human = entityManager.SpawnEntity("HumanDummy", coordinates);
|
||||||
@@ -75,19 +76,19 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
|
|||||||
Assert.True(entityManager.TryGetComponent(secondCuffs, out HandcuffComponent? _), $"Second handcuffs has no {nameof(HandcuffComponent)}");
|
Assert.True(entityManager.TryGetComponent(secondCuffs, out HandcuffComponent? _), $"Second handcuffs has no {nameof(HandcuffComponent)}");
|
||||||
|
|
||||||
// Test to ensure cuffed players register the handcuffs
|
// Test to ensure cuffed players register the handcuffs
|
||||||
cuffed.TryAddNewCuffs(human, cuffs);
|
cuffableSys.TryAddNewCuffs(human, human, cuffs, cuffed);
|
||||||
Assert.True(cuffed.CuffedHandCount > 0,
|
Assert.True(cuffed.CuffedHandCount > 0,
|
||||||
"Handcuffing a player did not result in their hands being cuffed");
|
"Handcuffing a player did not result in their hands being cuffed");
|
||||||
|
|
||||||
// Test to ensure a player with 4 hands will still only have 2 hands cuffed
|
// Test to ensure a player with 4 hands will still only have 2 hands cuffed
|
||||||
AddHand(cuffed.Owner);
|
AddHand(human);
|
||||||
AddHand(cuffed.Owner);
|
AddHand(human);
|
||||||
|
|
||||||
Assert.That(cuffed.CuffedHandCount, Is.EqualTo(2));
|
Assert.That(cuffed.CuffedHandCount, Is.EqualTo(2));
|
||||||
Assert.That(hands.SortedHands.Count(), Is.EqualTo(4));
|
Assert.That(hands.SortedHands.Count, Is.EqualTo(4));
|
||||||
|
|
||||||
// Test to give a player with 4 hands 2 sets of cuffs
|
// Test to give a player with 4 hands 2 sets of cuffs
|
||||||
cuffed.TryAddNewCuffs(human, secondCuffs);
|
cuffableSys.TryAddNewCuffs(human, human, secondCuffs, cuffed);
|
||||||
Assert.True(cuffed.CuffedHandCount == 4, "Player doesn't have correct amount of hands cuffed");
|
Assert.True(cuffed.CuffedHandCount == 4, "Player doesn't have correct amount of hands cuffed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Content.Server.Administration.Components;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This is used for forcing someone to be disarmed 100% of the time.
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed class DisarmProneComponent : Component { }
|
|
||||||
@@ -24,6 +24,7 @@ using Content.Server.Tabletop;
|
|||||||
using Content.Server.Tabletop.Components;
|
using Content.Server.Tabletop.Components;
|
||||||
using Content.Server.Tools.Systems;
|
using Content.Server.Tools.Systems;
|
||||||
using Content.Shared.Administration;
|
using Content.Shared.Administration;
|
||||||
|
using Content.Shared.Administration.Components;
|
||||||
using Content.Shared.Body.Components;
|
using Content.Shared.Body.Components;
|
||||||
using Content.Shared.Body.Part;
|
using Content.Shared.Body.Part;
|
||||||
using Content.Shared.Clothing.Components;
|
using Content.Shared.Clothing.Components;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Content.Server.Cuffs.Components;
|
using Content.Server.Cuffs;
|
||||||
using Content.Shared.Alert;
|
using Content.Shared.Alert;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
@@ -13,10 +13,9 @@ namespace Content.Server.Alert.Click
|
|||||||
{
|
{
|
||||||
public void AlertClicked(EntityUid player)
|
public void AlertClicked(EntityUid player)
|
||||||
{
|
{
|
||||||
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(player, out CuffableComponent? cuffableComponent))
|
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||||
{
|
var cuffableSys = entityManager.System<CuffableSystem>();
|
||||||
cuffableComponent.TryUncuff(player);
|
cuffableSys.TryUncuff(player, player);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,295 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using Content.Server.Administration.Logs;
|
|
||||||
using Content.Server.DoAfter;
|
|
||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Hands.Systems;
|
|
||||||
using Content.Shared.ActionBlocker;
|
|
||||||
using Content.Shared.Alert;
|
|
||||||
using Content.Shared.Cuffs.Components;
|
|
||||||
using Content.Shared.Hands.Components;
|
|
||||||
using Content.Shared.Database;
|
|
||||||
using Content.Shared.Hands.EntitySystems;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Interaction.Components;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Robust.Server.Containers;
|
|
||||||
using Robust.Server.GameObjects;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Content.Server.Recycling.Components;
|
|
||||||
using Content.Shared.DoAfter;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
|
|
||||||
namespace Content.Server.Cuffs.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
[ComponentReference(typeof(SharedCuffableComponent))]
|
|
||||||
public sealed class CuffableComponent : SharedCuffableComponent
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
|
||||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
|
||||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
|
||||||
|
|
||||||
private bool _uncuffing;
|
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
Owner.EnsureComponentWarn<HandsComponent>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ComponentState GetComponentState()
|
|
||||||
{
|
|
||||||
// there are 2 approaches i can think of to handle the handcuff overlay on players
|
|
||||||
// 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same.
|
|
||||||
// 2 - allow for several different player overlays for each different cuff type.
|
|
||||||
// approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it.
|
|
||||||
// right now we're doing approach #1.
|
|
||||||
|
|
||||||
if (CuffedHandCount > 0)
|
|
||||||
{
|
|
||||||
if (_entMan.TryGetComponent<HandcuffComponent?>(LastAddedCuffs, out var cuffs))
|
|
||||||
{
|
|
||||||
return new CuffableComponentState(CuffedHandCount,
|
|
||||||
CanStillInteract,
|
|
||||||
cuffs.CuffedRSI,
|
|
||||||
$"{cuffs.OverlayIconState}-{CuffedHandCount}",
|
|
||||||
cuffs.Color);
|
|
||||||
// the iconstate is formatted as blah-2, blah-4, blah-6, etc.
|
|
||||||
// the number corresponds to how many hands are cuffed.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CuffableComponentState(CuffedHandCount,
|
|
||||||
CanStillInteract,
|
|
||||||
"/Objects/Misc/handcuffs.rsi",
|
|
||||||
"body-overlay-2",
|
|
||||||
Color.White);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a set of cuffs to an existing CuffedComponent.
|
|
||||||
/// </summary>
|
|
||||||
public bool TryAddNewCuffs(EntityUid user, EntityUid handcuff)
|
|
||||||
{
|
|
||||||
if (!_entMan.HasComponent<HandcuffComponent>(handcuff))
|
|
||||||
{
|
|
||||||
Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(handcuff, Owner))
|
|
||||||
{
|
|
||||||
Logger.Warning("Handcuffs being applied to player are obstructed or too far away! This should not happen!");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sys = _entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>();
|
|
||||||
|
|
||||||
// Success!
|
|
||||||
sys.TryDrop(user, handcuff);
|
|
||||||
|
|
||||||
Container.Insert(handcuff);
|
|
||||||
UpdateHeldItems(handcuff);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds virtual cuff items to the user's hands.
|
|
||||||
/// </summary>
|
|
||||||
public void UpdateHeldItems(EntityUid handcuff)
|
|
||||||
{
|
|
||||||
// TODO when ecs-ing this, we probably don't just want to use the generic virtual-item entity, and instead
|
|
||||||
// want to add our own item, so that use-in-hand triggers an uncuff attempt and the like.
|
|
||||||
|
|
||||||
if (!_entMan.TryGetComponent(Owner, out HandsComponent? handsComponent)) return;
|
|
||||||
|
|
||||||
var handSys = _entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>();
|
|
||||||
|
|
||||||
var freeHands = 0;
|
|
||||||
foreach (var hand in handSys.EnumerateHands(Owner, handsComponent))
|
|
||||||
{
|
|
||||||
if (hand.HeldEntity == null)
|
|
||||||
{
|
|
||||||
freeHands++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is this entity removable? (it might be an existing handcuff blocker)
|
|
||||||
if (_entMan.HasComponent<UnremoveableComponent>(hand.HeldEntity))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
handSys.DoDrop(Owner, hand, true, handsComponent);
|
|
||||||
freeHands++;
|
|
||||||
if (freeHands == 2)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var virtSys = _entMan.EntitySysManager.GetEntitySystem<HandVirtualItemSystem>();
|
|
||||||
|
|
||||||
if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem1))
|
|
||||||
_entMan.EnsureComponent<UnremoveableComponent>(virtItem1.Value);
|
|
||||||
|
|
||||||
if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem2))
|
|
||||||
_entMan.EnsureComponent<UnremoveableComponent>(virtItem2.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the status effect indicator on the HUD.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateAlert()
|
|
||||||
{
|
|
||||||
if (CanStillInteract)
|
|
||||||
{
|
|
||||||
EntitySystem.Get<AlertsSystem>().ClearAlert(Owner, AlertType.Handcuffed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
EntitySystem.Get<AlertsSystem>().ShowAlert(Owner, AlertType.Handcuffed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
|
|
||||||
/// If the uncuffing succeeds, the cuffs will drop on the floor.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="user">The cuffed entity</param>
|
|
||||||
/// <param name="cuffsToRemove">Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity.</param>
|
|
||||||
public async void TryUncuff(EntityUid user, EntityUid? cuffsToRemove = null)
|
|
||||||
{
|
|
||||||
if (_uncuffing) return;
|
|
||||||
|
|
||||||
var isOwner = user == Owner;
|
|
||||||
|
|
||||||
if (cuffsToRemove == null)
|
|
||||||
{
|
|
||||||
if (Container.ContainedEntities.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cuffsToRemove = LastAddedCuffs;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!Container.ContainedEntities.Contains(cuffsToRemove.Value))
|
|
||||||
{
|
|
||||||
Logger.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_entMan.TryGetComponent<HandcuffComponent?>(cuffsToRemove, out var cuff))
|
|
||||||
{
|
|
||||||
Logger.Warning($"A user is trying to remove handcuffs without a {nameof(HandcuffComponent)}. This should never happen!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var attempt = new UncuffAttemptEvent(user, Owner);
|
|
||||||
_entMan.EventBus.RaiseLocalEvent(user, attempt, true);
|
|
||||||
|
|
||||||
if (attempt.Cancelled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOwner && !EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(user, Owner))
|
|
||||||
{
|
|
||||||
user.PopupMessage(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.PopupMessage(Loc.GetString("cuffable-component-start-removing-cuffs-message"));
|
|
||||||
|
|
||||||
if (isOwner)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(cuff.StartBreakoutSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SoundSystem.Play(cuff.StartUncuffSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
|
|
||||||
var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime, target: Owner)
|
|
||||||
{
|
|
||||||
BreakOnUserMove = true,
|
|
||||||
BreakOnTargetMove = true,
|
|
||||||
BreakOnDamage = true,
|
|
||||||
BreakOnStun = true,
|
|
||||||
NeedHand = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
|
|
||||||
_uncuffing = true;
|
|
||||||
|
|
||||||
var result = await doAfterSystem.WaitDoAfter(doAfterEventArgs);
|
|
||||||
|
|
||||||
_uncuffing = false;
|
|
||||||
|
|
||||||
if (result != DoAfterStatus.Cancelled)
|
|
||||||
{
|
|
||||||
Uncuff(user, cuffsToRemove.Value, cuff, isOwner);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-fail-message"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Lord forgive me for putting this here
|
|
||||||
//Cuff ECS when
|
|
||||||
public void Uncuff(EntityUid user, EntityUid cuffsToRemove, HandcuffComponent cuff, bool isOwner)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(cuff.EndUncuffSound.GetSound(), Filter.Pvs(Owner), Owner);
|
|
||||||
|
|
||||||
_entMan.EntitySysManager.GetEntitySystem<HandVirtualItemSystem>().DeleteInHandsMatching(user, cuffsToRemove);
|
|
||||||
Container.Remove(cuffsToRemove);
|
|
||||||
|
|
||||||
if (cuff.BreakOnRemove)
|
|
||||||
{
|
|
||||||
_entMan.QueueDeleteEntity(cuffsToRemove);
|
|
||||||
var trash = _entMan.SpawnEntity(cuff.BrokenPrototype, MapCoordinates.Nullspace);
|
|
||||||
_entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(user, trash);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(user, cuffsToRemove);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CuffedHandCount == 0)
|
|
||||||
{
|
|
||||||
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-success-message"));
|
|
||||||
|
|
||||||
if (!isOwner)
|
|
||||||
{
|
|
||||||
user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message", ("otherName", user)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user == Owner)
|
|
||||||
{
|
|
||||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed themselves");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed {_entMan.ToPrettyString(Owner):player}");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!isOwner)
|
|
||||||
{
|
|
||||||
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount), ("otherName", user)));
|
|
||||||
user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-partial-success-message", ("otherName", user), ("cuffedHandCount", CuffedHandCount)));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
using Content.Server.Administration.Components;
|
|
||||||
using Content.Server.Administration.Logs;
|
|
||||||
using Content.Server.DoAfter;
|
|
||||||
using Content.Shared.Cuffs.Components;
|
|
||||||
using Content.Shared.Database;
|
|
||||||
using Content.Shared.DoAfter;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Stunnable;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Cuffs.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
[ComponentReference(typeof(SharedHandcuffComponent))]
|
|
||||||
public sealed class HandcuffComponent : SharedHandcuffComponent
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IEntityManager _entities = default!;
|
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The time it takes to apply a <see cref="CuffedComponent"/> to an entity.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("cuffTime")]
|
|
||||||
public float CuffTime { get; set; } = 3.5f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The time it takes to remove a <see cref="CuffedComponent"/> from an entity.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("uncuffTime")]
|
|
||||||
public float UncuffTime { get; set; } = 3.5f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The time it takes for a cuffed entity to remove <see cref="CuffedComponent"/> from itself.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("breakoutTime")]
|
|
||||||
public float BreakoutTime { get; set; } = 30f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("stunBonus")]
|
|
||||||
public float StunBonus { get; set; } = 2f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Will the cuffs break when removed?
|
|
||||||
/// </summary>
|
|
||||||
[DataField("breakOnRemove")]
|
|
||||||
public bool BreakOnRemove { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Will the cuffs break when removed?
|
|
||||||
/// </summary>
|
|
||||||
[DataField("brokenPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
public string? BrokenPrototype { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The path of the RSI file used for the player cuffed overlay.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("cuffedRSI")]
|
|
||||||
public string? CuffedRSI { get; set; } = "Objects/Misc/handcuffs.rsi";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The iconstate used with the RSI file for the player cuffed overlay.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("bodyIconState")]
|
|
||||||
public string? OverlayIconState { get; set; } = "body-overlay";
|
|
||||||
|
|
||||||
[DataField("startCuffSound")]
|
|
||||||
public SoundSpecifier StartCuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_start.ogg");
|
|
||||||
|
|
||||||
[DataField("endCuffSound")]
|
|
||||||
public SoundSpecifier EndCuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_end.ogg");
|
|
||||||
|
|
||||||
[DataField("startBreakoutSound")]
|
|
||||||
public SoundSpecifier StartBreakoutSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_breakout_start.ogg");
|
|
||||||
|
|
||||||
[DataField("startUncuffSound")]
|
|
||||||
public SoundSpecifier StartUncuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_start.ogg");
|
|
||||||
|
|
||||||
[DataField("endUncuffSound")]
|
|
||||||
public SoundSpecifier EndUncuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_end.ogg");
|
|
||||||
[DataField("color")]
|
|
||||||
public Color Color { get; set; } = Color.White;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used to prevent DoAfter getting spammed.
|
|
||||||
/// </summary>
|
|
||||||
public bool Cuffing;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update the cuffed state of an entity
|
|
||||||
/// </summary>
|
|
||||||
public async void TryUpdateCuff(EntityUid user, EntityUid target, CuffableComponent cuffs)
|
|
||||||
{
|
|
||||||
var cuffTime = CuffTime;
|
|
||||||
|
|
||||||
if (_entities.HasComponent<StunnedComponent>(target))
|
|
||||||
{
|
|
||||||
cuffTime = MathF.Max(0.1f, cuffTime - StunBonus);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_entities.HasComponent<DisarmProneComponent>(target))
|
|
||||||
cuffTime = 0.0f; // cuff them instantly.
|
|
||||||
|
|
||||||
var doAfterEventArgs = new DoAfterEventArgs(user, cuffTime, default, target)
|
|
||||||
{
|
|
||||||
BreakOnTargetMove = true,
|
|
||||||
BreakOnUserMove = true,
|
|
||||||
BreakOnDamage = true,
|
|
||||||
BreakOnStun = true,
|
|
||||||
NeedHand = true
|
|
||||||
};
|
|
||||||
|
|
||||||
Cuffing = true;
|
|
||||||
|
|
||||||
var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterEventArgs);
|
|
||||||
|
|
||||||
Cuffing = false;
|
|
||||||
|
|
||||||
// TODO these pop-ups need third-person variants (i.e. {$user} is cuffing {$target}!
|
|
||||||
|
|
||||||
if (result != DoAfterStatus.Cancelled)
|
|
||||||
{
|
|
||||||
if (cuffs.TryAddNewCuffs(user, Owner))
|
|
||||||
{
|
|
||||||
SoundSystem.Play(EndCuffSound.GetSound(), Filter.Pvs(Owner), Owner);
|
|
||||||
if (target == user)
|
|
||||||
{
|
|
||||||
user.PopupMessage(Loc.GetString("handcuff-component-cuff-self-success-message"));
|
|
||||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entities.ToPrettyString(user):player} has cuffed himself");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
user.PopupMessage(Loc.GetString("handcuff-component-cuff-other-success-message",("otherName", target)));
|
|
||||||
target.PopupMessage(Loc.GetString("handcuff-component-cuff-by-other-success-message", ("otherName", user)));
|
|
||||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entities.ToPrettyString(user):player} has cuffed {_entities.ToPrettyString(target):player}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (target == user)
|
|
||||||
{
|
|
||||||
user.PopupMessage(Loc.GetString("handcuff-component-cuff-interrupt-self-message"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
user.PopupMessage(Loc.GetString("handcuff-component-cuff-interrupt-message",("targetName", target)));
|
|
||||||
target.PopupMessage(Loc.GetString("handcuff-component-cuff-interrupt-other-message",("otherName", user)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +1,44 @@
|
|||||||
using System.Linq;
|
|
||||||
using Content.Server.Cuffs.Components;
|
|
||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Shared.ActionBlocker;
|
|
||||||
using Content.Shared.Cuffs;
|
using Content.Shared.Cuffs;
|
||||||
using Content.Shared.Hands;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Verbs;
|
|
||||||
using Content.Shared.Weapons.Melee.Events;
|
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Player;
|
using Content.Shared.Cuffs.Components;
|
||||||
using Content.Shared.Interaction;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Content.Server.Hands.Systems;
|
|
||||||
using Content.Shared.Mobs.Systems;
|
|
||||||
|
|
||||||
namespace Content.Server.Cuffs
|
namespace Content.Server.Cuffs
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class CuffableSystem : SharedCuffableSystem
|
public sealed class CuffableSystem : SharedCuffableSystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
|
||||||
[Dependency] private readonly HandVirtualItemSystem _virtualSystem = default!;
|
|
||||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
||||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
|
||||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<HandCountChangedEvent>(OnHandCountChanged);
|
SubscribeLocalEvent<HandcuffComponent, ComponentGetState>(OnHandcuffGetState);
|
||||||
SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
|
SubscribeLocalEvent<CuffableComponent, ComponentGetState>(OnCuffableGetState);
|
||||||
SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
|
|
||||||
SubscribeLocalEvent<HandcuffComponent, AfterInteractEvent>(OnCuffAfterInteract);
|
|
||||||
SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
|
|
||||||
SubscribeLocalEvent<CuffableComponent, EntRemovedFromContainerMessage>(OnCuffsRemoved);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCuffsRemoved(EntityUid uid, CuffableComponent component, EntRemovedFromContainerMessage args)
|
private void OnHandcuffGetState(EntityUid uid, HandcuffComponent component, ref ComponentGetState args)
|
||||||
{
|
{
|
||||||
if (args.Container.ID == component.Container.ID)
|
args.State = new HandcuffComponentState(component.OverlayIconState, component.Cuffing);
|
||||||
_virtualSystem.DeleteInHandsMatching(uid, args.Entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsEvent<Verb> args)
|
private void OnCuffableGetState(EntityUid uid, CuffableComponent component, ref ComponentGetState args)
|
||||||
{
|
{
|
||||||
// Can the user access the cuffs, and is there even anything to uncuff?
|
// there are 2 approaches i can think of to handle the handcuff overlay on players
|
||||||
if (!args.CanAccess || component.CuffedHandCount == 0 || args.Hands == null)
|
// 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same.
|
||||||
return;
|
// 2 - allow for several different player overlays for each different cuff type.
|
||||||
|
// approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it.
|
||||||
// We only check can interact if the user is not uncuffing themselves. As a result, the verb will show up
|
// right now we're doing approach #1.
|
||||||
// when the user is incapacitated & trying to uncuff themselves, but TryUncuff() will still fail when
|
HandcuffComponent? cuffs = null;
|
||||||
// attempted.
|
if (component.CuffedHandCount > 0)
|
||||||
if (args.User != args.Target && !args.CanInteract)
|
TryComp(component.LastAddedCuffs, out cuffs);
|
||||||
return;
|
args.State = new CuffableComponentState(component.CuffedHandCount,
|
||||||
|
component.CanStillInteract,
|
||||||
Verb verb = new()
|
component.Uncuffing,
|
||||||
{
|
cuffs?.CuffedRSI,
|
||||||
Act = () => component.TryUncuff(args.User),
|
$"{cuffs?.OverlayIconState}-{component.CuffedHandCount}",
|
||||||
DoContactInteraction = true,
|
cuffs?.Color);
|
||||||
Text = Loc.GetString("uncuff-verb-get-data-text")
|
// the iconstate is formatted as blah-2, blah-4, blah-6, etc.
|
||||||
};
|
// the number corresponds to how many hands are cuffed.
|
||||||
//TODO VERB ICON add uncuffing symbol? may re-use the alert symbol showing that you are currently cuffed?
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCuffAfterInteract(EntityUid uid, HandcuffComponent component, AfterInteractEvent args)
|
|
||||||
{
|
|
||||||
if (args.Target is not {Valid: true} target)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!args.CanReach)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("handcuff-component-too-far-away-error"), args.User, args.User);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TryCuffing(uid, args.User, args.Target.Value, component);
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryCuffing(EntityUid handcuff, EntityUid user, EntityUid target, HandcuffComponent component)
|
|
||||||
{
|
|
||||||
if (component.Cuffing || !EntityManager.TryGetComponent<CuffableComponent>(target, out var cuffed))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!EntityManager.TryGetComponent<HandsComponent?>(target, out var hands))
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("handcuff-component-target-has-no-hands-error",("targetName", target)), user, user);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cuffed.CuffedHandCount >= hands.Count)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("handcuff-component-target-has-no-free-hands-error",("targetName", target)), user, user);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO these messages really need third-party variants. I.e., "{$user} starts cuffing {$target}!"
|
|
||||||
if (target == user)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("handcuff-component-target-self"), user, user);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-target-message",("targetName", target)), user, user);
|
|
||||||
_popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-by-other-message",("otherName", user)), target, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
_audio.PlayPvs(component.StartCuffSound, handcuff);
|
|
||||||
|
|
||||||
component.TryUpdateCuff(user, target, cuffed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCuffMeleeHit(EntityUid uid, HandcuffComponent component, MeleeHitEvent args)
|
|
||||||
{
|
|
||||||
if (!args.HitEntities.Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
TryCuffing(uid, args.User, args.HitEntities.First(), component);
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void OnUncuffAttempt(UncuffAttemptEvent args)
|
|
||||||
{
|
|
||||||
if (args.Cancelled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!EntityManager.EntityExists(args.User))
|
|
||||||
{
|
|
||||||
// Should this even be possible?
|
|
||||||
args.Cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If the user is the target, special logic applies.
|
|
||||||
// This is because the CanInteract blocking of the cuffs prevents self-uncuff.
|
|
||||||
if (args.User == args.Target)
|
|
||||||
{
|
|
||||||
// This UncuffAttemptEvent check should probably be In MobStateSystem, not here?
|
|
||||||
if (_mobState.IsIncapacitated(args.User))
|
|
||||||
{
|
|
||||||
args.Cancel();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// TODO Find a way for cuffable to check ActionBlockerSystem.CanInteract() without blocking itself
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Check if the user can interact.
|
|
||||||
if (!_actionBlockerSystem.CanInteract(args.User, args.Target))
|
|
||||||
{
|
|
||||||
args.Cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (args.Cancelled)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("cuffable-component-cannot-interact-message"), args.Target, args.User);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs.
|
|
||||||
/// </summary>
|
|
||||||
private void OnHandCountChanged(HandCountChangedEvent message)
|
|
||||||
{
|
|
||||||
var owner = message.Sender;
|
|
||||||
|
|
||||||
if (!EntityManager.TryGetComponent(owner, out CuffableComponent? cuffable) ||
|
|
||||||
!cuffable.Initialized)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dirty = false;
|
|
||||||
var handCount = EntityManager.GetComponentOrNull<HandsComponent>(owner)?.Count ?? 0;
|
|
||||||
|
|
||||||
while (cuffable.CuffedHandCount > handCount && cuffable.CuffedHandCount > 0)
|
|
||||||
{
|
|
||||||
dirty = true;
|
|
||||||
|
|
||||||
var container = cuffable.Container;
|
|
||||||
var entity = container.ContainedEntities[^1];
|
|
||||||
|
|
||||||
container.Remove(entity);
|
|
||||||
EntityManager.GetComponent<TransformComponent>(entity).WorldPosition = EntityManager.GetComponent<TransformComponent>(owner).WorldPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dirty)
|
|
||||||
{
|
|
||||||
UpdateCuffState(owner, cuffable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event fired on the User when the User attempts to cuff the Target.
|
|
||||||
/// Should generate popups on the User.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class UncuffAttemptEvent : CancellableEntityEventArgs
|
|
||||||
{
|
|
||||||
public readonly EntityUid User;
|
|
||||||
public readonly EntityUid Target;
|
|
||||||
|
|
||||||
public UncuffAttemptEvent(EntityUid user, EntityUid target)
|
|
||||||
{
|
|
||||||
User = user;
|
|
||||||
Target = target;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,39 +9,6 @@ namespace Content.Server.Hands.Systems
|
|||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class HandVirtualItemSystem : SharedHandVirtualItemSystem
|
public sealed class HandVirtualItemSystem : SharedHandVirtualItemSystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
|
||||||
|
|
||||||
public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user) => TrySpawnVirtualItemInHand(blockingEnt, user, out _);
|
|
||||||
|
|
||||||
public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user, [NotNullWhen(true)] out EntityUid? virtualItem)
|
|
||||||
{
|
|
||||||
if (!_handsSystem.TryGetEmptyHand(user, out var hand))
|
|
||||||
{
|
|
||||||
virtualItem = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pos = EntityManager.GetComponent<TransformComponent>(user).Coordinates;
|
|
||||||
virtualItem = EntityManager.SpawnEntity("HandVirtualItem", pos);
|
|
||||||
var virtualItemComp = EntityManager.GetComponent<HandVirtualItemComponent>(virtualItem.Value);
|
|
||||||
virtualItemComp.BlockingEntity = blockingEnt;
|
|
||||||
_handsSystem.DoPickup(user, hand, virtualItem.Value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes all virtual items in a user's hands with
|
|
||||||
/// the specified blocked entity.
|
|
||||||
/// </summary>
|
|
||||||
public void DeleteInHandsMatching(EntityUid user, EntityUid matching)
|
|
||||||
{
|
|
||||||
foreach (var hand in _handsSystem.EnumerateHands(user))
|
|
||||||
{
|
|
||||||
if (TryComp(hand.HeldEntity, out HandVirtualItemComponent? virt) && virt.BlockingEntity == matching)
|
|
||||||
{
|
|
||||||
Delete(virt, user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Server.Cuffs.Components;
|
using Content.Server.Cuffs;
|
||||||
|
using Content.Shared.Cuffs.Components;
|
||||||
using Content.Shared.Implants;
|
using Content.Shared.Implants;
|
||||||
using Content.Shared.Implants.Components;
|
using Content.Shared.Implants.Components;
|
||||||
using Content.Shared.Interaction.Events;
|
using Content.Shared.Interaction.Events;
|
||||||
@@ -9,6 +10,7 @@ namespace Content.Server.Implants;
|
|||||||
|
|
||||||
public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly CuffableSystem _cuffable = default!;
|
||||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
@@ -26,10 +28,7 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
|||||||
if (!TryComp<CuffableComponent>(component.ImplantedEntity, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1)
|
if (!TryComp<CuffableComponent>(component.ImplantedEntity, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (TryComp<HandcuffComponent>(cuffs.LastAddedCuffs, out var cuff))
|
_cuffable.Uncuff(component.ImplantedEntity.Value, cuffs.LastAddedCuffs, cuffs.LastAddedCuffs);
|
||||||
{
|
|
||||||
cuffs.Uncuff(component.ImplantedEntity.Value, cuffs.LastAddedCuffs, cuff, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Relays
|
#region Relays
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Content.Server.Cuffs.Components;
|
|
||||||
using Content.Server.Objectives.Interfaces;
|
using Content.Server.Objectives.Interfaces;
|
||||||
using Content.Server.Station.Components;
|
using Content.Server.Station.Components;
|
||||||
|
using Content.Shared.Cuffs.Components;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Map.Components;
|
using Robust.Shared.Map.Components;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Content.Server.Cuffs.Components;
|
using System.Linq;
|
||||||
using Content.Server.DoAfter;
|
using Content.Server.DoAfter;
|
||||||
using Content.Server.Ensnaring;
|
using Content.Server.Ensnaring;
|
||||||
using Content.Server.Hands.Components;
|
using Content.Server.Hands.Components;
|
||||||
@@ -14,6 +14,8 @@ using Content.Shared.Verbs;
|
|||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Content.Server.Administration.Logs;
|
using Content.Server.Administration.Logs;
|
||||||
|
using Content.Shared.Cuffs;
|
||||||
|
using Content.Shared.Cuffs.Components;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.DoAfter;
|
using Content.Shared.DoAfter;
|
||||||
using Content.Shared.Ensnaring.Components;
|
using Content.Shared.Ensnaring.Components;
|
||||||
@@ -25,6 +27,7 @@ namespace Content.Server.Strip
|
|||||||
{
|
{
|
||||||
public sealed class StrippableSystem : SharedStrippableSystem
|
public sealed class StrippableSystem : SharedStrippableSystem
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly SharedCuffableSystem _cuffable = default!;
|
||||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||||
@@ -95,9 +98,9 @@ namespace Content.Server.Strip
|
|||||||
// is the target a handcuff?
|
// is the target a handcuff?
|
||||||
if (TryComp(hand.HeldEntity, out HandVirtualItemComponent? virt)
|
if (TryComp(hand.HeldEntity, out HandVirtualItemComponent? virt)
|
||||||
&& TryComp(target, out CuffableComponent? cuff)
|
&& TryComp(target, out CuffableComponent? cuff)
|
||||||
&& cuff.Container.Contains(virt.BlockingEntity))
|
&& _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity))
|
||||||
{
|
{
|
||||||
cuff.TryUncuff(user, virt.BlockingEntity);
|
_cuffable.TryUncuff(target, user, virt.BlockingEntity, cuffable: cuff);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using Content.Server.Contests;
|
|||||||
using Content.Server.Examine;
|
using Content.Server.Examine;
|
||||||
using Content.Server.Hands.Components;
|
using Content.Server.Hands.Components;
|
||||||
using Content.Server.Movement.Systems;
|
using Content.Server.Movement.Systems;
|
||||||
|
using Content.Shared.Administration.Components;
|
||||||
using Content.Shared.CombatMode;
|
using Content.Shared.CombatMode;
|
||||||
using Content.Shared.Damage;
|
using Content.Shared.Damage;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Content.Shared.Weapons.Melee;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
|
namespace Content.Shared.Administration.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is used for forcing someone to be disarmed 100% of the time.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
[Access(typeof(SharedMeleeWeaponSystem))]
|
||||||
|
public sealed class DisarmProneComponent : Component
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
72
Content.Shared/Cuffs/Components/CuffableComponent.cs
Normal file
72
Content.Shared/Cuffs/Components/CuffableComponent.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Shared.Cuffs.Components;
|
||||||
|
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
[Access(typeof(SharedCuffableSystem))]
|
||||||
|
public sealed class CuffableComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The current RSI for the handcuff layer
|
||||||
|
/// </summary>
|
||||||
|
[DataField("currentRSI"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public string? CurrentRSI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many of this entity's hands are currently cuffed.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public int CuffedHandCount => Container.ContainedEntities.Count * 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The last pair of cuffs that was added to this entity.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public EntityUid LastAddedCuffs => Container.ContainedEntities[^1];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Container of various handcuffs currently applied to the entity.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadOnly)]
|
||||||
|
public Container Container = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not the entity can still interact (is not cuffed)
|
||||||
|
/// </summary>
|
||||||
|
[DataField("canStillInteract"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public bool CanStillInteract = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not the entity is currently in the process of being uncuffed.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("uncuffing"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public bool Uncuffing;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class CuffableComponentState : ComponentState
|
||||||
|
{
|
||||||
|
public readonly bool CanStillInteract;
|
||||||
|
public readonly bool Uncuffing;
|
||||||
|
public readonly int NumHandsCuffed;
|
||||||
|
public readonly string? RSI;
|
||||||
|
public readonly string? IconState;
|
||||||
|
public readonly Color? Color;
|
||||||
|
|
||||||
|
public CuffableComponentState(int numHandsCuffed, bool canStillInteract, bool uncuffing, string? rsiPath, string? iconState, Color? color)
|
||||||
|
{
|
||||||
|
NumHandsCuffed = numHandsCuffed;
|
||||||
|
CanStillInteract = canStillInteract;
|
||||||
|
Uncuffing = uncuffing;
|
||||||
|
RSI = rsiPath;
|
||||||
|
IconState = iconState;
|
||||||
|
Color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ByRefEvent]
|
||||||
|
public readonly record struct CuffedStateChangeEvent;
|
||||||
|
|
||||||
113
Content.Shared/Cuffs/Components/HandcuffComponent.cs
Normal file
113
Content.Shared/Cuffs/Components/HandcuffComponent.cs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Shared.Cuffs.Components;
|
||||||
|
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
[Access(typeof(SharedCuffableSystem))]
|
||||||
|
public sealed class HandcuffComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The time it takes to cuff an entity.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("cuffTime"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public float CuffTime = 3.5f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time it takes to uncuff an entity.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("uncuffTime"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public float UncuffTime = 3.5f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time it takes for a cuffed entity to uncuff itself.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("breakoutTime"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public float BreakoutTime = 30f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("stunBonus"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public float StunBonus = 2f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Will the cuffs break when removed?
|
||||||
|
/// </summary>
|
||||||
|
[DataField("breakOnRemove"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public bool BreakOnRemove;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Will the cuffs break when removed?
|
||||||
|
/// </summary>
|
||||||
|
[DataField("brokenPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public string? BrokenPrototype;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path of the RSI file used for the player cuffed overlay.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("cuffedRSI"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public string? CuffedRSI = "Objects/Misc/handcuffs.rsi";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The iconstate used with the RSI file for the player cuffed overlay.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("bodyIconState"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public string? OverlayIconState = "body-overlay";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An opptional color specification for <see cref="OverlayIconState"/>
|
||||||
|
/// </summary>
|
||||||
|
[DataField("color"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public Color Color = Color.White;
|
||||||
|
|
||||||
|
[DataField("startCuffSound"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public SoundSpecifier StartCuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_start.ogg");
|
||||||
|
|
||||||
|
[DataField("endCuffSound"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public SoundSpecifier EndCuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_end.ogg");
|
||||||
|
|
||||||
|
[DataField("startBreakoutSound"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public SoundSpecifier StartBreakoutSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_breakout_start.ogg");
|
||||||
|
|
||||||
|
[DataField("startUncuffSound"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public SoundSpecifier StartUncuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_start.ogg");
|
||||||
|
|
||||||
|
[DataField("endUncuffSound"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public SoundSpecifier EndUncuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_end.ogg");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to prevent DoAfter getting spammed.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("cuffing"), ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public bool Cuffing;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class HandcuffComponentState : ComponentState
|
||||||
|
{
|
||||||
|
public readonly string? IconState;
|
||||||
|
public readonly bool Cuffing;
|
||||||
|
|
||||||
|
public HandcuffComponentState(string? iconState, bool cuffing)
|
||||||
|
{
|
||||||
|
IconState = iconState;
|
||||||
|
Cuffing = cuffing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired on the User when the User attempts to cuff the Target.
|
||||||
|
/// Should generate popups on the User.
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct UncuffAttemptEvent(EntityUid User, EntityUid Target)
|
||||||
|
{
|
||||||
|
public readonly EntityUid User = User;
|
||||||
|
public readonly EntityUid Target = Target;
|
||||||
|
public bool Cancelled = false;
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Shared.Cuffs.Components
|
|
||||||
{
|
|
||||||
[ByRefEvent]
|
|
||||||
public readonly struct CuffedStateChangeEvent { }
|
|
||||||
|
|
||||||
[NetworkedComponent()]
|
|
||||||
public abstract class SharedCuffableComponent : Component
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
|
||||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How many of this entity's hands are currently cuffed.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables]
|
|
||||||
public int CuffedHandCount => Container.ContainedEntities.Count * 2;
|
|
||||||
|
|
||||||
public EntityUid LastAddedCuffs => Container.ContainedEntities[^1];
|
|
||||||
|
|
||||||
public IReadOnlyList<EntityUid> StoredEntities => Container.ContainedEntities;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Container of various handcuffs currently applied to the entity.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables(VVAccess.ReadOnly)]
|
|
||||||
public Container Container { get; set; } = default!;
|
|
||||||
|
|
||||||
protected override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
Container = _sysMan.GetEntitySystem<SharedContainerSystem>().EnsureContainer<Container>(Owner, _componentFactory.GetComponentName(GetType()));
|
|
||||||
}
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
public bool CanStillInteract { get; set; } = true;
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
protected sealed class CuffableComponentState : ComponentState
|
|
||||||
{
|
|
||||||
public bool CanStillInteract { get; }
|
|
||||||
public int NumHandsCuffed { get; }
|
|
||||||
public string? RSI { get; }
|
|
||||||
public string IconState { get; }
|
|
||||||
public Color Color { get; }
|
|
||||||
|
|
||||||
public CuffableComponentState(int numHandsCuffed, bool canStillInteract, string? rsiPath, string iconState, Color color)
|
|
||||||
{
|
|
||||||
NumHandsCuffed = numHandsCuffed;
|
|
||||||
CanStillInteract = canStillInteract;
|
|
||||||
RSI = rsiPath;
|
|
||||||
IconState = iconState;
|
|
||||||
Color = color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Shared.Cuffs.Components
|
|
||||||
{
|
|
||||||
[NetworkedComponent()]
|
|
||||||
public abstract class SharedHandcuffComponent : Component
|
|
||||||
{
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
protected sealed class HandcuffedComponentState : ComponentState
|
|
||||||
{
|
|
||||||
public string? IconState { get; }
|
|
||||||
|
|
||||||
public HandcuffedComponentState(string? iconState)
|
|
||||||
{
|
|
||||||
IconState = iconState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +1,155 @@
|
|||||||
|
using System.Linq;
|
||||||
using Content.Shared.ActionBlocker;
|
using Content.Shared.ActionBlocker;
|
||||||
|
using Content.Shared.Administration.Components;
|
||||||
|
using Content.Shared.Administration.Logs;
|
||||||
using Content.Shared.Alert;
|
using Content.Shared.Alert;
|
||||||
using Content.Shared.Cuffs.Components;
|
using Content.Shared.Cuffs.Components;
|
||||||
using Content.Shared.DragDrop;
|
using Content.Shared.Database;
|
||||||
|
using Content.Shared.DoAfter;
|
||||||
using Content.Shared.Hands;
|
using Content.Shared.Hands;
|
||||||
using Content.Shared.Hands.Components;
|
using Content.Shared.Hands.Components;
|
||||||
|
using Content.Shared.Hands.EntitySystems;
|
||||||
|
using Content.Shared.IdentityManagement;
|
||||||
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Interaction.Components;
|
||||||
using Content.Shared.Interaction.Events;
|
using Content.Shared.Interaction.Events;
|
||||||
using Content.Shared.Inventory.Events;
|
using Content.Shared.Inventory.Events;
|
||||||
using Content.Shared.Item;
|
using Content.Shared.Item;
|
||||||
|
using Content.Shared.Mobs.Systems;
|
||||||
using Content.Shared.Movement.Events;
|
using Content.Shared.Movement.Events;
|
||||||
using Content.Shared.Physics.Pull;
|
using Content.Shared.Physics.Pull;
|
||||||
|
using Content.Shared.Popups;
|
||||||
using Content.Shared.Pulling.Components;
|
using Content.Shared.Pulling.Components;
|
||||||
using Content.Shared.Pulling.Events;
|
using Content.Shared.Pulling.Events;
|
||||||
using Content.Shared.Rejuvenate;
|
using Content.Shared.Rejuvenate;
|
||||||
|
using Content.Shared.Stunnable;
|
||||||
|
using Content.Shared.Verbs;
|
||||||
|
using Content.Shared.Weapons.Melee.Events;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Shared.Cuffs
|
namespace Content.Shared.Cuffs
|
||||||
{
|
{
|
||||||
public abstract class SharedCuffableSystem : EntitySystem
|
public abstract class SharedCuffableSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
|
||||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||||
|
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly INetManager _net = default!;
|
||||||
|
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
|
||||||
|
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||||
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||||
|
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||||
|
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||||
|
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||||
|
[Dependency] private readonly SharedHandVirtualItemSystem _handVirtualItem = default!;
|
||||||
|
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||||
|
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||||
|
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, EntRemovedFromContainerMessage>(OnCuffCountChanged);
|
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, EntInsertedIntoContainerMessage>(OnCuffCountChanged);
|
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, RejuvenateEvent>(OnRejuvenate);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, StopPullingEvent>(HandleStopPull);
|
SubscribeLocalEvent<HandCountChangedEvent>(OnHandCountChanged);
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, UpdateCanMoveEvent>(HandleMoveAttempt);
|
SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, AttackAttemptEvent>(CheckAct);
|
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, UseAttemptEvent>(CheckAct);
|
SubscribeLocalEvent<CuffableComponent, EntRemovedFromContainerMessage>(OnCuffsRemovedFromContainer);
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, InteractionAttemptEvent>(CheckAct);
|
SubscribeLocalEvent<CuffableComponent, EntInsertedIntoContainerMessage>(OnCuffsInsertedIntoContainer);
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
|
SubscribeLocalEvent<CuffableComponent, RejuvenateEvent>(OnRejuvenate);
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
|
SubscribeLocalEvent<CuffableComponent, ComponentInit>(OnStartup);
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, DropAttemptEvent>(CheckAct);
|
SubscribeLocalEvent<CuffableComponent, StopPullingEvent>(HandleStopPull);
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, PickupAttemptEvent>(CheckAct);
|
SubscribeLocalEvent<CuffableComponent, UpdateCanMoveEvent>(HandleMoveAttempt);
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
|
SubscribeLocalEvent<CuffableComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, PullStartedMessage>(OnPull);
|
SubscribeLocalEvent<CuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
|
||||||
SubscribeLocalEvent<SharedCuffableComponent, PullStoppedMessage>(OnPull);
|
SubscribeLocalEvent<CuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
|
||||||
|
SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
|
||||||
|
SubscribeLocalEvent<CuffableComponent, DoAfterEvent>(OnCuffableDoAfter);
|
||||||
|
SubscribeLocalEvent<CuffableComponent, PullStartedMessage>(OnPull);
|
||||||
|
SubscribeLocalEvent<CuffableComponent, PullStoppedMessage>(OnPull);
|
||||||
|
SubscribeLocalEvent<CuffableComponent, DropAttemptEvent>(CheckAct);
|
||||||
|
SubscribeLocalEvent<CuffableComponent, PickupAttemptEvent>(CheckAct);
|
||||||
|
SubscribeLocalEvent<CuffableComponent, AttackAttemptEvent>(CheckAct);
|
||||||
|
SubscribeLocalEvent<CuffableComponent, UseAttemptEvent>(CheckAct);
|
||||||
|
SubscribeLocalEvent<CuffableComponent, InteractionAttemptEvent>(CheckAct);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<HandcuffComponent, AfterInteractEvent>(OnCuffAfterInteract);
|
||||||
|
SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
|
||||||
|
SubscribeLocalEvent<HandcuffComponent, DoAfterEvent>(OnAddCuffDoAfter);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRejuvenate(EntityUid uid, SharedCuffableComponent component, RejuvenateEvent args)
|
private void OnUncuffAttempt(ref UncuffAttemptEvent args)
|
||||||
|
{
|
||||||
|
if (args.Cancelled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Exists(args.User) || Deleted(args.User))
|
||||||
|
{
|
||||||
|
// Should this even be possible?
|
||||||
|
args.Cancelled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is the target, special logic applies.
|
||||||
|
// This is because the CanInteract blocking of the cuffs prevents self-uncuff.
|
||||||
|
if (args.User == args.Target)
|
||||||
|
{
|
||||||
|
// This UncuffAttemptEvent check should probably be In MobStateSystem, not here?
|
||||||
|
if (_mobState.IsIncapacitated(args.User))
|
||||||
|
{
|
||||||
|
args.Cancelled = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO Find a way for cuffable to check ActionBlockerSystem.CanInteract() without blocking itself
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Check if the user can interact.
|
||||||
|
if (!_actionBlocker.CanInteract(args.User, args.Target))
|
||||||
|
{
|
||||||
|
args.Cancelled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Cancelled && _net.IsServer)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("cuffable-component-cannot-interact-message"), args.Target, args.User);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStartup(EntityUid uid, CuffableComponent component, ComponentInit args)
|
||||||
|
{
|
||||||
|
component.Container = _container.EnsureContainer<Container>(uid, _componentFactory.GetComponentName(component.GetType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRejuvenate(EntityUid uid, CuffableComponent component, RejuvenateEvent args)
|
||||||
{
|
{
|
||||||
_container.EmptyContainer(component.Container, true, attachToGridOrMap: true);
|
_container.EmptyContainer(component.Container, true, attachToGridOrMap: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCuffCountChanged(EntityUid uid, SharedCuffableComponent component, ContainerModifiedMessage args)
|
private void OnCuffsRemovedFromContainer(EntityUid uid, CuffableComponent component, EntRemovedFromContainerMessage args)
|
||||||
|
{
|
||||||
|
if (args.Container.ID == component.Container.ID)
|
||||||
|
{
|
||||||
|
_handVirtualItem.DeleteInHandsMatching(uid, args.Entity);
|
||||||
|
UpdateCuffState(uid, component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCuffsInsertedIntoContainer(EntityUid uid, CuffableComponent component, ContainerModifiedMessage args)
|
||||||
{
|
{
|
||||||
if (args.Container == component.Container)
|
if (args.Container == component.Container)
|
||||||
UpdateCuffState(uid, component);
|
UpdateCuffState(uid, component);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateCuffState(EntityUid uid, SharedCuffableComponent component)
|
public void UpdateCuffState(EntityUid uid, CuffableComponent component)
|
||||||
{
|
{
|
||||||
var canInteract = TryComp(uid, out SharedHandsComponent? hands) && hands.Hands.Count > component.CuffedHandCount;
|
var canInteract = TryComp(uid, out SharedHandsComponent? hands) && hands.Hands.Count > component.CuffedHandCount;
|
||||||
|
|
||||||
@@ -63,7 +158,7 @@ namespace Content.Shared.Cuffs
|
|||||||
|
|
||||||
component.CanStillInteract = canInteract;
|
component.CanStillInteract = canInteract;
|
||||||
Dirty(component);
|
Dirty(component);
|
||||||
_blocker.UpdateCanMove(uid);
|
_actionBlocker.UpdateCanMove(uid);
|
||||||
|
|
||||||
if (component.CanStillInteract)
|
if (component.CanStillInteract)
|
||||||
_alerts.ClearAlert(uid, AlertType.Handcuffed);
|
_alerts.ClearAlert(uid, AlertType.Handcuffed);
|
||||||
@@ -74,7 +169,7 @@ namespace Content.Shared.Cuffs
|
|||||||
RaiseLocalEvent(uid, ref ev);
|
RaiseLocalEvent(uid, ref ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBeingPulledAttempt(EntityUid uid, SharedCuffableComponent component, BeingPulledAttemptEvent args)
|
private void OnBeingPulledAttempt(EntityUid uid, CuffableComponent component, BeingPulledAttemptEvent args)
|
||||||
{
|
{
|
||||||
if (!TryComp<SharedPullableComponent>(uid, out var pullable))
|
if (!TryComp<SharedPullableComponent>(uid, out var pullable))
|
||||||
return;
|
return;
|
||||||
@@ -82,13 +177,14 @@ namespace Content.Shared.Cuffs
|
|||||||
if (pullable.Puller != null && !component.CanStillInteract) // If we are being pulled already and cuffed, we can't get pulled again.
|
if (pullable.Puller != null && !component.CanStillInteract) // If we are being pulled already and cuffed, we can't get pulled again.
|
||||||
args.Cancel();
|
args.Cancel();
|
||||||
}
|
}
|
||||||
private void OnPull(EntityUid uid, SharedCuffableComponent component, PullMessage args)
|
|
||||||
|
private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args)
|
||||||
{
|
{
|
||||||
if (!component.CanStillInteract)
|
if (!component.CanStillInteract)
|
||||||
_blocker.UpdateCanMove(uid);
|
_actionBlocker.UpdateCanMove(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleMoveAttempt(EntityUid uid, SharedCuffableComponent component, UpdateCanMoveEvent args)
|
private void HandleMoveAttempt(EntityUid uid, CuffableComponent component, UpdateCanMoveEvent args)
|
||||||
{
|
{
|
||||||
if (component.CanStillInteract || !EntityManager.TryGetComponent(uid, out SharedPullableComponent? pullable) || !pullable.BeingPulled)
|
if (component.CanStillInteract || !EntityManager.TryGetComponent(uid, out SharedPullableComponent? pullable) || !pullable.BeingPulled)
|
||||||
return;
|
return;
|
||||||
@@ -96,32 +192,466 @@ namespace Content.Shared.Cuffs
|
|||||||
args.Cancel();
|
args.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleStopPull(EntityUid uid, SharedCuffableComponent component, StopPullingEvent args)
|
private void HandleStopPull(EntityUid uid, CuffableComponent component, StopPullingEvent args)
|
||||||
{
|
{
|
||||||
if (args.User == null || !EntityManager.EntityExists(args.User.Value)) return;
|
if (args.User == null || !Exists(args.User.Value))
|
||||||
|
return;
|
||||||
|
|
||||||
if (args.User.Value == component.Owner && !component.CanStillInteract)
|
if (args.User.Value == uid && !component.CanStillInteract)
|
||||||
{
|
|
||||||
args.Cancel();
|
args.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsEvent<Verb> args)
|
||||||
|
{
|
||||||
|
// Can the user access the cuffs, and is there even anything to uncuff?
|
||||||
|
if (!args.CanAccess || component.CuffedHandCount == 0 || args.Hands == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// We only check can interact if the user is not uncuffing themselves. As a result, the verb will show up
|
||||||
|
// when the user is incapacitated & trying to uncuff themselves, but TryUncuff() will still fail when
|
||||||
|
// attempted.
|
||||||
|
if (args.User != args.Target && !args.CanInteract)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Verb verb = new()
|
||||||
|
{
|
||||||
|
Act = () => TryUncuff(uid, args.User, cuffable: component),
|
||||||
|
DoContactInteraction = true,
|
||||||
|
Text = Loc.GetString("uncuff-verb-get-data-text")
|
||||||
|
};
|
||||||
|
//TODO VERB ICON add uncuffing symbol? may re-use the alert symbol showing that you are currently cuffed?
|
||||||
|
args.Verbs.Add(verb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCuffableDoAfter(EntityUid uid, CuffableComponent component, DoAfterEvent args)
|
||||||
|
{
|
||||||
|
if (args.Args.Target is not { } target || args.Args.Used is not { } used)
|
||||||
|
return;
|
||||||
|
if (args.Handled)
|
||||||
|
return;
|
||||||
|
args.Handled = true;
|
||||||
|
|
||||||
|
component.Uncuffing = false;
|
||||||
|
Dirty(component);
|
||||||
|
|
||||||
|
var user = args.Args.User;
|
||||||
|
|
||||||
|
if (!args.Cancelled)
|
||||||
|
{
|
||||||
|
Uncuff(target, user, used, component);
|
||||||
|
}
|
||||||
|
else if (_net.IsServer)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-fail-message"), user, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCuffAfterInteract(EntityUid uid, HandcuffComponent component, AfterInteractEvent args)
|
||||||
|
{
|
||||||
|
if (args.Target is not {Valid: true} target)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!args.CanReach)
|
||||||
|
{
|
||||||
|
if (_net.IsServer)
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-too-far-away-error"), args.User, args.User);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryCuffing(args.User, target, uid, component);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCuffMeleeHit(EntityUid uid, HandcuffComponent component, MeleeHitEvent args)
|
||||||
|
{
|
||||||
|
if (!args.HitEntities.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
TryCuffing(uid, args.User, args.HitEntities.First(), component);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAddCuffDoAfter(EntityUid uid, HandcuffComponent component, DoAfterEvent args)
|
||||||
|
{
|
||||||
|
var user = args.Args.User;
|
||||||
|
var target = args.Args.Target!.Value;
|
||||||
|
|
||||||
|
if (!TryComp<CuffableComponent>(target, out var cuffable))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.Handled)
|
||||||
|
return;
|
||||||
|
args.Handled = true;
|
||||||
|
component.Cuffing = false;
|
||||||
|
|
||||||
|
if (!args.Cancelled && TryAddNewCuffs(target, user, uid, cuffable))
|
||||||
|
{
|
||||||
|
_audio.PlayPvs(component.EndCuffSound, uid);
|
||||||
|
if (!_net.IsServer)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-cuff-observer-success-message",
|
||||||
|
("user", Identity.Name(user, EntityManager)), ("target", Identity.Name(target, EntityManager))),
|
||||||
|
target, Filter.Pvs(target, entityManager: EntityManager)
|
||||||
|
.RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true);
|
||||||
|
|
||||||
|
if (target == user)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-cuff-self-success-message"), user, user);
|
||||||
|
_adminLog.Add(LogType.Action, LogImpact.Medium,
|
||||||
|
$"{ToPrettyString(user):player} has cuffed himself");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-cuff-other-success-message",
|
||||||
|
("otherName", Identity.Name(target, EntityManager, user))), user, user);
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-cuff-by-other-success-message",
|
||||||
|
("otherName", Identity.Name(user, EntityManager, target))), target, target);
|
||||||
|
_adminLog.Add(LogType.Action, LogImpact.Medium,
|
||||||
|
$"{ToPrettyString(user):player} has cuffed {ToPrettyString(target):player}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!_net.IsServer)
|
||||||
|
return;
|
||||||
|
if (target == user)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-cuff-interrupt-self-message"), user, user);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-cuff-interrupt-message",
|
||||||
|
("targetName", Identity.Name(target, EntityManager, user))), user, user);
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-cuff-interrupt-other-message",
|
||||||
|
("otherName", Identity.Name(user, EntityManager, target))), target, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs.
|
||||||
|
/// </summary>
|
||||||
|
private void OnHandCountChanged(HandCountChangedEvent message)
|
||||||
|
{
|
||||||
|
var owner = message.Sender;
|
||||||
|
|
||||||
|
if (!TryComp(owner, out CuffableComponent? cuffable) ||
|
||||||
|
!cuffable.Initialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dirty = false;
|
||||||
|
var handCount = CompOrNull<SharedHandsComponent>(owner)?.Count ?? 0;
|
||||||
|
|
||||||
|
while (cuffable.CuffedHandCount > handCount && cuffable.CuffedHandCount > 0)
|
||||||
|
{
|
||||||
|
dirty = true;
|
||||||
|
|
||||||
|
var container = cuffable.Container;
|
||||||
|
var entity = container.ContainedEntities[^1];
|
||||||
|
|
||||||
|
container.Remove(entity);
|
||||||
|
_transform.SetWorldPosition(entity, _transform.GetWorldPosition(owner));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirty)
|
||||||
|
{
|
||||||
|
UpdateCuffState(owner, cuffable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds virtual cuff items to the user's hands.
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateHeldItems(EntityUid uid, EntityUid handcuff, CuffableComponent? component = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref component))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO we probably don't just want to use the generic virtual-item entity, and instead
|
||||||
|
// want to add our own item, so that use-in-hand triggers an uncuff attempt and the like.
|
||||||
|
|
||||||
|
if (!TryComp<SharedHandsComponent>(uid, out var handsComponent))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var freeHands = 0;
|
||||||
|
foreach (var hand in _hands.EnumerateHands(uid, handsComponent))
|
||||||
|
{
|
||||||
|
if (hand.HeldEntity == null)
|
||||||
|
{
|
||||||
|
freeHands++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this entity removable? (it might be an existing handcuff blocker)
|
||||||
|
if (HasComp<UnremoveableComponent>(hand.HeldEntity))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_hands.DoDrop(uid, hand, true, handsComponent);
|
||||||
|
freeHands++;
|
||||||
|
if (freeHands == 2)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_handVirtualItem.TrySpawnVirtualItemInHand(handcuff, uid, out var virtItem1))
|
||||||
|
EnsureComp<UnremoveableComponent>(virtItem1.Value);
|
||||||
|
|
||||||
|
if (_handVirtualItem.TrySpawnVirtualItemInHand(handcuff, uid, out var virtItem2))
|
||||||
|
EnsureComp<UnremoveableComponent>(virtItem2.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a set of cuffs to an existing CuffedComponent.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryAddNewCuffs(EntityUid target, EntityUid user, EntityUid handcuff, CuffableComponent? component = null, HandcuffComponent? cuff = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(target, ref component) || !Resolve(handcuff, ref cuff))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!_interaction.InRangeUnobstructed(handcuff, target))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Success!
|
||||||
|
_hands.TryDrop(user, handcuff);
|
||||||
|
|
||||||
|
component.Container.Insert(handcuff);
|
||||||
|
UpdateHeldItems(target, handcuff, component);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TryCuffing(EntityUid user, EntityUid target, EntityUid handcuff, HandcuffComponent? handcuffComponent = null, CuffableComponent? cuffable = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(handcuff, ref handcuffComponent) || !Resolve(target, ref cuffable, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (handcuffComponent.Cuffing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!TryComp<SharedHandsComponent?>(target, out var hands))
|
||||||
|
{
|
||||||
|
if (_net.IsServer)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-target-has-no-hands-error",
|
||||||
|
("targetName", Identity.Name(target, EntityManager, user))), user, user);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cuffable.CuffedHandCount >= hands.Count)
|
||||||
|
{
|
||||||
|
if (_net.IsServer)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-target-has-no-free-hands-error",
|
||||||
|
("targetName", Identity.Name(target, EntityManager, user))), user, user);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_net.IsServer)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-observer",
|
||||||
|
("user", Identity.Name(user, EntityManager)), ("target", Identity.Name(target, EntityManager))),
|
||||||
|
target, Filter.Pvs(target, entityManager: EntityManager)
|
||||||
|
.RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true);
|
||||||
|
|
||||||
|
if (target == user)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-target-self"), user, user);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-target-message",
|
||||||
|
("targetName", Identity.Name(target, EntityManager, user))), user, user);
|
||||||
|
_popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-by-other-message",
|
||||||
|
("otherName", Identity.Name(user, EntityManager, target))), target, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_audio.PlayPvs(handcuffComponent.StartCuffSound, handcuff);
|
||||||
|
|
||||||
|
var cuffTime = handcuffComponent.CuffTime;
|
||||||
|
|
||||||
|
if (HasComp<StunnedComponent>(target))
|
||||||
|
cuffTime = MathF.Max(0.1f, cuffTime - handcuffComponent.StunBonus);
|
||||||
|
|
||||||
|
if (HasComp<DisarmProneComponent>(target))
|
||||||
|
cuffTime = 0.0f; // cuff them instantly.
|
||||||
|
|
||||||
|
var doAfterEventArgs = new DoAfterEventArgs(user, cuffTime, default, target, handcuff)
|
||||||
|
{
|
||||||
|
RaiseOnUser = false,
|
||||||
|
RaiseOnTarget = false,
|
||||||
|
RaiseOnUsed = true,
|
||||||
|
BreakOnTargetMove = true,
|
||||||
|
BreakOnUserMove = true,
|
||||||
|
BreakOnDamage = true,
|
||||||
|
BreakOnStun = true,
|
||||||
|
NeedHand = true
|
||||||
|
};
|
||||||
|
|
||||||
|
handcuffComponent.Cuffing = true;
|
||||||
|
if (_net.IsServer)
|
||||||
|
_doAfter.DoAfter(doAfterEventArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
|
||||||
|
/// If the uncuffing succeeds, the cuffs will drop on the floor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="target"></param>
|
||||||
|
/// <param name="user">The cuffed entity</param>
|
||||||
|
/// <param name="cuffsToRemove">Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity.</param>
|
||||||
|
/// <param name="cuffable"></param>
|
||||||
|
/// <param name="cuff"></param>
|
||||||
|
public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove = null, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(target, ref cuffable))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (cuffable.Uncuffing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var isOwner = user == target;
|
||||||
|
|
||||||
|
if (cuffsToRemove == null)
|
||||||
|
{
|
||||||
|
if (cuffable.Container.ContainedEntities.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cuffsToRemove = cuffable.LastAddedCuffs;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!cuffable.Container.ContainedEntities.Contains(cuffsToRemove.Value))
|
||||||
|
{
|
||||||
|
Logger.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Resolve(cuffsToRemove.Value, ref cuff))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var attempt = new UncuffAttemptEvent(user, target);
|
||||||
|
RaiseLocalEvent(user, ref attempt, true);
|
||||||
|
|
||||||
|
if (attempt.Cancelled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOwner && !_interaction.InRangeUnobstructed(user, target))
|
||||||
|
{
|
||||||
|
if (_net.IsServer)
|
||||||
|
_popup.PopupEntity(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"), user, user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_net.IsServer)
|
||||||
|
_popup.PopupEntity(Loc.GetString("cuffable-component-start-removing-cuffs-message"), user, user);
|
||||||
|
|
||||||
|
_audio.PlayPredicted(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, target, user);
|
||||||
|
|
||||||
|
var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
|
||||||
|
var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime, default, target, cuffsToRemove)
|
||||||
|
{
|
||||||
|
RaiseOnTarget = true,
|
||||||
|
RaiseOnUsed = false,
|
||||||
|
RaiseOnUser = false,
|
||||||
|
BreakOnUserMove = true,
|
||||||
|
BreakOnTargetMove = true,
|
||||||
|
BreakOnDamage = true,
|
||||||
|
BreakOnStun = true,
|
||||||
|
NeedHand = true
|
||||||
|
};
|
||||||
|
|
||||||
|
cuffable.Uncuffing = true;
|
||||||
|
Dirty(cuffable);
|
||||||
|
if (_net.IsServer)
|
||||||
|
_doAfter.DoAfter(doAfterEventArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Uncuff(EntityUid target, EntityUid user, EntityUid cuffsToRemove, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(target, ref cuffable) || !Resolve(cuffsToRemove, ref cuff))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_audio.PlayPvs(cuff.EndUncuffSound, target);
|
||||||
|
|
||||||
|
cuffable.Container.Remove(cuffsToRemove);
|
||||||
|
|
||||||
|
if (cuff.BreakOnRemove)
|
||||||
|
{
|
||||||
|
QueueDel(cuffsToRemove);
|
||||||
|
var trash = Spawn(cuff.BrokenPrototype, Transform(cuffsToRemove).Coordinates);
|
||||||
|
_hands.PickupOrDrop(user, trash);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_hands.PickupOrDrop(user, cuffsToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only play popups on server because popups suck
|
||||||
|
if (_net.IsServer)
|
||||||
|
{
|
||||||
|
if (cuffable.CuffedHandCount == 0)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-success-message"), user, user);
|
||||||
|
|
||||||
|
if (target != user)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message",
|
||||||
|
("otherName", Identity.Name(user, EntityManager, user))), target, target);
|
||||||
|
_adminLog.Add(LogType.Action, LogImpact.Medium,
|
||||||
|
$"{ToPrettyString(user):player} has successfully uncuffed {ToPrettyString(target):player}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_adminLog.Add(LogType.Action, LogImpact.Medium,
|
||||||
|
$"{ToPrettyString(user):player} has successfully uncuffed themselves");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (user != target)
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
|
||||||
|
("cuffedHandCount", cuffable.CuffedHandCount),
|
||||||
|
("otherName", Identity.Name(user, EntityManager, user))), user, user);
|
||||||
|
_popup.PopupEntity(Loc.GetString(
|
||||||
|
"cuffable-component-remove-cuffs-by-other-partial-success-message",
|
||||||
|
("otherName", Identity.Name(user, EntityManager, user)),
|
||||||
|
("cuffedHandCount", cuffable.CuffedHandCount)), target, target);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
|
||||||
|
("cuffedHandCount", cuffable.CuffedHandCount)), user, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region ActionBlocker
|
#region ActionBlocker
|
||||||
|
|
||||||
private void CheckAct(EntityUid uid, SharedCuffableComponent component, CancellableEntityEventArgs args)
|
private void CheckAct(EntityUid uid, CuffableComponent component, CancellableEntityEventArgs args)
|
||||||
{
|
{
|
||||||
if (!component.CanStillInteract)
|
if (!component.CanStillInteract)
|
||||||
args.Cancel();
|
args.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEquipAttempt(EntityUid uid, SharedCuffableComponent component, IsEquippingAttemptEvent args)
|
private void OnEquipAttempt(EntityUid uid, CuffableComponent component, IsEquippingAttemptEvent args)
|
||||||
{
|
{
|
||||||
// is this a self-equip, or are they being stripped?
|
// is this a self-equip, or are they being stripped?
|
||||||
if (args.Equipee == uid)
|
if (args.Equipee == uid)
|
||||||
CheckAct(uid, component, args);
|
CheckAct(uid, component, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUnequipAttempt(EntityUid uid, SharedCuffableComponent component, IsUnequippingAttemptEvent args)
|
private void OnUnequipAttempt(EntityUid uid, CuffableComponent component, IsUnequippingAttemptEvent args)
|
||||||
{
|
{
|
||||||
// is this a self-equip, or are they being stripped?
|
// is this a self-equip, or are they being stripped?
|
||||||
if (args.Unequipee == uid)
|
if (args.Unequipee == uid)
|
||||||
@@ -129,5 +659,10 @@ namespace Content.Shared.Cuffs
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public IReadOnlyList<EntityUid> GetAllCuffs(CuffableComponent component)
|
||||||
|
{
|
||||||
|
return component.Container.ContainedEntities;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Content.Shared.Hands.Components;
|
using Content.Shared.Hands.Components;
|
||||||
|
using Content.Shared.Hands.EntitySystems;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Interaction;
|
||||||
using Content.Shared.Inventory.Events;
|
using Content.Shared.Inventory.Events;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
|
||||||
namespace Content.Shared.Hands;
|
namespace Content.Shared.Hands;
|
||||||
|
|
||||||
public abstract class SharedHandVirtualItemSystem : EntitySystem
|
public abstract class SharedHandVirtualItemSystem : EntitySystem
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly INetManager _net = default!;
|
||||||
|
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
@@ -14,6 +20,43 @@ public abstract class SharedHandVirtualItemSystem : EntitySystem
|
|||||||
SubscribeLocalEvent<HandVirtualItemComponent, BeforeRangedInteractEvent>(HandleBeforeInteract);
|
SubscribeLocalEvent<HandVirtualItemComponent, BeforeRangedInteractEvent>(HandleBeforeInteract);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user)
|
||||||
|
{
|
||||||
|
return TrySpawnVirtualItemInHand(blockingEnt, user, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user, [NotNullWhen(true)] out EntityUid? virtualItem)
|
||||||
|
{
|
||||||
|
if (!_hands.TryGetEmptyHand(user, out var hand))
|
||||||
|
{
|
||||||
|
virtualItem = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pos = Transform(user).Coordinates;
|
||||||
|
virtualItem = Spawn("HandVirtualItem", pos);
|
||||||
|
var virtualItemComp = EntityManager.GetComponent<HandVirtualItemComponent>(virtualItem.Value);
|
||||||
|
virtualItemComp.BlockingEntity = blockingEnt;
|
||||||
|
_hands.DoPickup(user, hand, virtualItem.Value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes all virtual items in a user's hands with
|
||||||
|
/// the specified blocked entity.
|
||||||
|
/// </summary>
|
||||||
|
public void DeleteInHandsMatching(EntityUid user, EntityUid matching)
|
||||||
|
{
|
||||||
|
foreach (var hand in _hands.EnumerateHands(user))
|
||||||
|
{
|
||||||
|
if (TryComp(hand.HeldEntity, out HandVirtualItemComponent? virt) && virt.BlockingEntity == matching)
|
||||||
|
{
|
||||||
|
Delete(virt, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnBeingEquippedAttempt(EntityUid uid, HandVirtualItemComponent component, BeingEquippedAttemptEvent args)
|
private void OnBeingEquippedAttempt(EntityUid uid, HandVirtualItemComponent component, BeingEquippedAttemptEvent args)
|
||||||
{
|
{
|
||||||
args.Cancel();
|
args.Cancel();
|
||||||
@@ -34,10 +77,10 @@ public abstract class SharedHandVirtualItemSystem : EntitySystem
|
|||||||
public void Delete(HandVirtualItemComponent comp, EntityUid user)
|
public void Delete(HandVirtualItemComponent comp, EntityUid user)
|
||||||
{
|
{
|
||||||
var userEv = new VirtualItemDeletedEvent(comp.BlockingEntity, user);
|
var userEv = new VirtualItemDeletedEvent(comp.BlockingEntity, user);
|
||||||
RaiseLocalEvent(user, userEv, false);
|
RaiseLocalEvent(user, userEv);
|
||||||
var targEv = new VirtualItemDeletedEvent(comp.BlockingEntity, user);
|
var targEv = new VirtualItemDeletedEvent(comp.BlockingEntity, user);
|
||||||
RaiseLocalEvent(comp.BlockingEntity, targEv, false);
|
RaiseLocalEvent(comp.BlockingEntity, targEv);
|
||||||
|
|
||||||
EntityManager.QueueDeleteEntity(comp.Owner);
|
QueueDel(comp.Owner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ handcuff-component-cuffs-broken-error = The cuffs are broken!
|
|||||||
handcuff-component-target-has-no-hands-error = {$targetName} has no hands!
|
handcuff-component-target-has-no-hands-error = {$targetName} has no hands!
|
||||||
handcuff-component-target-has-no-free-hands-error = {$targetName} has no free hands!
|
handcuff-component-target-has-no-free-hands-error = {$targetName} has no free hands!
|
||||||
handcuff-component-too-far-away-error = You are too far away to use the cuffs!
|
handcuff-component-too-far-away-error = You are too far away to use the cuffs!
|
||||||
|
handcuff-component-start-cuffing-observer = {$user} starts cuffing {$target}!
|
||||||
handcuff-component-start-cuffing-target-message = You start cuffing {$targetName}.
|
handcuff-component-start-cuffing-target-message = You start cuffing {$targetName}.
|
||||||
handcuff-component-start-cuffing-by-other-message = {$otherName} starts cuffing you!
|
handcuff-component-start-cuffing-by-other-message = {$otherName} starts cuffing you!
|
||||||
|
handcuff-component-cuff-observer-success-message = {$user} cuffs {$target}.
|
||||||
handcuff-component-cuff-other-success-message = You successfully cuff {$otherName}.
|
handcuff-component-cuff-other-success-message = You successfully cuff {$otherName}.
|
||||||
handcuff-component-cuff-by-other-success-message = You have been cuffed by {$otherName}!
|
handcuff-component-cuff-by-other-success-message = You have been cuffed by {$otherName}!
|
||||||
handcuff-component-cuff-self-success-message = You cuff yourself.
|
handcuff-component-cuff-self-success-message = You cuff yourself.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
size: 3
|
size: 3
|
||||||
- type: Handcuff
|
- type: Handcuff
|
||||||
cuffedRSI: Objects/Misc/handcuffs.rsi
|
cuffedRSI: Objects/Misc/handcuffs.rsi
|
||||||
iconState: body-overlay
|
bodyIconState: body-overlay
|
||||||
- type: Sprite
|
- type: Sprite
|
||||||
sprite: Objects/Misc/handcuffs.rsi
|
sprite: Objects/Misc/handcuffs.rsi
|
||||||
state: handcuff
|
state: handcuff
|
||||||
|
|||||||
Reference in New Issue
Block a user