Handcuff system (#1831)

* Implemented most serverside logic

* All serverside cuff logic complete

* SFX, Clientside HUD stuff, Other logic.

* fffff

* Cuffs 1.0

* missing loc string

* Cuffs are stored in the balls now.

* Basic integrationtest

* Support stripping menu.

* rrr

* Fixes

* properties

* gun emoji

* fixes

* get rid of unused

* reeee

* Update Content.Shared/GameObjects/ContentNetIDs.cs

Co-authored-by: Víctor Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
This commit is contained in:
nuke
2020-08-25 08:54:23 -04:00
committed by GitHub
parent 6b56297c69
commit a62935dab2
44 changed files with 1085 additions and 36 deletions

View File

@@ -0,0 +1,58 @@
using Robust.Client.Graphics;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Shared.IoC;
using Robust.Shared.GameObjects;
using Content.Shared.GameObjects.Components.ActionBlocking;
using Content.Shared.Preferences.Appearance;
using Robust.Client.GameObjects;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components.ActionBlocking
{
[RegisterComponent]
public class CuffableComponent : SharedCuffableComponent
{
[ViewVariables]
private string _currentRSI = default;
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{
if (!(curState is CuffableComponentState cuffState))
{
return;
}
CanStillInteract = cuffState.CanStillInteract;
if (Owner.TryGetComponent<SpriteComponent>(out var sprite))
{
sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, cuffState.NumHandsCuffed > 0);
sprite.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;
sprite.LayerSetState(HumanoidVisualLayers.Handcuffs, new RSI.StateId(cuffState.IconState), new ResourcePath(cuffState.RSI));
}
else
{
sprite.LayerSetState(HumanoidVisualLayers.Handcuffs, new RSI.StateId(cuffState.IconState)); // TODO: safety check to see if RSI contains the state?
}
}
}
}
public override void OnRemove()
{
base.OnRemove();
if (Owner.TryGetComponent<SpriteComponent>(out var sprite))
{
sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false);
}
}
}
}

View File

@@ -0,0 +1,27 @@
using Robust.Shared.GameObjects;
using Content.Shared.GameObjects.Components.ActionBlocking;
using Robust.Client.Graphics;
using Robust.Client.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Client.GameObjects.Components.ActionBlocking
{
[RegisterComponent]
public class HandcuffComponent : SharedHandcuffComponent
{
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{
var cuffState = curState as HandcuffedComponentState;
if (cuffState == null || cuffState.IconState == string.Empty)
{
return;
}
if (Owner.TryGetComponent<SpriteComponent>(out var sprite))
{
sprite.LayerSetState(0, new RSI.StateId(cuffState.IconState)); // TODO: safety check to see if RSI contains the state?
}
}
}
}

View File

@@ -4,8 +4,10 @@ using Content.Shared.GameObjects.Components.GUI;
using Content.Shared.GameObjects.Components.Inventory;
using JetBrains.Annotations;
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.UserInterface;
using Robust.Shared.ViewVariables;
using Robust.Shared.Localization;
using static Content.Shared.GameObjects.Components.Inventory.EquipmentSlotDefines;
namespace Content.Client.GameObjects.Components.HUD.Inventory
@@ -15,6 +17,7 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
{
public Dictionary<Slots, string> Inventory { get; private set; }
public Dictionary<string, string> Hands { get; private set; }
public Dictionary<EntityUid, string> Handcuffs { get; private set; }
[ViewVariables]
private StrippingMenu _strippingMenu;
@@ -49,7 +52,8 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
_strippingMenu.ClearButtons();
if(Inventory != null)
if (Inventory != null)
{
foreach (var (slot, name) in Inventory)
{
_strippingMenu.AddButton(EquipmentSlotDefines.SlotNames[slot], name, (ev) =>
@@ -57,8 +61,10 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
SendMessage(new StrippingInventoryButtonPressed(slot));
});
}
}
if(Hands != null)
if (Hands != null)
{
foreach (var (hand, name) in Hands)
{
_strippingMenu.AddButton(hand, name, (ev) =>
@@ -66,6 +72,18 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
SendMessage(new StrippingHandButtonPressed(hand));
});
}
}
if (Handcuffs != null)
{
foreach (var (id, name) in Handcuffs)
{
_strippingMenu.AddButton(Loc.GetString("Restraints"), name, (ev) =>
{
SendMessage(new StrippingHandcuffButtonPressed(id));
});
}
}
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -76,6 +94,7 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
Inventory = stripState.Inventory;
Hands = stripState.Hands;
Handcuffs = stripState.Handcuffs;
UpdateMenu();
}

View File

@@ -156,7 +156,8 @@ namespace Content.Client.GameObjects.Components.Items
}
else
{
var (rsi, state) = maybeInHands.Value;
var (rsi, state, color) = maybeInHands.Value;
_sprite.LayerSetColor($"hand-{name}", color);
_sprite.LayerSetVisible($"hand-{name}", true);
_sprite.LayerSetState($"hand-{name}", state, rsi);
}

