diff --git a/Content.Client/Cuffs/Components/CuffableComponent.cs b/Content.Client/Cuffs/Components/CuffableComponent.cs index a074c044fb..a519a8ad39 100644 --- a/Content.Client/Cuffs/Components/CuffableComponent.cs +++ b/Content.Client/Cuffs/Components/CuffableComponent.cs @@ -52,6 +52,9 @@ namespace Content.Client.Cuffs.Components } } } + + var ev = new CuffedStateChangeEvent(); + _entityManager.EventBus.RaiseLocalEvent(Owner, ref ev); } protected override void OnRemove() diff --git a/Content.Client/Ensnaring/EnsnareableSystem.cs b/Content.Client/Ensnaring/EnsnareableSystem.cs new file mode 100644 index 0000000000..9ccb5dcba5 --- /dev/null +++ b/Content.Client/Ensnaring/EnsnareableSystem.cs @@ -0,0 +1,46 @@ +using Content.Client.Ensnaring.Components; +using Content.Shared.Ensnaring; +using Robust.Client.GameObjects; + +namespace Content.Client.Ensnaring.Visualizers; + +public sealed class EnsnareableSystem : SharedEnsnareableSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnAppearanceChange); + } + + private void OnComponentInit(EntityUid uid, EnsnareableComponent component, ComponentInit args) + { + if(!TryComp(uid, out var sprite)) + return; + + // TODO remove this, this should just be in yaml. + sprite.LayerMapReserveBlank(EnsnaredVisualLayers.Ensnared); + } + + private void OnAppearanceChange(EntityUid uid, EnsnareableComponent component, ref AppearanceChangeEvent args) + { + if (args.Sprite == null || !args.Sprite.LayerMapTryGet(EnsnaredVisualLayers.Ensnared, out var layer)) + return; + + if (args.Component.TryGetData(EnsnareableVisuals.IsEnsnared, out bool isEnsnared)) + { + if (component.Sprite != null) + { + args.Sprite.LayerSetRSI(layer, component.Sprite); + args.Sprite.LayerSetState(layer, component.State); + args.Sprite.LayerSetVisible(layer, isEnsnared); + } + } + } +} + +public enum EnsnaredVisualLayers : byte +{ + Ensnared, +} diff --git a/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerComponent.cs b/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerComponent.cs deleted file mode 100644 index 3c651f6235..0000000000 --- a/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.Utility; - -namespace Content.Client.Ensnaring.Visualizers; -[RegisterComponent] -[Access(typeof(EnsnareableVisualizerSystem))] -public sealed class EnsnareableVisualizerComponent : Component -{ - -} diff --git a/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerSystem.cs b/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerSystem.cs deleted file mode 100644 index c20897fc9e..0000000000 --- a/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerSystem.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Content.Client.Ensnaring.Components; -using Content.Shared.Ensnaring; -using Robust.Client.GameObjects; -using Robust.Shared.Utility; - -namespace Content.Client.Ensnaring.Visualizers; - -public sealed class EnsnareableVisualizerSystem : VisualizerSystem -{ - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnComponentInit); - } - - private void OnComponentInit(EntityUid uid, EnsnareableVisualizerComponent component, ComponentInit args) - { - if(!TryComp(uid, out var sprite)) - return; - - sprite.LayerMapReserveBlank(EnsnaredVisualLayers.Ensnared); - } - - protected override void OnAppearanceChange(EntityUid uid, EnsnareableComponent component, ref AppearanceChangeEvent args) - { - if (args.Component.TryGetData(EnsnareableVisuals.IsEnsnared, out bool isEnsnared)) - { - if (args.Sprite != null && component.Sprite != null) - { - args.Sprite.LayerSetRSI(EnsnaredVisualLayers.Ensnared, component.Sprite); - args.Sprite.LayerSetState(EnsnaredVisualLayers.Ensnared, component.State); - args.Sprite.LayerSetVisible(EnsnaredVisualLayers.Ensnared, isEnsnared); - } - } - } -} - -public enum EnsnaredVisualLayers : byte -{ - Ensnared, -} diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs index dab97ee99b..66d518aa40 100644 --- a/Content.Client/Hands/Systems/HandsSystem.cs +++ b/Content.Client/Hands/Systems/HandsSystem.cs @@ -1,6 +1,6 @@ -using System.Diagnostics.CodeAnalysis; using Content.Client.Animations; using Content.Client.Examine; +using Content.Client.Strip; using Content.Client.Verbs; using Content.Shared.Hands; using Content.Shared.Hands.Components; @@ -13,6 +13,7 @@ using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Timing; +using System.Diagnostics.CodeAnalysis; namespace Content.Client.Hands.Systems { @@ -23,6 +24,7 @@ namespace Content.Client.Hands.Systems [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly StrippableSystem _stripSys = default!; [Dependency] private readonly ExamineSystem _examine = default!; [Dependency] private readonly VerbSystem _verbs = default!; @@ -92,6 +94,8 @@ namespace Content.Client.Hands.Systems component.SortedHands = new(state.HandNames); } + _stripSys.UpdateUi(uid); + if (component.ActiveHand == null && state.ActiveHand == null) return; //edge case @@ -236,6 +240,7 @@ namespace Content.Client.Hands.Systems if (!handComp.Hands.TryGetValue(args.Container.ID, out var hand)) return; UpdateHandVisuals(uid, args.Entity, hand); + _stripSys.UpdateUi(uid); if (uid != _playerManager.LocalPlayer?.ControlledEntity) return; @@ -251,6 +256,7 @@ namespace Content.Client.Hands.Systems if (!handComp.Hands.TryGetValue(args.Container.ID, out var hand)) return; UpdateHandVisuals(uid, args.Entity, hand); + _stripSys.UpdateUi(uid); if (uid != _playerManager.LocalPlayer?.ControlledEntity) return; diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index c516b2b6ed..1a971d4e3c 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -1,110 +1,226 @@ +using Content.Client.Cuffs.Components; +using Content.Client.Examine; +using Content.Client.Hands; using Content.Client.Strip; +using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; +using Content.Client.UserInterface.Systems.Hands.Controls; +using Content.Shared.Ensnaring.Components; +using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; +using Content.Shared.Input; +using Content.Shared.Inventory; using Content.Shared.Strip.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Input; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using static Content.Client.Inventory.ClientInventorySystem; +using static Robust.Client.UserInterface.Control; namespace Content.Client.Inventory { [UsedImplicitly] public sealed class StrippableBoundUserInterface : BoundUserInterface { - public Dictionary<(string ID, string Name), string>? Inventory { get; private set; } - public Dictionary? Hands { get; private set; } - public Dictionary? Handcuffs { get; private set; } - public Dictionary? Ensnare { get; private set; } + private const int ButtonSeparation = 4; + + [Dependency] private readonly IPrototypeManager _protoMan = default!; + [Dependency] private readonly IEntityManager _entMan = default!; + private ExamineSystem _examine = default!; + private InventorySystem _inv = default!; [ViewVariables] private StrippingMenu? _strippingMenu; + public const string HiddenPocketEntityId = "StrippingHiddenEntity"; + private EntityUid _virtualHiddenEntity; + public StrippableBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { + IoCManager.InjectDependencies(this); + _examine = _entMan.EntitySysManager.GetEntitySystem(); + _inv = _entMan.EntitySysManager.GetEntitySystem(); + var title = Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner.Owner, _entMan))); + _strippingMenu = new StrippingMenu(title, this); + _strippingMenu.OnClose += Close; + _virtualHiddenEntity = _entMan.SpawnEntity(HiddenPocketEntityId, MapCoordinates.Nullspace); } protected override void Open() { base.Open(); - - var entMan = IoCManager.Resolve(); - _strippingMenu = new StrippingMenu($"{Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner.Owner, entMan)))}"); - - _strippingMenu.OnClose += Close; - _strippingMenu.OpenCenteredLeft(); - UpdateMenu(); + _strippingMenu?.OpenCenteredLeft(); } protected override void Dispose(bool disposing) { base.Dispose(disposing); + + _entMan.DeleteEntity(_virtualHiddenEntity); + if (!disposing) return; _strippingMenu?.Dispose(); } - private void UpdateMenu() + public void DirtyMenu() { - if (_strippingMenu == null) return; + if (_strippingMenu != null) + _strippingMenu.Dirty = true; + } + + public void UpdateMenu() + { + if (_strippingMenu == null) + return; _strippingMenu.ClearButtons(); - if (Inventory != null) + if (_entMan.TryGetComponent(Owner.Owner, out InventoryComponent? inv) && _protoMan.TryIndex(inv.TemplateId, out var template)) { - foreach (var (slot, name) in Inventory) + foreach (var slot in template.Slots) { - _strippingMenu.AddButton(slot.Name, name, (ev) => - { - SendMessage(new StrippingInventoryButtonPressed(slot.ID)); - }); + AddInventoryButton(slot.Name, template, inv); } } - if (Hands != null) + if (_entMan.TryGetComponent(Owner.Owner, out HandsComponent? handsComp)) { - foreach (var (hand, name) in Hands) + // good ol hands shit code. there is a GuiHands comparer that does the same thing... but these are hands + // and not gui hands... which are different... + foreach (var hand in handsComp.Hands.Values) { - _strippingMenu.AddButton(hand, name, (ev) => - { - SendMessage(new StrippingHandButtonPressed(hand)); - }); + if (hand.Location != HandLocation.Right) + continue; + + AddHandButton(hand); + } + + foreach (var hand in handsComp.Hands.Values) + { + if (hand.Location != HandLocation.Middle) + continue; + + AddHandButton(hand); + } + + foreach (var hand in handsComp.Hands.Values) + { + if (hand.Location != HandLocation.Left) + continue; + + AddHandButton(hand); } } - if (Handcuffs != null) + // snare-removal button. This is just the old button before the change to item slots. It is pretty out of place. + if (_entMan.TryGetComponent(Owner.Owner, out SharedEnsnareableComponent? snare) && snare.IsEnsnared) { - foreach (var (id, name) in Handcuffs) + var button = new Button() { - _strippingMenu.AddButton(Loc.GetString("strippable-bound-user-interface-stripping-menu-handcuffs-button"), name, (ev) => - { - SendMessage(new StrippingHandcuffButtonPressed(id)); - }); - } + Text = Loc.GetString("strippable-bound-user-interface-stripping-menu-ensnare-button"), + StyleClasses = { StyleBase.ButtonOpenRight } + }; + + button.OnPressed += (_) => SendMessage(new StrippingEnsnareButtonPressed()); + + _strippingMenu.SnareContainer.AddChild(button); } - if (Ensnare != null) - { - foreach (var (id, name) in Ensnare) - { - _strippingMenu.AddButton(Loc.GetString("strippable-bound-user-interface-stripping-menu-ensnare-button"), name, (ev) => - { - SendMessage(new StrippingEnsnareButtonPressed(id)); - }); - } - } + // TODO fix layout container measuring (its broken atm). + // _strippingMenu.InvalidateMeasure(); + // _strippingMenu.Contents.Measure(Vector2.Infinity); + + // TODO allow windows to resize based on content's desired size + + // for now: shit-code + // this breaks for drones (too many hands, lots of empty vertical space), and looks shit for monkeys and the like. + // but the window is realizable, so eh. + _strippingMenu.SetSize = (220, snare?.IsEnsnared == true ? 550 : 530); } - protected override void UpdateState(BoundUserInterfaceState state) + private void AddHandButton(Hand hand) { - base.UpdateState(state); + var button = new HandButton(hand.Name, hand.Location); - if (state is not StrippingBoundUserInterfaceState stripState) return; + button.Pressed += SlotPressed; - Inventory = stripState.Inventory; - Hands = stripState.Hands; - Handcuffs = stripState.Handcuffs; - Ensnare = stripState.Ensnare; + if (_entMan.TryGetComponent(hand.HeldEntity, out HandVirtualItemComponent? virt)) + { + button.Blocked = true; + if (_entMan.TryGetComponent(Owner.Owner, out CuffableComponent? cuff) && cuff.Container.Contains(virt.BlockingEntity)) + button.BlockedRect.MouseFilter = MouseFilterMode.Ignore; + } + + UpdateEntityIcon(button, hand.HeldEntity); + _strippingMenu!.HandsContainer.AddChild(button); + } - UpdateMenu(); + private void SlotPressed(GUIBoundKeyEventArgs ev, SlotControl slot) + { + // TODO: allow other interactions? Verbs? But they should then generate a pop-up and/or have a delay so the + // user that is being stripped can prevent the verbs from being exectuted. + // So for now: only stripping & examining + if (ev.Function == EngineKeyFunctions.Use) + { + SendMessage(new StrippingSlotButtonPressed(slot.SlotName, slot is HandButton)); + } + else if (ev.Function == ContentKeyFunctions.ExamineEntity && slot.Entity != null) + { + _examine.DoExamine(slot.Entity.Value); + return; + } + + if (ev.Function != EngineKeyFunctions.Use) + return; + } + + private void AddInventoryButton(string slotId, InventoryTemplatePrototype template, InventoryComponent inv) + { + if (!_inv.TryGetSlotContainer(inv.Owner, slotId, out var container, out var slotDef, inv)) + return; + + var entity = container.ContainedEntity; + + // If this is a full pocket, obscure the real entity + if (entity != null && slotDef.StripHidden) + entity = _virtualHiddenEntity; + + var button = new SlotButton(new SlotData(slotDef, container)); + button.Pressed += SlotPressed; + + _strippingMenu!.InventoryContainer.AddChild(button); + + UpdateEntityIcon(button, entity); + + LayoutContainer.SetPosition(button, slotDef.StrippingWindowPos * (SlotControl.DefaultButtonSize + ButtonSeparation)); + } + + private void UpdateEntityIcon(SlotControl button, EntityUid? entity) + { + // Hovering, highlighting & storage are features of general hands & inv GUIs. This UI just re-uses these because I'm lazy. + button.ClearHover(); + button.StorageButton.Visible = false; + + if (entity == null) + { + button.SpriteView.Sprite = null; + return; + } + + SpriteComponent? sprite; + if (_entMan.TryGetComponent(entity, out HandVirtualItemComponent? virt)) + _entMan.TryGetComponent(virt.BlockingEntity, out sprite); + else if (!_entMan.TryGetComponent(entity, out sprite)) + return; + + button.SpriteView.Sprite = sprite; } } } diff --git a/Content.Client/Strip/StrippableSystem.cs b/Content.Client/Strip/StrippableSystem.cs new file mode 100644 index 0000000000..59083c4515 --- /dev/null +++ b/Content.Client/Strip/StrippableSystem.cs @@ -0,0 +1,43 @@ +using Content.Client.Inventory; +using Content.Shared.Cuffs.Components; +using Content.Shared.Ensnaring.Components; +using Content.Shared.Hands; +using Content.Shared.Inventory.Events; +using Robust.Client.GameObjects; + +namespace Content.Client.Strip; + +/// +/// This is the client-side stripping system, which just triggers UI updates on events. +/// +public sealed class StrippableSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnCuffStateChange); + SubscribeLocalEvent(UpdateUi); + SubscribeLocalEvent(UpdateUi); + SubscribeLocalEvent(UpdateUi); + SubscribeLocalEvent(UpdateUi); + SubscribeLocalEvent(UpdateUi); + } + + private void OnCuffStateChange(EntityUid uid, StrippableComponent component, ref CuffedStateChangeEvent args) + { + UpdateUi(uid, component); + } + + public void UpdateUi(EntityUid uid, StrippableComponent? component = null, EntityEventArgs? args = null) + { + if (!TryComp(uid, out ClientUserInterfaceComponent? uiComp)) + return; + + foreach (var ui in uiComp.Interfaces) + { + if (ui is StrippableBoundUserInterface stripUi) + stripUi.DirtyMenu(); + } + } +} diff --git a/Content.Client/Strip/StrippingMenu.cs b/Content.Client/Strip/StrippingMenu.cs index 42a8747a6c..eea867b794 100644 --- a/Content.Client/Strip/StrippingMenu.cs +++ b/Content.Client/Strip/StrippingMenu.cs @@ -1,64 +1,45 @@ -using System; -using Content.Client.Stylesheets; -using Robust.Client.UserInterface; +using Content.Client.Inventory; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Timing; using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Strip { public sealed class StrippingMenu : DefaultWindow { - private readonly BoxContainer _vboxContainer; + public LayoutContainer InventoryContainer = new(); + public BoxContainer HandsContainer = new() { Orientation = LayoutOrientation.Horizontal }; + public BoxContainer SnareContainer = new(); + private StrippableBoundUserInterface _bui; + public bool Dirty = true; - public StrippingMenu(string title) + public StrippingMenu(string title, StrippableBoundUserInterface bui) { - MinSize = SetSize = (400, 620); Title = title; + _bui = bui; - _vboxContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - VerticalExpand = true, - SeparationOverride = 5, - }; - - Contents.AddChild(_vboxContainer); + var box = new BoxContainer() { Orientation = LayoutOrientation.Vertical, Margin = new Thickness(0, 8) }; + Contents.AddChild(box); + box.AddChild(SnareContainer); + box.AddChild(HandsContainer); + box.AddChild(InventoryContainer); } public void ClearButtons() { - _vboxContainer.DisposeAllChildren(); + InventoryContainer.DisposeAllChildren(); + HandsContainer.DisposeAllChildren(); + SnareContainer.DisposeAllChildren(); } - public void AddButton(string title, string name, Action onPressed) + protected override void FrameUpdate(FrameEventArgs args) { - var button = new Button() - { - Text = name, - StyleClasses = { StyleBase.ButtonOpenRight } - }; + if (!Dirty) + return; - button.OnPressed += onPressed; - - _vboxContainer.AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalExpand = true, - SeparationOverride = 5, - Children = - { - new Label() - { - Text = $"{title}:" - }, - new Control() - { - HorizontalExpand = true - }, - button, - } - }); + Dirty = false; + _bui.UpdateMenu(); } } } diff --git a/Content.Client/UserInterface/Controls/SlotControl.cs b/Content.Client/UserInterface/Controls/SlotControl.cs index ce60db5ade..245a71ecbe 100644 --- a/Content.Client/UserInterface/Controls/SlotControl.cs +++ b/Content.Client/UserInterface/Controls/SlotControl.cs @@ -1,4 +1,4 @@ -using Content.Client.Cooldown; +using Content.Client.Cooldown; using Content.Client.UserInterface.Systems.Inventory.Controls; using Robust.Client.Graphics; using Robust.Client.UserInterface; @@ -12,6 +12,8 @@ namespace Content.Client.UserInterface.Controls { private const string HighlightShader = "SelectionOutlineInrange"; + public static int DefaultButtonSize = 64; + public TextureRect ButtonRect { get; } public TextureRect BlockedRect { get; } public TextureRect HighlightRect { get; } @@ -111,7 +113,7 @@ namespace Content.Client.UserInterface.Controls { IoCManager.InjectDependencies(this); Name = "SlotButton_null"; - MinSize = (64, 64); + MinSize = (DefaultButtonSize, DefaultButtonSize); AddChild(ButtonRect = new TextureRect { TextureScale = (2, 2), diff --git a/Content.Server/Cuffs/Components/CuffableComponent.cs b/Content.Server/Cuffs/Components/CuffableComponent.cs index 6d85332d64..cfa2ebf2a7 100644 --- a/Content.Server/Cuffs/Components/CuffableComponent.cs +++ b/Content.Server/Cuffs/Components/CuffableComponent.cs @@ -2,12 +2,15 @@ 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; @@ -17,9 +20,6 @@ using Robust.Shared.Player; namespace Content.Server.Cuffs.Components { - [ByRefEvent] - public readonly struct CuffedStateChangeEvent {} - [RegisterComponent] [ComponentReference(typeof(SharedCuffableComponent))] public sealed class CuffableComponent : SharedCuffableComponent @@ -39,18 +39,12 @@ namespace Content.Server.Cuffs.Components public IReadOnlyList StoredEntities => Container.ContainedEntities; - /// - /// Container of various handcuffs currently applied to the entity. - /// - [ViewVariables(VVAccess.ReadOnly)] - public Container Container { get; set; } = default!; - private bool _uncuffing; protected override void Initialize() { base.Initialize(); - Container = _sysMan.GetEntitySystem().EnsureContainer(Owner, _componentFactory.GetComponentName(GetType())); + Owner.EnsureComponentWarn(); } @@ -101,19 +95,19 @@ namespace Content.Server.Cuffs.Components return true; } - var sys = _sysMan.GetEntitySystem(); + var sys = _entMan.EntitySysManager.GetEntitySystem(); // Success! sys.TryDrop(user, handcuff); Container.Insert(handcuff); CanStillInteract = _entMan.TryGetComponent(Owner, out HandsComponent? ownerHands) && ownerHands.Hands.Count() > CuffedHandCount; - _sysMan.GetEntitySystem().UpdateCanMove(Owner); + _entMan.EntitySysManager.GetEntitySystem().UpdateCanMove(Owner); var ev = new CuffedStateChangeEvent(); _entMan.EventBus.RaiseLocalEvent(Owner, ref ev, true); UpdateAlert(); - UpdateHeldItems(); + UpdateHeldItems(handcuff); Dirty(_entMan); return true; } @@ -126,29 +120,43 @@ namespace Content.Server.Cuffs.Components } /// - /// Check how many items the user is holding and if it's more than the number of cuffed hands, drop some items. + /// Adds virtual cuff items to the user's hands. /// - public void UpdateHeldItems() + 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 sys = _sysMan.GetEntitySystem(); + var handSys = _entMan.EntitySysManager.GetEntitySystem(); - var freeHandCount = handsComponent.Hands.Count() - CuffedHandCount; - - foreach (var hand in handsComponent.Hands.Values) + var freeHands = 0; + foreach (var hand in handSys.EnumerateHands(Owner, handsComponent)) { - if (hand.IsEmpty) - continue; - - if (freeHandCount > 0) + if (hand.HeldEntity == null) { - freeHandCount--; + freeHands++; continue; } - sys.TryDrop(Owner, hand, checkActionBlocker: false, handsComp: handsComponent); + // Is this entity removable? (it might be an existing handcuff blocker) + if (_entMan.HasComponent(hand.HeldEntity)) + continue; + + handSys.DoDrop(Owner, hand, true, handsComponent); + freeHands++; + if (freeHands == 2) + break; } + + var virtSys = _entMan.EntitySysManager.GetEntitySystem(); + + if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem1)) + _entMan.EnsureComponent(virtItem1.Value); + + if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem2)) + _entMan.EnsureComponent(virtItem2.Value); } /// @@ -247,7 +255,7 @@ namespace Content.Server.Cuffs.Components { SoundSystem.Play(cuff.EndUncuffSound.GetSound(), Filter.Pvs(Owner), Owner); - Container.ForceRemove(cuffsToRemove.Value); + _entMan.EntitySysManager.GetEntitySystem().DeleteInHandsMatching(user, cuffsToRemove.Value); _entMan.EntitySysManager.GetEntitySystem().PickupOrDrop(user, cuffsToRemove.Value); if (cuff.BreakOnRemove) @@ -264,12 +272,9 @@ namespace Content.Server.Cuffs.Components } } - if (_entMan.TryGetComponent(Owner, out HandsComponent? handsComponent)) - CanStillInteract = handsComponent.SortedHands.Count() > CuffedHandCount; - else - CanStillInteract = true; + CanStillInteract = _entMan.TryGetComponent(Owner, out HandsComponent? handsComponent) && handsComponent.SortedHands.Count() > CuffedHandCount; + _entMan.EntitySysManager.GetEntitySystem().UpdateCanMove(Owner); - _sysMan.GetEntitySystem().UpdateCanMove(Owner); var ev = new CuffedStateChangeEvent(); _entMan.EventBus.RaiseLocalEvent(Owner, ref ev, true); UpdateAlert(); diff --git a/Content.Server/Cuffs/Components/HandcuffComponent.cs b/Content.Server/Cuffs/Components/HandcuffComponent.cs index 29fb0539cb..a3bad7b3d4 100644 --- a/Content.Server/Cuffs/Components/HandcuffComponent.cs +++ b/Content.Server/Cuffs/Components/HandcuffComponent.cs @@ -179,6 +179,8 @@ namespace Content.Server.Cuffs.Components 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)) diff --git a/Content.Server/Cuffs/CuffableSystem.cs b/Content.Server/Cuffs/CuffableSystem.cs index 1cfb07b524..50410f44f0 100644 --- a/Content.Server/Cuffs/CuffableSystem.cs +++ b/Content.Server/Cuffs/CuffableSystem.cs @@ -8,6 +8,9 @@ using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Shared.Player; using Content.Shared.Interaction; +using Robust.Shared.Audio; +using Robust.Shared.Containers; +using Content.Server.Hands.Systems; using Content.Shared.MobState.EntitySystems; namespace Content.Server.Cuffs @@ -16,6 +19,7 @@ namespace Content.Server.Cuffs 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 SharedMobStateSystem _mobState = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; @@ -28,6 +32,13 @@ namespace Content.Server.Cuffs SubscribeLocalEvent(OnUncuffAttempt); SubscribeLocalEvent>(AddUncuffVerb); SubscribeLocalEvent(OnCuffAfterInteract); + SubscribeLocalEvent(OnCuffsRemoved); + } + + private void OnCuffsRemoved(EntityUid uid, CuffableComponent component, EntRemovedFromContainerMessage args) + { + if (args.Container.ID == component.Container.ID) + _virtualSystem.DeleteInHandsMatching(uid, args.Entity); } private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsEvent args) @@ -62,6 +73,8 @@ namespace Content.Server.Cuffs return; } + // TODO these messages really need third-party variants. I.e., "{$user} starts cuffing {$target}!" + if (component.Broken) { _popup.PopupEntity(Loc.GetString("handcuff-component-cuffs-broken-error"), args.User, Filter.Entities(args.User)); @@ -93,7 +106,7 @@ namespace Content.Server.Cuffs else { _popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-target-message",("targetName", args.Target)), args.User, Filter.Entities(args.User)); - _popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-by-other-message",("otherName", args.User)), target, Filter.Entities(args.User)); + _popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-by-other-message",("otherName", args.User)), target, Filter.Entities(args.Target.Value)); } _audio.PlayPvs(component.StartCuffSound, uid); diff --git a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs index 2bd21ca207..618b67a46c 100644 --- a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs +++ b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using Content.Server.DoAfter; using Content.Server.Ensnaring.Components; using Content.Shared.Alert; @@ -64,6 +64,7 @@ public sealed partial class EnsnareableSystem component.Ensnared = target; ensnareable.Container.Insert(component.Owner); ensnareable.IsEnsnared = true; + Dirty(ensnareable); UpdateAlert(ensnareable); var ev = new EnsnareEvent(component.WalkSpeed, component.SprintSpeed); @@ -127,6 +128,7 @@ public sealed partial class EnsnareableSystem ensnareable.Container.ForceRemove(component.Owner); ensnareable.IsEnsnared = false; + Dirty(ensnareable); component.Ensnared = null; UpdateAlert(ensnareable); diff --git a/Content.Server/Ensnaring/EnsnareableSystem.cs b/Content.Server/Ensnaring/EnsnareableSystem.cs index e9ff866da8..9c216ce47d 100644 --- a/Content.Server/Ensnaring/EnsnareableSystem.cs +++ b/Content.Server/Ensnaring/EnsnareableSystem.cs @@ -1,4 +1,4 @@ -using Content.Server.Ensnaring.Components; +using Content.Server.Ensnaring.Components; using Content.Server.Popups; using Content.Shared.Ensnaring; using Content.Shared.Ensnaring.Components; @@ -37,6 +37,7 @@ public sealed partial class EnsnareableSystem : SharedEnsnareableSystem component.Container.Remove(args.EnsnaringEntity); component.IsEnsnared = false; + Dirty(component); ensnaring.Ensnared = null; _popup.PopupEntity(Loc.GetString("ensnare-component-try-free-complete", ("ensnare", args.EnsnaringEntity)), diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index 8f8b08fb8f..5e53fb86d8 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -15,7 +15,6 @@ namespace Content.Server.Entry "ClientEntitySpawner", "HandheldGPS", "CableVisualizer", - "EnsnareableVisualizer", }; } } diff --git a/Content.Server/Hands/Systems/HandVirtualItemSystem.cs b/Content.Server/Hands/Systems/HandVirtualItemSystem.cs index eede44cabd..efb0fab953 100644 --- a/Content.Server/Hands/Systems/HandVirtualItemSystem.cs +++ b/Content.Server/Hands/Systems/HandVirtualItemSystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using JetBrains.Annotations; +using System.Diagnostics.CodeAnalysis; namespace Content.Server.Hands.Systems { @@ -10,16 +11,21 @@ namespace Content.Server.Hands.Systems { [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user) + 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(user).Coordinates; - var virtualItem = EntityManager.SpawnEntity("HandVirtualItem", pos); - var virtualItemComp = EntityManager.GetComponent(virtualItem); + virtualItem = EntityManager.SpawnEntity("HandVirtualItem", pos); + var virtualItemComp = EntityManager.GetComponent(virtualItem.Value); virtualItemComp.BlockingEntity = blockingEnt; - _handsSystem.DoPickup(user, hand, virtualItem); + _handsSystem.DoPickup(user, hand, virtualItem.Value); return true; } diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index 851cd036b9..e7bdc1b113 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -106,23 +106,6 @@ namespace Content.Server.Hands.Systems } #region EntityInsertRemove - public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, SharedHandsComponent? hands = null) - { - base.DoDrop(uid, hand,doDropInteraction, hands); - - // update gui of anyone stripping this entity. - _strippableSystem.SendUpdate(uid); - } - - public override void DoPickup(EntityUid uid, Hand hand, EntityUid entity, SharedHandsComponent? hands = null) - { - base.DoPickup(uid, hand, entity, hands); - - // update gui of anyone stripping this entity. - _strippableSystem.SendUpdate(uid); - } - - public override void PickupAnimation(EntityUid item, EntityCoordinates initialPosition, Vector2 finalPosition, EntityUid? exclude) { diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index b4727b77b4..8974c8ae25 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -1,24 +1,20 @@ -using System.Threading; using Content.Server.Cuffs.Components; using Content.Server.DoAfter; using Content.Server.Ensnaring; using Content.Server.Ensnaring.Components; using Content.Server.Hands.Components; -using Content.Server.Inventory; -using Content.Server.UserInterface; -using Content.Shared.Ensnaring.Components; +using Content.Shared.CombatMode; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; using Content.Shared.Interaction.Events; using Content.Shared.Inventory; -using Content.Shared.Inventory.Events; using Content.Shared.Popups; using Content.Shared.Strip.Components; using Content.Shared.Verbs; -using Content.Shared.CombatMode; using Robust.Server.GameObjects; using Robust.Shared.Player; +using System.Threading; namespace Content.Server.Strip { @@ -38,93 +34,69 @@ namespace Content.Server.Strip base.Initialize(); SubscribeLocalEvent>(AddStripVerb); - SubscribeLocalEvent>(AddExamineVerb); - SubscribeLocalEvent(OnDidEquip); - SubscribeLocalEvent(OnDidUnequip); - SubscribeLocalEvent(OnCompInit); - SubscribeLocalEvent(OnCuffStateChange); - SubscribeLocalEvent(OnEnsnareChange); // BUI - SubscribeLocalEvent(OnStripInvButtonMessage); - SubscribeLocalEvent(OnStripHandMessage); - SubscribeLocalEvent(OnStripHandcuffMessage); - SubscribeLocalEvent(OnStripEnsnareMessage); + SubscribeLocalEvent(OnStripButtonPressed); + SubscribeLocalEvent(OnStripEnsnareMessage); } - private void OnStripHandcuffMessage(EntityUid uid, StrippableComponent component, StrippingHandcuffButtonPressed args) + + private void OnStripEnsnareMessage(EntityUid uid, EnsnareableComponent component, StrippingEnsnareButtonPressed args) { if (args.Session.AttachedEntity is not {Valid: true} user) return; - if (TryComp(component.Owner, out var cuffed)) - { - foreach (var entity in cuffed.StoredEntities) - { - if (entity != args.Handcuff) continue; - cuffed.TryUncuff(user, entity); - return; - } - } - } - - private void OnStripEnsnareMessage(EntityUid uid, StrippableComponent component, StrippingEnsnareButtonPressed args) - { - if (args.Session.AttachedEntity is not {Valid: true} user) - return; - - var ensnareQuery = GetEntityQuery(); - - foreach (var entity in ensnareQuery.GetComponent(uid).Container.ContainedEntities) + foreach (var entity in component.Container.ContainedEntities) { if (!TryComp(entity, out var ensnaring)) continue; - if (entity != args.Ensnare) - continue; - _ensnaring.TryFree(component.Owner, ensnaring, user); return; } } - private void OnStripHandMessage(EntityUid uid, StrippableComponent component, StrippingHandButtonPressed args) + private void OnStripButtonPressed(EntityUid uid, StrippableComponent component, StrippingSlotButtonPressed args) { if (args.Session.AttachedEntity is not {Valid: true} user || !TryComp(user, out var userHands)) return; - var placingItem = userHands.ActiveHandEntity != null; - - if (TryComp(component.Owner, out var hands)) + if (args.IsHand) { - if (hands.Hands.TryGetValue(args.Hand, out var hand) && !hand.IsEmpty) - placingItem = false; - - if (placingItem) - PlaceActiveHandItemInHands(user, args.Hand, component); - else - TakeItemFromHands(user, args.Hand, component); + StripHand(uid, user, args.Slot, component, userHands); + return; } + + if (!TryComp(component.Owner, out var inventory)) + return; + + var hasEnt = _inventorySystem.TryGetSlotEntity(component.Owner, args.Slot, out _, inventory); + + if (userHands.ActiveHandEntity != null && !hasEnt) + PlaceActiveHandItemInInventory(user, args.Slot, component); + else if (userHands.ActiveHandEntity == null && hasEnt) + TakeItemFromInventory(user, args.Slot, component); } - private void OnStripInvButtonMessage(EntityUid uid, StrippableComponent component, StrippingInventoryButtonPressed args) + private void StripHand(EntityUid target, EntityUid user, string handId, StrippableComponent component, HandsComponent userHands) { - if (args.Session.AttachedEntity is not {Valid: true} user || - !TryComp(user, out var userHands)) + if (!TryComp(target, out var targetHands) + || !targetHands.Hands.TryGetValue(handId, out var hand)) return; - var placingItem = userHands.ActiveHandEntity != null; - - if (TryComp(component.Owner, out var inventory)) + // is the target a handcuff? + if (TryComp(hand.HeldEntity, out HandVirtualItemComponent? virt) + && TryComp(target, out CuffableComponent? cuff) + && cuff.Container.Contains(virt.BlockingEntity)) { - if (_inventorySystem.TryGetSlotEntity(component.Owner, args.Slot, out _, inventory)) - placingItem = false; - - if (placingItem) - PlaceActiveHandItemInInventory(user, args.Slot, component); - else - TakeItemFromInventory(user, args.Slot, component); + cuff.TryUncuff(user, virt.BlockingEntity); + return; } + + if (hand.IsEmpty && userHands.ActiveHandEntity != null) + PlaceActiveHandItemInHands(user, handId, component); + else if (!hand.IsEmpty && userHands.ActiveHandEntity == null) + TakeItemFromHands(user, handId, component); } public void StartOpeningStripper(EntityUid user, StrippableComponent component, bool openInCombat = false) @@ -140,124 +112,6 @@ namespace Content.Server.Strip } } - private void OnCompInit(EntityUid uid, StrippableComponent component, ComponentInit args) - { - EnsureComp(uid); - SendUpdate(uid, component); - } - - private void OnCuffStateChange(EntityUid uid, StrippableComponent component, ref CuffedStateChangeEvent args) - { - UpdateState(uid, component); - } - - private void OnEnsnareChange(EntityUid uid, StrippableComponent component, EnsnaredChangedEvent args) - { - SendUpdate(uid, component); - } - - private void OnDidUnequip(EntityUid uid, StrippableComponent component, DidUnequipEvent args) - { - SendUpdate(uid, component); - } - - private void OnDidEquip(EntityUid uid, StrippableComponent component, DidEquipEvent args) - { - SendUpdate(uid, component); - } - - public void SendUpdate(EntityUid uid, StrippableComponent? strippableComponent = null) - { - var bui = uid.GetUIOrNull(StrippingUiKey.Key); - - if (!Resolve(uid, ref strippableComponent, false) || bui == null) - { - return; - } - - var cuffs = new Dictionary(); - var ensnare = new Dictionary(); - var inventory = new Dictionary<(string ID, string Name), string>(); - var hands = new Dictionary(); - - if (TryComp(uid, out CuffableComponent? cuffed)) - { - foreach (var entity in cuffed.StoredEntities) - { - var name = Name(entity); - cuffs.Add(entity, name); - } - } - - var ensnareQuery = GetEntityQuery(); - - if (ensnareQuery.TryGetComponent(uid, out var _)) - { - foreach (var entity in ensnareQuery.GetComponent(uid).Container.ContainedEntities) - { - var name = Name(entity); - ensnare.Add(entity, name); - } - } - - if (_inventorySystem.TryGetSlots(uid, out var slots)) - { - foreach (var slot in slots) - { - var name = "None"; - - if (_inventorySystem.TryGetSlotEntity(uid, slot.Name, out var item)) - { - if (!slot.StripHidden) - { - name = Name(item.Value); - } - - else - { - name = Loc.GetString("strippable-bound-user-interface-stripping-menu-obfuscate"); - } - } - - inventory[(slot.Name, slot.DisplayName)] = name; - } - } - - if (TryComp(uid, out HandsComponent? handsComp)) - { - foreach (var hand in handsComp.Hands.Values) - { - if (hand.HeldEntity == null || HasComp(hand.HeldEntity)) - { - hands[hand.Name] = "None"; - continue; - } - - hands[hand.Name] = Name(hand.HeldEntity.Value); - } - } - - bui.SetState(new StrippingBoundUserInterfaceState(inventory, hands, cuffs, ensnare)); - } - - private void AddExamineVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args) - { - if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User) - return; - - if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor)) - return; - - ExamineVerb verb = new() - { - Text = Loc.GetString("strip-verb-get-data-text"), - IconTexture = "/Textures/Interface/VerbIcons/outfit.svg.192dpi.png", - Act = () => StartOpeningStripper(args.User, component, true), - Category = VerbCategory.Examine, - }; - args.Verbs.Add(verb); - } - private void AddStripVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args) { if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User) @@ -275,11 +129,6 @@ namespace Content.Server.Strip args.Verbs.Add(verb); } - private void UpdateState(EntityUid uid, StrippableComponent component) - { - SendUpdate(uid, component); - } - /// /// Places item in user's active hand to an inventory slot. /// @@ -354,8 +203,6 @@ namespace Content.Server.Strip { _inventorySystem.TryEquip(user, component.Owner, held, slot); } - - UpdateState(component.Owner, component); } /// @@ -488,8 +335,6 @@ namespace Content.Server.Strip _handsSystem.PickupOrDrop(user, item.Value); } - - UpdateState(component.Owner, component); } /// diff --git a/Content.Shared/Cuffs/Components/SharedCuffableComponent.cs b/Content.Shared/Cuffs/Components/SharedCuffableComponent.cs index 42523e8843..76c25b15d4 100644 --- a/Content.Shared/Cuffs/Components/SharedCuffableComponent.cs +++ b/Content.Shared/Cuffs/Components/SharedCuffableComponent.cs @@ -1,11 +1,31 @@ +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!; + + /// + /// Container of various handcuffs currently applied to the entity. + /// + [ViewVariables(VVAccess.ReadOnly)] + public Container Container { get; set; } = default!; + + protected override void Initialize() + { + base.Initialize(); + + Container = _sysMan.GetEntitySystem().EnsureContainer(Owner, _componentFactory.GetComponentName(GetType())); + } + [ViewVariables] public bool CanStillInteract { get; set; } = true; diff --git a/Content.Shared/Ensnaring/Components/SharedEnsnareableComponent.cs b/Content.Shared/Ensnaring/Components/SharedEnsnareableComponent.cs index cff199f570..c472fb8ecc 100644 --- a/Content.Shared/Ensnaring/Components/SharedEnsnareableComponent.cs +++ b/Content.Shared/Ensnaring/Components/SharedEnsnareableComponent.cs @@ -1,7 +1,11 @@ -namespace Content.Shared.Ensnaring.Components; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Ensnaring.Components; /// /// Use this on an entity that you would like to be ensnared by anything that has the /// +[NetworkedComponent] public abstract class SharedEnsnareableComponent : Component { /// @@ -26,6 +30,17 @@ public abstract class SharedEnsnareableComponent : Component public bool IsEnsnared; } +[Serializable, NetSerializable] +public sealed class EnsnareableComponentState : ComponentState +{ + public readonly bool IsEnsnared; + + public EnsnareableComponentState(bool isEnsnared) + { + IsEnsnared = isEnsnared; + } +} + public sealed class EnsnaredChangedEvent : EntityEventArgs { public readonly bool IsEnsnared; diff --git a/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs b/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs index e46ddb6034..df47d41568 100644 --- a/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs +++ b/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs @@ -1,5 +1,6 @@ -using Content.Shared.Ensnaring.Components; +using Content.Shared.Ensnaring.Components; using Content.Shared.Movement.Systems; +using Robust.Shared.GameStates; namespace Content.Shared.Ensnaring; @@ -16,6 +17,25 @@ public abstract class SharedEnsnareableSystem : EntitySystem SubscribeLocalEvent(OnEnsnare); SubscribeLocalEvent(OnEnsnareRemove); SubscribeLocalEvent(OnEnsnareChange); + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(EntityUid uid, SharedEnsnareableComponent component, ref ComponentHandleState args) + { + if (args.Current is not EnsnareableComponentState state) + return; + + if (state.IsEnsnared == component.IsEnsnared) + return; + + component.IsEnsnared = state.IsEnsnared; + RaiseLocalEvent(uid, new EnsnaredChangedEvent(component.IsEnsnared)); + } + + private void OnGetState(EntityUid uid, SharedEnsnareableComponent component, ref ComponentGetState args) + { + args.State = new EnsnareableComponentState(component.IsEnsnared); } private void OnEnsnare(EntityUid uid, SharedEnsnareableComponent component, EnsnareEvent args) diff --git a/Content.Shared/Examine/ExamineSystemShared.cs b/Content.Shared/Examine/ExamineSystemShared.cs index 74e5a838d2..18662ddab8 100644 --- a/Content.Shared/Examine/ExamineSystemShared.cs +++ b/Content.Shared/Examine/ExamineSystemShared.cs @@ -66,6 +66,10 @@ namespace Content.Shared.Examine [Pure] public bool CanExamine(EntityUid examiner, EntityUid examined) { + // special check for client-side entities stored in null-space for some UI guff. + if (examined.IsClientSide()) + return true; + return !Deleted(examined) && CanExamine(examiner, EntityManager.GetComponent(examined).MapPosition, entity => entity == examiner || entity == examined, examined); } @@ -73,6 +77,9 @@ namespace Content.Shared.Examine [Pure] public virtual bool CanExamine(EntityUid examiner, MapCoordinates target, Ignored? predicate = null, EntityUid? examined = null, ExaminerComponent? examinerComp = null) { + // TODO occluded container checks + // also requires checking if the examiner has either a storage or stripping UI open, as the item may be accessible via that UI + if (!Resolve(examiner, ref examinerComp, false)) return false; diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs index 3a779c37ba..fa9c9edd1b 100644 --- a/Content.Shared/Inventory/InventorySystem.Equip.cs +++ b/Content.Shared/Inventory/InventorySystem.Equip.cs @@ -242,9 +242,10 @@ public abstract partial class InventorySystem // Is the actor currently stripping the target? Here we could check if the actor has the stripping UI open, but // that requires server/client specific code. so lets just check if they **could** open the stripping UI. // Note that this doesn't check that the item is equipped by the target, as this is done elsewhere. - return actor != target - && TryComp(target, out SharedStrippableComponent? strip) - && strip.CanBeStripped(actor); + return actor != target && + HasComp(target) && + HasComp(actor) && + HasComp(actor); } public bool CanEquip(EntityUid uid, EntityUid itemUid, string slot, [NotNullWhen(false)] out string? reason, diff --git a/Content.Shared/Inventory/InventoryTemplatePrototype.cs b/Content.Shared/Inventory/InventoryTemplatePrototype.cs index 1bb50ea08f..f282df9ea6 100644 --- a/Content.Shared/Inventory/InventoryTemplatePrototype.cs +++ b/Content.Shared/Inventory/InventoryTemplatePrototype.cs @@ -1,4 +1,4 @@ -using Content.Shared.Whitelist; +using Content.Shared.Whitelist; using Robust.Shared.Prototypes; namespace Content.Shared.Inventory; @@ -24,6 +24,9 @@ public sealed class SlotDefinition [DataField("uiWindowPos", required: true)] public Vector2i UIWindowPosition { get; } + [DataField("strippingWindowPos", required: true)] + public Vector2i StrippingWindowPos { get; } + [DataField("dependsOn")] public string? DependsOn { get; } [DataField("displayName", required: true)] diff --git a/Content.Shared/Strip/Components/SharedStrippableComponent.cs b/Content.Shared/Strip/Components/SharedStrippableComponent.cs index 554043eff7..a40a015e4e 100644 --- a/Content.Shared/Strip/Components/SharedStrippableComponent.cs +++ b/Content.Shared/Strip/Components/SharedStrippableComponent.cs @@ -1,4 +1,4 @@ -using Content.Shared.ActionBlocker; +using Content.Shared.ActionBlocker; using Content.Shared.DragDrop; using Content.Shared.Hands.Components; using Content.Shared.Inventory; @@ -8,18 +8,14 @@ namespace Content.Shared.Strip.Components { public abstract class SharedStrippableComponent : Component, IDraggable { - public bool CanBeStripped(EntityUid by) - { - return by != Owner - && IoCManager.Resolve().HasComponent(@by) - && EntitySystem.Get().CanInteract(@by, Owner); - } - bool IDraggable.CanDrop(CanDropEvent args) { - return args.Target != args.Dragged - && args.Target == args.User - && CanBeStripped(args.User); + var ent = IoCManager.Resolve(); + return args.Target != args.Dragged && + args.Target == args.User && + ent.HasComponent(args.User) && + ent.HasComponent(args.User) && + ent.EntitySysManager.GetEntitySystem().CanInteract(args.User, args.Dragged); } public abstract bool Drop(DragDropEvent args); @@ -32,64 +28,24 @@ namespace Content.Shared.Strip.Components } [NetSerializable, Serializable] - public sealed class StrippingInventoryButtonPressed : BoundUserInterfaceMessage + public sealed class StrippingSlotButtonPressed : BoundUserInterfaceMessage { - public string Slot { get; } + public readonly string Slot; - public StrippingInventoryButtonPressed(string slot) + public readonly bool IsHand; + + public StrippingSlotButtonPressed(string slot, bool isHand) { Slot = slot; - } - } - - [NetSerializable, Serializable] - public sealed class StrippingHandButtonPressed : BoundUserInterfaceMessage - { - public string Hand { get; } - - public StrippingHandButtonPressed(string hand) - { - Hand = hand; - } - } - - [NetSerializable, Serializable] - public sealed class StrippingHandcuffButtonPressed : BoundUserInterfaceMessage - { - public EntityUid Handcuff { get; } - - public StrippingHandcuffButtonPressed(EntityUid handcuff) - { - Handcuff = handcuff; + IsHand = isHand; } } [NetSerializable, Serializable] public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage { - public EntityUid Ensnare { get; } - - public StrippingEnsnareButtonPressed(EntityUid ensnare) + public StrippingEnsnareButtonPressed() { - Ensnare = ensnare; - } - } - - [NetSerializable, Serializable] - public sealed class StrippingBoundUserInterfaceState : BoundUserInterfaceState - { - public Dictionary<(string ID, string Name), string> Inventory { get; } - public Dictionary Hands { get; } - public Dictionary Handcuffs { get; } - public Dictionary Ensnare { get; } - - public StrippingBoundUserInterfaceState(Dictionary<(string ID, string Name), string> inventory, Dictionary hands, Dictionary handcuffs, - Dictionary ensnare) - { - Inventory = inventory; - Hands = hands; - Handcuffs = handcuffs; - Ensnare = ensnare; } } diff --git a/Content.Shared/Strip/Components/SharedStrippingComponent.cs b/Content.Shared/Strip/Components/SharedStrippingComponent.cs index 9131b18edc..ef4c8b241f 100644 --- a/Content.Shared/Strip/Components/SharedStrippingComponent.cs +++ b/Content.Shared/Strip/Components/SharedStrippingComponent.cs @@ -1,4 +1,6 @@ +using Content.Shared.ActionBlocker; using Content.Shared.DragDrop; +using Content.Shared.Hands.Components; namespace Content.Shared.Strip.Components { @@ -10,8 +12,12 @@ namespace Content.Shared.Strip.Components { bool IDragDropOn.CanDragDropOn(DragDropEvent eventArgs) { - if (!IoCManager.Resolve().TryGetComponent(eventArgs.Dragged, out SharedStrippableComponent? strippable)) return false; - return strippable.CanBeStripped(Owner); + var ent = IoCManager.Resolve(); + return eventArgs.Target != eventArgs.Dragged && + eventArgs.Target == eventArgs.User && + ent.HasComponent(eventArgs.Dragged) && + ent.HasComponent(eventArgs.User) && + ent.EntitySysManager.GetEntitySystem().CanInteract(eventArgs.User, eventArgs.Dragged); } bool IDragDropOn.DragDropOn(DragDropEvent eventArgs) diff --git a/Resources/Locale/en-US/strip/strippable-component.ftl b/Resources/Locale/en-US/strip/strippable-component.ftl index 71e9cf3f08..0c7746cc38 100644 --- a/Resources/Locale/en-US/strip/strippable-component.ftl +++ b/Resources/Locale/en-US/strip/strippable-component.ftl @@ -16,6 +16,4 @@ strip-verb-get-data-text = Strip ## UI strippable-bound-user-interface-stripping-menu-title = {$ownerName}'s inventory -strippable-bound-user-interface-stripping-menu-handcuffs-button = Restraints -strippable-bound-user-interface-stripping-menu-ensnare-button = Leg Restraints -strippable-bound-user-interface-stripping-menu-obfuscate = Occupied +strippable-bound-user-interface-stripping-menu-ensnare-button = Remove Leg Restraints \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 09ae684d82..7af56f8620 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -232,7 +232,6 @@ normalState: Generic_mob_burning alternateState: Standing fireStackAlternateState: 3 - - type: EnsnareableVisualizer - type: CombatMode canDisarm: true - type: Climbing diff --git a/Resources/Prototypes/Entities/Virtual/stripping_hidden.yml b/Resources/Prototypes/Entities/Virtual/stripping_hidden.yml new file mode 100644 index 0000000000..86c01b507a --- /dev/null +++ b/Resources/Prototypes/Entities/Virtual/stripping_hidden.yml @@ -0,0 +1,13 @@ +# Special entity that indicates that there is something in a player's pockets, without showing the actual entity. +# This **could** just be done via UI elements, but the existing UI uses sprite-views & the examine-system. So this is easier than special casing this. + +- type: entity + id: StrippingHiddenEntity + name: Hidden Entity + description: There is something in this pocket. #Or maybe they ar... nah... too obvious a joke. + noSpawn: false + components: + - type: Sprite + texture: Interface/VerbIcons/information.svg.192dpi.png + netsync: false + scale: 0.3,0.3 diff --git a/Resources/Prototypes/InventoryTemplates/drone_inventory_template.yml b/Resources/Prototypes/InventoryTemplates/drone_inventory_template.yml index 98f14b7bf0..e7b49c37e5 100644 --- a/Resources/Prototypes/InventoryTemplates/drone_inventory_template.yml +++ b/Resources/Prototypes/InventoryTemplates/drone_inventory_template.yml @@ -6,5 +6,6 @@ slotFlags: HEAD slotGroup: MainHotbar uiWindowPos: 0,0 + strippingWindowPos: 0,0 displayName: Head offset: 0, -0.45 diff --git a/Resources/Prototypes/InventoryTemplates/human_inventory_template.yml b/Resources/Prototypes/InventoryTemplates/human_inventory_template.yml index 6385b08bae..3c7df3cc57 100644 --- a/Resources/Prototypes/InventoryTemplates/human_inventory_template.yml +++ b/Resources/Prototypes/InventoryTemplates/human_inventory_template.yml @@ -6,12 +6,14 @@ slotFlags: FEET stripTime: 3 uiWindowPos: 1,3 + strippingWindowPos: 1,3 displayName: Shoes - name: jumpsuit slotTexture: uniform slotFlags: INNERCLOTHING stripTime: 6 uiWindowPos: 0,2 + strippingWindowPos: 0,2 displayName: Jumpsuit - name: outerClothing slotTexture: suit @@ -19,38 +21,45 @@ slotGroup: MainHotbar stripTime: 6 uiWindowPos: 1,2 + strippingWindowPos: 1,2 displayName: Suit - name: gloves slotTexture: gloves slotFlags: GLOVES uiWindowPos: 2,2 + strippingWindowPos: 2,2 displayName: Gloves - name: neck slotTexture: neck slotFlags: NECK uiWindowPos: 0,1 + strippingWindowPos: 0,1 displayName: Neck - name: mask slotTexture: mask slotFlags: MASK uiWindowPos: 1,1 + strippingWindowPos: 1,1 displayName: Mask - name: eyes slotTexture: glasses slotFlags: EYES stripTime: 3 uiWindowPos: 0,0 + strippingWindowPos: 0,0 displayName: Eyes - name: ears slotTexture: ears slotFlags: EARS stripTime: 3 uiWindowPos: 2,0 + strippingWindowPos: 2,0 displayName: Ears - name: head slotTexture: head slotFlags: HEAD uiWindowPos: 1,0 + strippingWindowPos: 1,0 displayName: Head - name: pocket1 slotTexture: pocket @@ -58,6 +67,7 @@ slotGroup: MainHotbar stripTime: 3 uiWindowPos: 0,3 + strippingWindowPos: 0,4 dependsOn: jumpsuit displayName: Pocket 1 stripHidden: true @@ -67,6 +77,7 @@ slotGroup: MainHotbar stripTime: 3 uiWindowPos: 2,3 + strippingWindowPos: 1,4 dependsOn: jumpsuit displayName: Pocket 2 stripHidden: true @@ -75,6 +86,7 @@ slotFlags: SUITSTORAGE stripTime: 3 uiWindowPos: 2,0 + strippingWindowPos: 2,5 dependsOn: outerClothing displayName: Suit Storage - name: id @@ -83,6 +95,7 @@ slotGroup: SecondHotbar stripTime: 6 uiWindowPos: 2,1 + strippingWindowPos: 2,4 dependsOn: jumpsuit displayName: ID - name: belt @@ -91,6 +104,7 @@ slotGroup: SecondHotbar stripTime: 6 uiWindowPos: 3,1 + strippingWindowPos: 1,5 displayName: Belt - name: back slotTexture: back @@ -98,4 +112,5 @@ slotGroup: SecondHotbar stripTime: 6 uiWindowPos: 3,0 + strippingWindowPos: 0,5 displayName: Back diff --git a/Resources/Prototypes/InventoryTemplates/monkey_inventory_template.yml b/Resources/Prototypes/InventoryTemplates/monkey_inventory_template.yml index 572bd67ec8..3dd1213c48 100644 --- a/Resources/Prototypes/InventoryTemplates/monkey_inventory_template.yml +++ b/Resources/Prototypes/InventoryTemplates/monkey_inventory_template.yml @@ -6,4 +6,5 @@ slotFlags: HEAD slotGroup: MainHotbar uiWindowPos: 0,0 + strippingWindowPos: 0,0 displayName: Head