View File

@@ -12,6 +12,7 @@ using Robust.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components.Items
@@ -25,6 +26,8 @@ namespace Content.Client.GameObjects.Components.Items
[ViewVariables] protected ResourcePath RsiPath;
[ViewVariables(VVAccess.ReadWrite)] protected Color Color;
private string _equippedPrefix;
[ViewVariables(VVAccess.ReadWrite)]
@@ -40,7 +43,7 @@ namespace Content.Client.GameObjects.Components.Items
}
}
public (RSI rsi, RSI.StateId stateId)? GetInHandStateInfo(HandLocation hand)
public (RSI rsi, RSI.StateId stateId, Color color)? GetInHandStateInfo(HandLocation hand)
{
if (RsiPath == null)
{
@@ -52,7 +55,7 @@ namespace Content.Client.GameObjects.Components.Items
var stateId = EquippedPrefix != null ? $"{EquippedPrefix}-inhand-{handName}" : $"inhand-{handName}";
if (rsi.TryGetState(stateId, out _))
{
return (rsi, stateId);
return (rsi, stateId, Color);
}
return null;
@@ -62,6 +65,7 @@ namespace Content.Client.GameObjects.Components.Items
{
base.ExposeData(serializer);
serializer.DataFieldCached(ref Color, "color", Color.White);
serializer.DataFieldCached(ref RsiPath, "sprite", null);
serializer.DataFieldCached(ref _equippedPrefix, "HeldPrefix", null);
}

View File

@@ -1,8 +1,9 @@
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Appearance;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Content.Client.GameObjects.Components.ActionBlocking;
namespace Content.Client.GameObjects.Components.Mobs
{
@@ -49,6 +50,15 @@ namespace Content.Client.GameObjects.Components.Mobs
sprite.LayerSetVisible(HumanoidVisualLayers.StencilMask, Sex == Sex.Female);
if (Owner.TryGetComponent<CuffableComponent>(out var cuffed))
{
sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, !cuffed.CanStillInteract);
}
else
{
sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false);
}
var hairStyle = Appearance.HairStyleName;
if (string.IsNullOrWhiteSpace(hairStyle) || !HairStyles.HairStylesMap.ContainsKey(hairStyle))
hairStyle = HairStyles.DefaultHairStyle;

View File

@@ -0,0 +1,93 @@
#nullable enable
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Content.Server.GameObjects.Components.ActionBlocking;
using System.Linq;
using Content.Server.GameObjects.Components.Body;
using Content.Shared.Body.Part;
using Content.Shared.GameObjects.Components.Body;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Robust.Shared.Prototypes;
using Content.Server.Body;
using Content.Client.GameObjects.Components.Items;
namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
{
[TestFixture]
[TestOf(typeof(CuffableComponent))]
[TestOf(typeof(HandcuffComponent))]
public class CuffUnitTest : ContentIntegrationTest
{
[Test]
public async Task Test()
{
var server = StartServerDummyTicker();
IEntity human;
IEntity otherHuman;
IEntity cuffs;
IEntity cables;
HandcuffComponent cableHandcuff;
HandcuffComponent handcuff;
CuffableComponent cuffed;
IHandsComponent hands;
BodyManagerComponent body;
server.Assert(() =>
{
var mapManager = IoCManager.Resolve<IMapManager>();
mapManager.CreateNewMapEntity(MapId.Nullspace);
var entityManager = IoCManager.Resolve<IEntityManager>();
// Spawn the entities
human = entityManager.SpawnEntity("BaseHumanMob_Content", MapCoordinates.Nullspace);
otherHuman = entityManager.SpawnEntity("BaseHumanMob_Content", MapCoordinates.Nullspace);
cuffs = entityManager.SpawnEntity("Handcuffs", MapCoordinates.Nullspace);
cables = entityManager.SpawnEntity("Cablecuffs", MapCoordinates.Nullspace);
human.Transform.WorldPosition = otherHuman.Transform.WorldPosition;
// Test for components existing
Assert.True(human.TryGetComponent(out cuffed!), $"Human has no {nameof(CuffableComponent)}");
Assert.True(human.TryGetComponent(out hands!), $"Human has no {nameof(HandsComponent)}");
Assert.True(human.TryGetComponent(out body!), $"Human has no {nameof(BodyManagerComponent)}");
Assert.True(cuffs.TryGetComponent(out handcuff!), $"Handcuff has no {nameof(HandcuffComponent)}");
Assert.True(cables.TryGetComponent(out cableHandcuff!), $"Cablecuff has no {nameof(HandcuffComponent)}");
// Test to ensure cuffed players register the handcuffs
cuffed.AddNewCuffs(cuffs);
Assert.True(cuffed.CuffedHandCount > 0, "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
AddHand(body);
AddHand(body);
Assert.True(cuffed.CuffedHandCount == 2 && hands.Hands.Count() == 4, "Player doesn't have correct amount of hands cuffed");
// Test to give a player with 4 hands 2 sets of cuffs
cuffed.AddNewCuffs(cables);
Assert.True(cuffed.CuffedHandCount == 4, "Player doesn't have correct amount of hands cuffed");
});
await server.WaitIdleAsync();
}
private void AddHand(BodyManagerComponent body)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.TryIndex("bodyPart.LHand.BasicHuman", out BodyPartPrototype prototype);
var part = new BodyPart(prototype);
var slot = part.GetHashCode().ToString();
body.Template.Slots.Add(slot, BodyPartType.Hand);
body.InstallBodyPart(part, slot);
}
}
}

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System.Linq;
using Content.Server.GameObjects.Components.Body;
using Content.Shared.Body.Part;
@@ -42,7 +42,7 @@ namespace Content.Server.Body
}
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.TryIndex("bodyPart.Hand.BasicHuman", out BodyPartPrototype prototype);
prototypeManager.TryIndex("bodyPart.LHand.BasicHuman", out BodyPartPrototype prototype);
var part = new BodyPart(prototype);
var slot = part.GetHashCode().ToString();

View File

@@ -0,0 +1,351 @@

using Robust.Server.GameObjects;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Robust.Shared.ViewVariables;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.ActionBlocking;
using Content.Shared.GameObjects.Verbs;
using Content.Server.GameObjects.Components.Items.Storage;
using Robust.Shared.Log;
using System.Linq;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.EntitySystems;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Shared.Maths;
using System;
using System.Collections.Generic;
using Serilog;
namespace Content.Server.GameObjects.Components.ActionBlocking
{
[RegisterComponent]
public class CuffableComponent : SharedCuffableComponent
{
[Dependency]
private readonly ISharedNotifyManager _notifyManager;
/// <summary>
/// How many of this entity's hands are currently cuffed.
/// </summary>
[ViewVariables]
public int CuffedHandCount => _container.ContainedEntities.Count * 2;
protected IEntity LastAddedCuffs => _container.ContainedEntities[_container.ContainedEntities.Count - 1];
public IReadOnlyList<IEntity> StoredEntities => _container.ContainedEntities;
/// <summary>
/// Container of various handcuffs currently applied to the entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
private Container _container = default!;
private bool _dirtyThisFrame = false;
private float _interactRange;
private IHandsComponent _hands;
public event Action OnCuffedStateChanged;
public override void Initialize()
{
base.Initialize();
_container = ContainerManagerComponent.Ensure<Container>(Name, Owner);
_interactRange = SharedInteractionSystem.InteractionRange / 2;
if (!Owner.TryGetComponent(out _hands))
{
Logger.Warning("Player does not have an IHandsComponent!");
}
}
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 (LastAddedCuffs.TryGetComponent<HandcuffComponent>(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>
/// <param name="prototype"></param>
public void AddNewCuffs(IEntity handcuff)
{
if (!handcuff.HasComponent<HandcuffComponent>())
{
Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!");
return;
}
if (!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(
handcuff.Transform.MapPosition,
Owner.Transform.MapPosition,
_interactRange,
ignoredEnt: Owner))
{
Logger.Warning("Handcuffs being applied to player are obstructed or too far away! This should not happen!");
return;
}
_container.Insert(handcuff);
CanStillInteract = _hands.Hands.Count() > CuffedHandCount;
OnCuffedStateChanged.Invoke();
UpdateStatusEffect();
UpdateHeldItems();
Dirty();
}
public void Update(float frameTime)
{
UpdateHandCount();
}
/// <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 UpdateHandCount()
{
_dirtyThisFrame = false;
var handCount = _hands.Hands.Count();
while (CuffedHandCount > handCount && CuffedHandCount > 0)
{
_dirtyThisFrame = true;
var entity = _container.ContainedEntities[_container.ContainedEntities.Count - 1];
_container.Remove(entity);
entity.Transform.WorldPosition = Owner.Transform.GridPosition.Position;
}
if (_dirtyThisFrame)
{
CanStillInteract = handCount > CuffedHandCount;
OnCuffedStateChanged.Invoke();
Dirty();
}
}
/// <summary>
/// Check how many items the user is holding and if it's more than the number of cuffed hands, drop some items.
/// </summary>
public void UpdateHeldItems()
{
var itemCount = _hands.GetAllHeldItems().Count();
var freeHandCount = _hands.Hands.Count() - CuffedHandCount;
if (freeHandCount < itemCount)
{
foreach (ItemComponent item in _hands.GetAllHeldItems())
{
if (freeHandCount < itemCount)
{
freeHandCount++;
_hands.Drop(item.Owner);
}
else
{
break;
}
}
}
}
/// <summary>
/// Updates the status effect indicator on the HUD.
/// </summary>
private void UpdateStatusEffect()
{
if (Owner.TryGetComponent(out ServerStatusEffectsComponent status))
{
status.ChangeStatusEffectIcon(StatusEffect.Cuffed,
CanStillInteract ? "/Textures/Interface/StatusEffects/Handcuffed/Uncuffed.png" : "/Textures/Interface/StatusEffects/Handcuffed/Handcuffed.png");
}
}
/// <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(IEntity user, IEntity cuffsToRemove = null)
{
var isOwner = user == Owner;
if (cuffsToRemove == null)
{
cuffsToRemove = LastAddedCuffs;
}
else
{
if (!_container.ContainedEntities.Contains(cuffsToRemove))
{
Logger.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
}
}
if (!cuffsToRemove.TryGetComponent<HandcuffComponent>(out var cuff))
{
Logger.Warning($"A user is trying to remove handcuffs without a {nameof(HandcuffComponent)}. This should never happen!");
return;
}
if (!isOwner && !ActionBlockerSystem.CanInteract(user))
{
user.PopupMessage(user, "You can't do that!");
return;
}
if (!isOwner &&
!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(
user.Transform.MapPosition,
Owner.Transform.MapPosition,
_interactRange,
ignoredEnt: Owner))
{
user.PopupMessage(user, "You are too far away to remove the cuffs.");
return;
}
if (!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(
cuffsToRemove.Transform.MapPosition,
Owner.Transform.MapPosition,
_interactRange,
ignoredEnt: Owner))
{
Logger.Warning("Handcuffs being removed from player are obstructed or too far away! This should not happen!");
return;
}
user.PopupMessage(user, "You start removing the cuffs.");
var audio = EntitySystem.Get<AudioSystem>();
audio.PlayFromEntity(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, Owner);
var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime)
{
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
NeedHand = true
};
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
var result = await doAfterSystem.DoAfter(doAfterEventArgs);
if (result != DoAfterStatus.Cancelled)
{
audio.PlayFromEntity(cuff.EndUncuffSound, Owner);
_container.ForceRemove(cuffsToRemove);
cuffsToRemove.Transform.AttachToGridOrMap();
cuffsToRemove.Transform.WorldPosition = Owner.Transform.WorldPosition;
if (cuff.BreakOnRemove)
{
cuff.Broken = true;
cuffsToRemove.Name = cuff.BrokenName;
cuffsToRemove.Description = cuff.BrokenDesc;
if (cuffsToRemove.TryGetComponent<SpriteComponent>(out var sprite))
{
sprite.LayerSetState(0, cuff.BrokenState); // TODO: safety check to see if RSI contains the state?
}
}
CanStillInteract = _hands.Hands.Count() > CuffedHandCount;
OnCuffedStateChanged.Invoke();
UpdateStatusEffect();
Dirty();
if (CuffedHandCount == 0)
{
_notifyManager.PopupMessage(user, user, Loc.GetString("You successfully remove the cuffs."));
if (!isOwner)
{
_notifyManager.PopupMessage(user, Owner, Loc.GetString("{0:theName} uncuffs your hands.", user));
}
}
else
{
if (!isOwner)
{
_notifyManager.PopupMessage(user, user, Loc.GetString("You successfully remove the cuffs. {0} of {0:theName}'s hands remain cuffed.", CuffedHandCount, user));
_notifyManager.PopupMessage(user, Owner, Loc.GetString("{0:theName} removes your cuffs. {1} of your hands remain cuffed.", user, CuffedHandCount));
}
else
{
_notifyManager.PopupMessage(user, user, Loc.GetString("You successfully remove the cuffs. {0} of your hands remain cuffed.", CuffedHandCount));
}
}
}
else
{
_notifyManager.PopupMessage(user, user, Loc.GetString("You fail to remove the cuffs."));
}
return;
}
/// <summary>
/// Allows the uncuffing of a cuffed person. Used by other people and by the component owner to break out of cuffs.
/// </summary>
[Verb]
private sealed class UncuffVerb : Verb<CuffableComponent>
{
protected override void GetData(IEntity user, CuffableComponent component, VerbData data)
{
if ((user != component.Owner && !ActionBlockerSystem.CanInteract(user)) || component.CuffedHandCount == 0)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Uncuff");
}
protected override void Activate(IEntity user, CuffableComponent component)
{
if (component.CuffedHandCount > 0)
{
component.TryUncuff(user);
}
}
}
}
}

View File

@@ -0,0 +1,248 @@
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Content.Server.GameObjects.Components.GUI;
using Robust.Shared.Serialization;
using Robust.Shared.Log;
using Robust.Shared.Localization;
using Robust.Shared.ViewVariables;
using Robust.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.ActionBlocking;
using Content.Server.GameObjects.Components.Mobs;
using Robust.Shared.Maths;
using System;
namespace Content.Server.GameObjects.Components.ActionBlocking
{
[RegisterComponent]
public class HandcuffComponent : SharedHandcuffComponent, IAfterInteract
{
[Dependency]
private readonly ISharedNotifyManager _notifyManager;
/// <summary>
/// The time it takes to apply a <see cref="CuffedComponent"/> to an entity.
/// </summary>
[ViewVariables]
public float CuffTime { get; set; }
/// <summary>
/// The time it takes to remove a <see cref="CuffedComponent"/> from an entity.
/// </summary>
[ViewVariables]
public float UncuffTime { get; set; }
/// <summary>
/// The time it takes for a cuffed entity to remove <see cref="CuffedComponent"/> from itself.
/// </summary>
[ViewVariables]
public float BreakoutTime { get; set; }
/// <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>
[ViewVariables]
public float StunBonus { get; set; }
/// <summary>
/// Will the cuffs break when removed?
/// </summary>
[ViewVariables]
public bool BreakOnRemove { get; set; }
/// <summary>
/// The path of the RSI file used for the player cuffed overlay.
/// </summary>
[ViewVariables]
public string CuffedRSI { get; set; }
/// <summary>
/// The iconstate used with the RSI file for the player cuffed overlay.
/// </summary>
[ViewVariables]
public string OverlayIconState { get; set; }
/// <summary>
/// The iconstate used for broken handcuffs
/// </summary>
[ViewVariables]
public string BrokenState { get; set; }
/// <summary>
/// The iconstate used for broken handcuffs
/// </summary>
[ViewVariables]
public string BrokenName { get; set; }
/// <summary>
/// The iconstate used for broken handcuffs
/// </summary>
[ViewVariables]
public string BrokenDesc { get; set; }
[ViewVariables]
public bool Broken
{
get
{
return _isBroken;
}
set
{
if (_isBroken != value)
{
_isBroken = value;
Dirty();
}
}
}
public string StartCuffSound { get; set; }
public string EndCuffSound { get; set; }
public string StartBreakoutSound { get; set; }
public string StartUncuffSound { get; set; }
public string EndUncuffSound { get; set; }
public Color Color { get; set; }
// Non-exposed data fields
private bool _isBroken = false;
private float _interactRange;
private DoAfterSystem _doAfterSystem;
private AudioSystem _audioSystem;
public override void Initialize()
{
base.Initialize();
_audioSystem = EntitySystem.Get<AudioSystem>();
_doAfterSystem = EntitySystem.Get<DoAfterSystem>();
_interactRange = SharedInteractionSystem.InteractionRange / 2;
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.CuffTime, "cuffTime", 5.0f);
serializer.DataField(this, x => x.BreakoutTime, "breakoutTime", 30.0f);
serializer.DataField(this, x => x.UncuffTime, "uncuffTime", 5.0f);
serializer.DataField(this, x => x.StunBonus, "stunBonus", 2.0f);
serializer.DataField(this, x => x.StartCuffSound, "startCuffSound", "/Audio/Items/Handcuffs/cuff_start.ogg");
serializer.DataField(this, x => x.EndCuffSound, "endCuffSound", "/Audio/Items/Handcuffs/cuff_end.ogg");
serializer.DataField(this, x => x.StartUncuffSound, "startUncuffSound", "/Audio/Items/Handcuffs/cuff_takeoff_start.ogg");
serializer.DataField(this, x => x.EndUncuffSound, "endUncuffSound", "/Audio/Items/Handcuffs/cuff_takeoff_end.ogg");
serializer.DataField(this, x => x.StartBreakoutSound, "startBreakoutSound", "/Audio/Items/Handcuffs/cuff_breakout_start.ogg");
serializer.DataField(this, x => x.CuffedRSI, "cuffedRSI", "Objects/Misc/handcuffs.rsi");
serializer.DataField(this, x => x.OverlayIconState, "bodyIconState", "body-overlay");
serializer.DataField(this, x => x.Color, "color", Color.White);
serializer.DataField(this, x => x.BreakOnRemove, "breakOnRemove", false);
serializer.DataField(this, x => x.BrokenState, "brokenIconState", string.Empty);
serializer.DataField(this, x => x.BrokenName, "brokenName", string.Empty);
serializer.DataField(this, x => x.BrokenDesc, "brokenDesc", string.Empty);
}
public override ComponentState GetComponentState()
{
return new HandcuffedComponentState(Broken ? BrokenState : string.Empty);
}
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null || !ActionBlockerSystem.CanUse(eventArgs.User) || !eventArgs.Target.TryGetComponent<CuffableComponent>(out var cuffed))
{
return;
}
if (eventArgs.Target == eventArgs.User)
{
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("You can't cuff yourself!"));
return;
}
if (Broken)
{
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("The cuffs are broken!"));
return;
}
if (!eventArgs.Target.TryGetComponent<HandsComponent>(out var hands))
{
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("{0:theName} has no hands!", eventArgs.Target));
return;
}
if (cuffed.CuffedHandCount == hands.Count)
{
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("{0:theName} has no free hands to handcuff!", eventArgs.Target));
return;
}
if (!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(
eventArgs.User.Transform.MapPosition,
eventArgs.Target.Transform.MapPosition,
_interactRange,
ignoredEnt: Owner))
{
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("You are too far away to use the cuffs!"));
return;
}
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("You start cuffing {0:theName}.", eventArgs.Target));
_notifyManager.PopupMessage(eventArgs.User, eventArgs.Target, Loc.GetString("{0:theName} starts cuffing you!", eventArgs.User));
_audioSystem.PlayFromEntity(StartCuffSound, Owner);
TryUpdateCuff(eventArgs.User, eventArgs.Target, cuffed);
}
/// <summary>
/// Update the cuffed state of an entity
/// </summary>
private async void TryUpdateCuff(IEntity user, IEntity target, CuffableComponent cuffs)
{
var cuffTime = CuffTime;
if (target.TryGetComponent<StunnableComponent>(out var stun) && stun.Stunned)
{
cuffTime = MathF.Max(0.1f, cuffTime - StunBonus);
}
var doAfterEventArgs = new DoAfterEventArgs(user, cuffTime, default, target)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
NeedHand = true
};
var result = await _doAfterSystem.DoAfter(doAfterEventArgs);
if (result != DoAfterStatus.Cancelled)
{
_audioSystem.PlayFromEntity(EndCuffSound, Owner);
_notifyManager.PopupMessage(user, user, Loc.GetString("You successfully cuff {0}.", target.Name));
_notifyManager.PopupMessage(target, target, Loc.GetString("You have been cuffed by {0}!", user.Name));
if (user.TryGetComponent<HandsComponent>(out var hands))
{
hands.Drop(Owner);
cuffs.AddNewCuffs(Owner);
}
else
{
Logger.Warning("Unable to remove handcuffs from player's hands! This should not happen!");
}
}
else
{
user.PopupMessage(user, Loc.GetString("You were interrupted while cuffing {0}!", target.Name));
target.PopupMessage(target, Loc.GetString("You interrupt {0} while they are cuffing you!", user.Name));
}
}
}
}

View File

@@ -1,6 +1,8 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Content.Server.GameObjects.Components.ActionBlocking;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Server.Interfaces;
@@ -28,7 +30,8 @@ namespace Content.Server.GameObjects.Components.GUI
public const float StripDelay = 2f;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(StrippingUiKey.Key);
[ViewVariables]
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(StrippingUiKey.Key);
public override void Initialize()
{
@@ -39,6 +42,14 @@ namespace Content.Server.GameObjects.Components.GUI
UserInterface.OnReceiveMessage += HandleUserInterfaceMessage;
}
Owner.EnsureComponent<InventoryComponent>();
Owner.EnsureComponent<HandsComponent>();
Owner.EnsureComponent<CuffableComponent>();
if (Owner.TryGetComponent(out CuffableComponent? cuffed))
{
cuffed.OnCuffedStateChanged += UpdateSubscribed;
}
if (Owner.TryGetComponent(out InventoryComponent? inventory))
{
inventory.OnItemChanged += UpdateSubscribed;
@@ -62,8 +73,9 @@ namespace Content.Server.GameObjects.Components.GUI
var inventory = GetInventorySlots();
var hands = GetHandSlots();
var cuffs = GetHandcuffs();
UserInterface.SetState(new StrippingBoundUserInterfaceState(inventory, hands));
UserInterface.SetState(new StrippingBoundUserInterfaceState(inventory, hands, cuffs));
}
public bool CanDragDrop(DragDropEventArgs eventArgs)
@@ -80,6 +92,23 @@ namespace Content.Server.GameObjects.Components.GUI
return true;
}
private Dictionary<EntityUid, string> GetHandcuffs()
{
var dictionary = new Dictionary<EntityUid, string>();
if (!Owner.TryGetComponent(out CuffableComponent? cuffed))
{
return dictionary;
}
foreach (IEntity entity in cuffed.StoredEntities)
{
dictionary.Add(entity.Uid, entity.Name);
}
return dictionary;
}
private Dictionary<Slots, string> GetInventorySlots()
{
var dictionary = new Dictionary<Slots, string>();
@@ -360,26 +389,46 @@ namespace Content.Server.GameObjects.Components.GUI
switch (obj.Message)
{
case StrippingInventoryButtonPressed inventoryMessage:
var inventory = Owner.GetComponent<InventoryComponent>();
if (inventory.TryGetSlotItem(inventoryMessage.Slot, out ItemComponent _))
placingItem = false;
if (Owner.TryGetComponent<InventoryComponent>(out var inventory))
{
if (inventory.TryGetSlotItem(inventoryMessage.Slot, out ItemComponent _))
placingItem = false;
if(placingItem)
PlaceActiveHandItemInInventory(user, inventoryMessage.Slot);
else
TakeItemFromInventory(user, inventoryMessage.Slot);
if (placingItem)
PlaceActiveHandItemInInventory(user, inventoryMessage.Slot);
else
TakeItemFromInventory(user, inventoryMessage.Slot);
}
break;
case StrippingHandButtonPressed handMessage:
var hands = Owner.GetComponent<HandsComponent>();
if (hands.TryGetItem(handMessage.Hand, out _))
placingItem = false;
if (Owner.TryGetComponent<HandsComponent>(out var hands))
{
if (hands.TryGetItem(handMessage.Hand, out _))
placingItem = false;
if(placingItem)
PlaceActiveHandItemInHands(user, handMessage.Hand);
else
TakeItemFromHands(user, handMessage.Hand);
if (placingItem)
PlaceActiveHandItemInHands(user, handMessage.Hand);
else
TakeItemFromHands(user, handMessage.Hand);
}
break;
case StrippingHandcuffButtonPressed handcuffMessage:
if (Owner.TryGetComponent<CuffableComponent>(out var cuffed))
{
foreach (var entity in cuffed.StoredEntities)
{
if (entity.Uid == handcuffMessage.Handcuff)
{
cuffed.TryUncuff(user, entity);
return;
}
}
}
break;
}
}

View File

@@ -61,7 +61,6 @@ namespace Content.Server.GameObjects.Components.Items.Clothing
});
serializer.DataField(ref _quickEquipEnabled, "QuickEquip", true);
serializer.DataFieldCached(ref _heatResistance, "HeatResistance", 323);
}

View File

@@ -1,4 +1,4 @@
using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.GameObjects.EntitySystems;

View File

@@ -0,0 +1,18 @@
using Content.Server.GameObjects.Components.ActionBlocking;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class CuffSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<CuffableComponent>())
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -2,10 +2,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Content.Server.GameObjects;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.Components.Items;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.Components.Container;

View File

@@ -0,0 +1,49 @@
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
using System;
namespace Content.Shared.GameObjects.Components.ActionBlocking
{
public class SharedCuffableComponent : Component, IActionBlocker
{
public override string Name => "Cuffable";
public override uint? NetID => ContentNetIDs.CUFFED;
[ViewVariables]
public bool CanStillInteract = true;
#region ActionBlockers
bool IActionBlocker.CanInteract() => CanStillInteract;
bool IActionBlocker.CanUse() => CanStillInteract;
bool IActionBlocker.CanPickup() => CanStillInteract;
bool IActionBlocker.CanDrop() => CanStillInteract;
bool IActionBlocker.CanAttack() => CanStillInteract;
bool IActionBlocker.CanEquip() => CanStillInteract;
bool IActionBlocker.CanUnequip() => CanStillInteract;
#endregion
[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) : base(ContentNetIDs.CUFFED)
{
NumHandsCuffed = numHandsCuffed;
CanStillInteract = canStillInteract;
RSI = rsiPath;
IconState = iconState;
Color = color;
}
}
}
}

View File

@@ -0,0 +1,23 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using System;
namespace Content.Shared.GameObjects.Components.ActionBlocking
{
public class SharedHandcuffComponent : Component
{
public override string Name => "Handcuff";
public override uint? NetID => ContentNetIDs.HANDCUFFS;
[Serializable, NetSerializable]
protected sealed class HandcuffedComponentState : ComponentState
{
public string IconState { get; }
public HandcuffedComponentState(string iconState) : base(ContentNetIDs.HANDCUFFS)
{
IconState = iconState;
}
}
}
}

View File

@@ -41,16 +41,29 @@ namespace Content.Shared.GameObjects.Components.GUI
}
}
[NetSerializable, Serializable]
public class StrippingHandcuffButtonPressed : BoundUserInterfaceMessage
{
public EntityUid Handcuff { get; }
public StrippingHandcuffButtonPressed(EntityUid handcuff)
{
Handcuff = handcuff;
}
}
[NetSerializable, Serializable]
public class StrippingBoundUserInterfaceState : BoundUserInterfaceState
{
public Dictionary<Slots, string> Inventory { get; }
public Dictionary<string, string> Hands { get; }
public Dictionary<EntityUid, string> Handcuffs { get; }
public StrippingBoundUserInterfaceState(Dictionary<Slots, string> inventory, Dictionary<string, string> hands)
public StrippingBoundUserInterfaceState(Dictionary<Slots, string> inventory, Dictionary<string, string> hands, Dictionary<EntityUid, string> handcuffs)
{
Inventory = inventory;
Hands = hands;
Handcuffs = handcuffs;
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
@@ -58,6 +58,7 @@ namespace Content.Shared.GameObjects.Components.Mobs
Thirst,
Pressure,
Stun,
Cuffed,
Buckled,
Piloting,
Pulling,

View File

@@ -68,6 +68,8 @@
public const uint BOLTACTION_BARREL = 1062;
public const uint PUMP_BARREL = 1063;
public const uint REVOLVER_BARREL = 1064;
public const uint CUFFED = 1065;
public const uint HANDCUFFS = 1066;
// Net IDs for integration tests.
public const uint PREDICTION_TEST = 10001;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.Preferences.Appearance
@@ -19,6 +19,7 @@ namespace Content.Shared.Preferences.Appearance
LLeg,
RFoot,
LFoot,
Handcuffs,
StencilMask
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -90,6 +90,10 @@
color: "#e8b59b"
sprite: Mobs/Species/Human/parts.rsi
state: r_foot
- map: ["enum.HumanoidVisualLayers.Handcuffs"]
color: "#ffffff"
sprite: Objects/Misc/handcuffs.rsi
state: body-overlay-2
- map: ["enum.Slots.IDCARD"]
- map: ["enum.Slots.GLOVES"]
- map: ["enum.Slots.SHOES"]
@@ -144,6 +148,7 @@
- type: BuckleVisualizer
- type: CombatMode
- type: Climbing
- type: Cuffable
- type: Teleportable
- type: CharacterInfo
- type: FootstepSound

View File

@@ -1,9 +1,17 @@
- type: entity
name: handcuffs
description: Just a prop for screenshots for now, sorry!
description: Used to detain criminals and other assholes.
id: Handcuffs
parent: BaseItem
components:
- type: Handcuff
cuffTime: 3.0
uncuffTime: 3.0
stunBonus: 2.0
breakoutTime: 20.0
cuffedRSI: Objects/Misc/handcuffs.rsi
iconState: body-overlay
- type: Sprite
sprite: Objects/Misc/handcuffs.rsi
state: handcuff
@@ -18,10 +26,29 @@
- type: entity
name: cable restraints
name: makeshift handcuffs
description: Homemade handcuffs crafted from spare cables.
id: Cablecuffs
parent: Handcuffs
components:
- type: Handcuff
cuffTime: 3.5
uncuffTime: 3.5
stunBonus: 2.0
breakoutTime: 15.0
cuffedRSI: Objects/Misc/cablecuffs.rsi
bodyIconState: body-overlay
color: red
breakOnRemove: true
brokenIconState: cuff-broken
brokenName: broken cables
brokenDesc: These cables are broken in several places and don't seem very useful.
startCuffSound: /Audio/Items/Handcuffs/rope_start.ogg
endCuffSound: /Audio/Items/Handcuffs/rope_end.ogg
startUncuffSound: /Audio/Items/Handcuffs/rope_start.ogg
endUncuffSound: /Audio/Items/Handcuffs/rope_breakout.ogg
startBreakoutSound: /Audio/Items/Handcuffs/rope_takeoff.ogg
- type: Sprite
sprite: Objects/Misc/cablecuffs.rsi
state: cuff
@@ -30,7 +57,9 @@
- type: Icon
sprite: Objects/Misc/cablecuffs.rsi
state: cuff
color: red
- type: Clothing
sprite: Objects/Misc/cablecuffs.rsi
color: red
Slots: [belt]

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 B

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -13,6 +13,51 @@
1
]
]
},
{
"name": "cuff-broken",
"directions": 1,
"delays": [
[
1
]
]
},
{
"name": "body-overlay-2",
"directions": 4,
"delays": [
[
1
],
[
1
],
[
1
],
[
1
]
]
},
{
"name": "body-overlay-4",
"directions": 4,
"delays": [
[
1
],
[
1
],
[
1
],
[
1
]
]
},
{
"name": "inhand-left",

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1 +1,8 @@
{"version": 1, "size": {"x": 32, "y": 32}, "states": [{"name": "handcuff", "directions": 1, "delays": [[1.0]]}, {"name": "inhand-left", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "inhand-right", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "equipped-BELT", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}]}
{"version": 1, "size": {"x": 32, "y": 32}, "states": [
{"name": "body-overlay-2", "directions": 4, "delays": [[1.0],[1.0],[1.0],[1.0]]},
{"name": "body-overlay-4", "directions": 4, "delays": [[1.0],[1.0],[1.0],[1.0]]},
{"name": "handcuff", "directions": 1, "delays": [[1.0]]},
{"name": "inhand-left", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]},
{"name": "inhand-right", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]},
{"name": "equipped-BELT", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}
]}